forked from WHC/Hermit
Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+283
@@ -0,0 +1,283 @@
|
||||
#include "cst_agent.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <getopt.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static void usage(const char *argv0) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s [--workspace DIR] [--session FILE] [--api-base URL]\n"
|
||||
" [--api-key KEY] [--model NAME] [--headless]\n"
|
||||
" [--prompt TEXT] [--demo-ctl] [--ask-key]\n"
|
||||
"\n"
|
||||
"Defaults: api-base=https://api.minimax.cn/v1 model=MiniMax-M2.7\n"
|
||||
"Session: <workspace>/.agent/sessions/<id>.jsonl (CURRENT tracked)\n"
|
||||
"API key: CST_API_KEY / OPENAI_API_KEY / MINIMAX_API_KEY\n"
|
||||
"Base: CST_API_BASE / OPENAI_BASE_URL (OpenAI-compatible …/v1)\n"
|
||||
"Model: CST_MODEL / OPENAI_MODEL\n"
|
||||
"Also: CST_MODELS, CST_AUTO_APPROVE=1, CST_SHELL, CST_EXE_DIR\n"
|
||||
"TUI: /new /session /model /compact /details /help\n"
|
||||
" Enter send · Ctrl+N newline · ↑↓ history · Esc cancel\n"
|
||||
" busy Enter queues · tool perm y/n/a=always · wheel scroll\n",
|
||||
argv0);
|
||||
}
|
||||
|
||||
static int g_ready;
|
||||
static ctl_tool_call_t g_call;
|
||||
|
||||
static void demo_on_ev(const ctl_event_t *ev, void *ud) {
|
||||
(void)ud;
|
||||
if (ev->type == CTL_EV_TEXT)
|
||||
fwrite(ev->text, 1, ev->text_len, stdout);
|
||||
if (ev->type == CTL_EV_CALL_READY) {
|
||||
snprintf(g_call.id, sizeof(g_call.id), "%s", ev->call->id);
|
||||
snprintf(g_call.name, sizeof(g_call.name), "%s", ev->call->name);
|
||||
for (int i = 0; i < g_call.nargs; i++) {
|
||||
free(g_call.args[i].value);
|
||||
g_call.args[i].value = NULL;
|
||||
}
|
||||
g_call.nargs = 0;
|
||||
for (int i = 0; i < ev->call->nargs; i++) {
|
||||
snprintf(g_call.args[i].key, sizeof(g_call.args[i].key), "%s",
|
||||
ev->call->args[i].key);
|
||||
g_call.args[i].value_len = ev->call->args[i].value_len;
|
||||
g_call.args[i].value = malloc(ev->call->args[i].value_len + 1);
|
||||
memcpy(g_call.args[i].value, ev->call->args[i].value,
|
||||
ev->call->args[i].value_len);
|
||||
g_call.args[i].value[ev->call->args[i].value_len] = 0;
|
||||
g_call.nargs++;
|
||||
}
|
||||
g_ready = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int demo_ctl_impl(const char *workspace) {
|
||||
printf("=== demo CTL (byte-at-a-time feed) ===\n");
|
||||
g_ready = 0;
|
||||
memset(&g_call, 0, sizeof(g_call));
|
||||
ctl_parser_t *p = ctl_parser_create(demo_on_ev, NULL);
|
||||
const char *stream =
|
||||
"Reading README.\n"
|
||||
"call t1 read_file\n"
|
||||
"arg path 9\n"
|
||||
"README.md"
|
||||
"end\n";
|
||||
for (size_t i = 0; stream[i]; i++)
|
||||
ctl_parser_feed(p, &stream[i], 1);
|
||||
if (ctl_parser_finish(p) != CTL_OK || !g_ready) {
|
||||
fprintf(stderr, "demo: CTL failed\n");
|
||||
ctl_parser_destroy(p);
|
||||
return 1;
|
||||
}
|
||||
printf("\npending_peak=%zu\n", ctl_parser_pending_peak(p));
|
||||
ctl_parser_destroy(p);
|
||||
|
||||
cst_tool_result_t r = {0};
|
||||
cst_tool_dispatch(workspace, &g_call, &r);
|
||||
printf("[tool result exit=%d]\n%.*s\n", r.exit_code, (int)r.out_len,
|
||||
r.out ? r.out : "");
|
||||
cst_tool_result_free(&r);
|
||||
for (int i = 0; i < g_call.nargs; i++)
|
||||
free(g_call.args[i].value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
os_set_prog_path(argv[0]);
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
|
||||
cst_agent_config_t cfg = {0};
|
||||
char cwd[4096];
|
||||
#ifdef _WIN32
|
||||
if (!_getcwd(cwd, (int)sizeof(cwd)))
|
||||
#else
|
||||
if (!getcwd(cwd, sizeof(cwd)))
|
||||
#endif
|
||||
snprintf(cwd, sizeof(cwd), ".");
|
||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", cwd);
|
||||
cfg.session_path[0] = 0; /* filled after workspace opts via .agent/store */
|
||||
/* OpenAI-compatible base URL aliases */
|
||||
const char *env_base = getenv("CST_API_BASE");
|
||||
if (!env_base || !env_base[0])
|
||||
env_base = getenv("OPENAI_BASE_URL");
|
||||
if (!env_base || !env_base[0])
|
||||
env_base = getenv("OPENAI_API_BASE");
|
||||
const char *env_model = getenv("CST_MODEL");
|
||||
if (!env_model || !env_model[0])
|
||||
env_model = getenv("OPENAI_MODEL");
|
||||
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s",
|
||||
env_base && env_base[0] ? env_base : "https://api.minimax.cn/v1");
|
||||
cfg.api_key[0] = 0;
|
||||
snprintf(cfg.model, sizeof(cfg.model), "%s",
|
||||
env_model && env_model[0] ? env_model : "MiniMax-M2.7");
|
||||
cfg.headless = 0;
|
||||
cfg.confirm_shell = 1;
|
||||
cfg.confirm_write = 1;
|
||||
cfg.auto_approve = 0;
|
||||
if (getenv("CST_AUTO_APPROVE"))
|
||||
cfg.auto_approve = 1;
|
||||
|
||||
int do_demo = 0;
|
||||
int force_ask_key = 0;
|
||||
int session_explicit = 0;
|
||||
const char *prompt = NULL;
|
||||
int key_from_cli = 0;
|
||||
|
||||
static struct option opts[] = {
|
||||
{"workspace", required_argument, 0, 'w'},
|
||||
{"session", required_argument, 0, 's'},
|
||||
{"api-base", required_argument, 0, 'b'},
|
||||
{"api-key", required_argument, 0, 'k'},
|
||||
{"model", required_argument, 0, 'm'},
|
||||
{"headless", no_argument, 0, 'H'},
|
||||
{"prompt", required_argument, 0, 'p'},
|
||||
{"demo-ctl", no_argument, 0, 'd'},
|
||||
{"ask-key", no_argument, 0, 'a'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}};
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, "w:s:b:k:m:Hp:dah", opts, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'w':
|
||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
||||
break;
|
||||
case 's':
|
||||
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s", optarg);
|
||||
session_explicit = 1;
|
||||
break;
|
||||
case 'b':
|
||||
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s", optarg);
|
||||
break;
|
||||
case 'k':
|
||||
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", optarg);
|
||||
key_from_cli = 1;
|
||||
break;
|
||||
case 'm':
|
||||
snprintf(cfg.model, sizeof(cfg.model), "%s", optarg);
|
||||
break;
|
||||
case 'H':
|
||||
cfg.headless = 1;
|
||||
break;
|
||||
case 'p':
|
||||
prompt = optarg;
|
||||
break;
|
||||
case 'd':
|
||||
do_demo = 1;
|
||||
break;
|
||||
case 'a':
|
||||
force_ask_key = 1;
|
||||
break;
|
||||
case 'h':
|
||||
usage(argv[0]);
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
default:
|
||||
usage(argv[0]);
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* trim trailing slash on api_base */
|
||||
size_t alen = strlen(cfg.api_base);
|
||||
while (alen > 0 && cfg.api_base[alen - 1] == '/')
|
||||
cfg.api_base[--alen] = 0;
|
||||
|
||||
/* Headless defaults to auto-approve shell/write (no interactive confirm). */
|
||||
if (cfg.headless)
|
||||
cfg.auto_approve = 1;
|
||||
|
||||
/* Default session under <workspace>/.agent/sessions/ (OpenCode-like). */
|
||||
if (!session_explicit) {
|
||||
char id[128];
|
||||
if (cst_agent_store_resolve_default(cfg.workspace_root, cfg.session_path,
|
||||
sizeof(cfg.session_path), id,
|
||||
sizeof(id)) != 0) {
|
||||
fprintf(stderr, "failed to init .agent/sessions under %s\n",
|
||||
cfg.workspace_root);
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "session %s\n", id);
|
||||
}
|
||||
|
||||
if (do_demo) {
|
||||
int rc = demo_ctl_impl(cfg.workspace_root);
|
||||
curl_global_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (force_ask_key || !key_from_cli) {
|
||||
if (force_ask_key) {
|
||||
if (cst_creds_prompt_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
|
||||
fprintf(stderr, "API key prompt cancelled\n");
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
if (cst_creds_save_api_key(cfg.api_key) != 0)
|
||||
fprintf(stderr, "warning: failed to save credentials\n");
|
||||
else
|
||||
fprintf(stderr, "saved API key to ~/.config/cst-agent/credentials\n");
|
||||
/* --ask-key alone: save and exit unless running a turn/TUI */
|
||||
if (!cfg.headless && !prompt) {
|
||||
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
} else if (!cfg.api_key[0]) {
|
||||
if (cst_creds_ensure_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
|
||||
fprintf(stderr, "no API key (set CST_API_KEY or use --ask-key)\n");
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else if (key_from_cli) {
|
||||
/* persist CLI-provided key so next launch can reuse */
|
||||
if (cst_creds_save_api_key(cfg.api_key) != 0)
|
||||
fprintf(stderr, "warning: failed to save credentials\n");
|
||||
}
|
||||
|
||||
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
|
||||
|
||||
cst_agent_t *agent = cst_agent_create(&cfg);
|
||||
if (!agent) {
|
||||
fprintf(stderr, "failed to create agent (session path?)\n");
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
if (cfg.headless) {
|
||||
if (!prompt) {
|
||||
fprintf(stderr, "--headless requires --prompt\n");
|
||||
rc = 1;
|
||||
} else {
|
||||
char err[512];
|
||||
if (cst_agent_run_turn(agent, prompt, err, sizeof(err)) != 0) {
|
||||
fprintf(stderr, "turn failed: %s\n", err);
|
||||
rc = 1;
|
||||
} else {
|
||||
cst_session_t *s = cst_agent_session(agent);
|
||||
for (int i = 0; i < cst_session_count(s); i++) {
|
||||
const cst_message_t *m = cst_session_get(s, i);
|
||||
printf("[%d role=%d] %s\n", i, (int)m->role,
|
||||
m->content ? m->content : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rc = cst_tui_run(agent);
|
||||
}
|
||||
|
||||
cst_agent_destroy(agent);
|
||||
curl_global_cleanup();
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user