#include "cst_agent.h" #include "cst_ext.h" #include "cst_models.h" #include "cst_parliament_cli.h" #include "cst_tools.h" #include "cst_version.h" #include "cst_webui.h" #include "os_compat.h" #include #include #include #include #include #include #ifdef _WIN32 #include #include #else #include #endif static void usage(const char *argv0) { fprintf(stderr, "Usage: %s [options] (works in the current directory)\n" " -P, --provider NAME minimax | deepseek | openai | teamorouter\n" " (last used)\n" " -m, --model NAME model (last used for the provider)\n" " -e, --effort LEVEL low | medium | high | max (last used)\n" " -M, --mode NAME coding (default, code framed) | general\n" " (documents, data, research; last used)\n" " -w, --workspace DIR project directory (default: current)\n" " -c, --continue reopen the last session (default)\n" " -N, --new start a new session\n" " -R, --resume ID open a session by id or unique prefix\n" " -L, --list-sessions list sessions and exit\n" " --migrate-store move sessions written by an older version\n" " into one directory per session, and exit\n" " --list-models [P] what is known about each provider's models\n" " (built-in, /models.json, GET /models)\n" " -A, --approval MODE ask | auto (default: edits run, shell in\n" " the sandbox or by safe rules) | yolo\n" " -y, --yolo same as --approval yolo (this run only)\n" " -C, --confirm LIST shell,write,net (default) | none\n" " -r, --protocol P native-eos (default) | native-inc | ctl |\n" " ctl-heredoc | xml\n" " -T, --tools PROFILE default (all tools) | minimal (paper set)\n" " -n, --max-steps N model calls per turn (30)\n" " --orchestrate delegate edits and shell work to role workers\n" " --no-orchestrate override workspace orchestration setting\n" " --realm-new create Realm file keys (legacy/dev)\n" " --realm-password-init create password-derived Realm (no sk on disk)\n" " --login unlock with password; this device becomes chair\n" " --new-project ID replace all parliament context for a new project\n" " --realm-enroll ID enroll this instance under the Realm\n" " --realm-label S display label for enroll/login\n" " --peer-listen [N] accept one Realm peer handshake (port N)\n" " --peer-dial H:N dial host:port and complete handshake\n" " --discover-wait MS advertise and list same-Realm peers\n" " -H, --headless -p TEXT run one prompt without the TUI\n" " --web serve the UI over HTTP on 127.0.0.1:8787\n" " --web-port N port (default 8787, 0 = pick a free one)\n" " --web-host ADDR bind address (default 127.0.0.1; a\n" " non-loopback address requires a token)\n" " --web-token T require ?token=T / Authorization: Bearer T\n" " --web-dev DIR serve the UI from DIR (development)\n" " --web-open open the browser at startup\n" " -a, --ask-key enter and save the provider's API key\n" " -k, --api-key KEY use KEY for this run only (not saved)\n" " -b, --api-base URL OpenAI-compatible base URL (…/v1)\n" " -s, --session FILE explicit session file\n" " -d, --demo-ctl local CTL parser demo (no API)\n" " -V, --version print the version and exit\n" "\n" "API keys: the TUI asks for a missing key and saves it per provider\n" " in the config dir (credentials, mode 0600); /key changes it.\n" " Fallbacks: (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n" "Settings: provider, models, effort and mode are remembered in\n" " settings.json next to the credentials.\n" "Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n" " CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n" " CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n" " CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n" " CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n" " CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n" "TUI: type /help inside for commands and keys\n", argv0); } static int parse_confirm(const char *s, cst_agent_config_t *cfg) { cfg->confirm_shell = cfg->confirm_write = cfg->confirm_net = 0; if (!s || strcmp(s, "none") == 0 || !s[0]) return 0; char buf[128]; snprintf(buf, sizeof(buf), "%s", s); for (char *tok = strtok(buf, ", "); tok; tok = strtok(NULL, ", ")) { if (strcmp(tok, "shell") == 0) cfg->confirm_shell = 1; else if (strcmp(tok, "write") == 0) cfg->confirm_write = 1; else if (strcmp(tok, "net") == 0) cfg->confirm_net = 1; else if (strcmp(tok, "all") == 0) cfg->confirm_shell = cfg->confirm_write = cfg->confirm_net = 1; else return -1; } return 0; } static void list_one_model(const char *provider, const char *id, const char *where) { cst_model_info_t mi; cst_model_info(provider, id, &mi); char tag[48], price[128] = ""; cst_model_info_label(&mi, tag, sizeof(tag)); if (mi.has_price) snprintf(price, sizeof(price), " %s%g/%g/%g per 1M (miss/hit/out)", mi.price.currency, mi.price.miss, mi.price.hit, mi.price.out); printf(" %-40s %-18s %-8s%s [%s]\n", id, tag, where, price, mi.source); } /* --list-models [provider]: the merged registry, to check models.json. */ static int list_models(const char *only) { char user[1100] = ""; cst_models_user_path(user, sizeof(user)); FILE *uf = user[0] ? fopen(user, "rb") : NULL; printf("user file: %s%s\n", user[0] ? user : "(no config dir)", uf ? "" : " (none)"); if (uf) fclose(uf); if (cst_models_load_error()[0]) printf("warning: %s\n", cst_models_load_error()); int shown = 0; static cst_model_meta_t cached[1024]; for (int i = 0; i < cst_provider_count(); i++) { const cst_provider_t *p = cst_provider_at(i); if (only && only[0] && strcmp(p->name, only) != 0) continue; shown++; printf("\n%s %s (%s; key %s)\n", p->name, p->api_base, cst_provider_source(p), p->key_env[0] ? p->key_env : "-"); char listed[16][128]; int nl = cst_provider_models(p, listed, 16); for (int k = 0; k < nl; k++) list_one_model(p->name, listed[k], strcmp(listed[k], p->model) == 0 ? "default" : "listed"); long age = 0; int nc = cst_models_cache_load(p->name, cached, 1024, &age); if (nc > 0) { printf(" -- %d from GET /models, fetched %ldh ago%s\n", nc, age / 3600, age >= CST_MODELS_CACHE_TTL ? " (stale)" : ""); for (int k = 0; k < nc; k++) list_one_model(p->name, cached[k].id, "api"); } else if (!nl) { printf(" (no models listed; the first one GET /models returns is " "used)\n"); } } if (only && only[0] && !shown) { fprintf(stderr, "unknown provider: %s\n", only); return 1; } return 0; } static int list_sessions(const char *ws) { static char ids[200][128]; int n = cst_agent_store_list(ws, ids, 200); char cur[128] = ""; cst_agent_store_get_current(ws, cur, sizeof(cur)); if (n == 0) { printf("no sessions under %s/.agent/sessions\n", ws); return 0; } for (int i = 0; i < n; i++) { cst_session_info_t in; if (cst_agent_store_info(ws, ids[i], &in) != 0) continue; char when[32] = "?"; time_t t = (time_t)in.mtime; struct tm tm; os_localtime(&t, &tm); strftime(when, sizeof(when), "%Y-%m-%d %H:%M", &tm); printf("%c %-24s %s %4d msgs %s\n", strcmp(cur, ids[i]) == 0 ? '*' : ' ', in.id, when, in.messages, in.title[0] ? in.title : in.first_user); } return 0; } /* Move a store written by an older version into one directory per session * (--migrate-store). Idempotent: what is already a directory is left alone. */ static int migrate_store(const char *ws) { int moved = 0, failed = 0; char err[256] = ""; if (cst_agent_store_migrate(ws, &moved, &failed, err, sizeof(err)) != 0) { fprintf(stderr, "cannot migrate %s/.agent/sessions: %s\n", ws, err[0] ? err : "unknown error"); return 1; } printf("migrated %d session(s) into %s/.agent/sessions//\n", moved, ws); if (failed) printf("%d session(s) could not be moved; their files are untouched\n", failed); if (moved == 0) printf("nothing to move (already one directory per session)\n"); return failed ? 1 : 0; } /* Exact id or unique prefix → id. */ static int resolve_session_id(const char *ws, const char *want, char *out, size_t out_sz) { static char ids[500][128]; int n = cst_agent_store_list(ws, ids, 500), hit = -1; for (int i = 0; i < n; i++) { if (strcmp(ids[i], want) == 0) { hit = i; break; } if (strncmp(ids[i], want, strlen(want)) == 0) { if (hit >= 0) return -2; hit = i; } } if (hit < 0) return -1; snprintf(out, out_sz, "%s", ids[hit]); return 0; } 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; } /* 0..65535, digits only: atoi turned "70000" into port 4464, "abc" into 0. */ static int parse_port(const char *s, int *out) { char *end = NULL; long v = strtol(s, &end, 10); if (!s[0] || *end || v < 0 || v > 65535) return -1; *out = (int)v; return 0; } int main(int argc, char **argv) { if (os_console_init() != 0) fprintf(stderr, "warning: the process code page is not UTF-8 (Windows 10 " "1903 or later is required); non-ASCII text may break\n"); os_set_prog_path(argv[0]); os_temp_dir_export(); 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 */ cfg.api_base[0] = 0; cfg.api_key[0] = 0; cfg.model[0] = 0; cfg.protocol = CST_PROTO_NATIVE_EOS; const char *env_proto = getenv("CST_PROTOCOL"); if (env_proto && env_proto[0] && cst_protocol_parse(env_proto, &cfg.protocol) != 0) { fprintf(stderr, "unknown CST_PROTOCOL=%s\n", env_proto); return 1; } cfg.max_steps = CST_DEFAULT_MAX_STEPS; const char *env_steps = getenv("CST_MAX_STEPS"); if (env_steps && atoi(env_steps) > 0) cfg.max_steps = atoi(env_steps); cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP; const char *env_cap = getenv("CST_TOOL_OUT_CAP"); if (env_cap && atol(env_cap) > 0) cfg.tool_out_cap = (size_t)atol(env_cap); const char *provider_name = getenv("CST_PROVIDER"); cfg.headless = 0; cfg.confirm_shell = 1; cfg.confirm_write = 1; cfg.confirm_net = 1; cfg.auto_approve = 0; cfg.tool_profile = CST_TOOLS_DEFAULT; const char *env_tools = getenv("CST_TOOLS"); if (env_tools && env_tools[0] && cst_tool_profile_parse(env_tools, &cfg.tool_profile) != 0) { fprintf(stderr, "unknown CST_TOOLS=%s (default | minimal)\n", env_tools); return 1; } if (getenv("CST_AUTO_APPROVE")) cfg.auto_approve = 1; const char *env_confirm = getenv("CST_CONFIRM"); if (env_confirm && parse_confirm(env_confirm, &cfg) != 0) { fprintf(stderr, "bad CST_CONFIRM=%s (shell,write,net | none)\n", env_confirm); return 1; } int approval = -1; const char *env_appr = getenv("CST_APPROVAL"); if (env_appr && env_appr[0] && cst_approval_parse(env_appr, &approval) != 0) { fprintf(stderr, "bad CST_APPROVAL=%s (ask | auto | yolo)\n", env_appr); return 1; } if (cfg.auto_approve) approval = CST_APPROVAL_YOLO; int do_demo = 0; int force_ask_key = 0; int session_explicit = 0; const char *prompt = NULL; int new_session = 0, do_list = 0, do_migrate = 0; const char *resume_id = NULL; int do_realm_new = 0, do_realm_enroll = 0, do_peer_listen = 0, do_peer_dial = 0, do_discover = 0, do_realm_pw_init = 0, do_login = 0, do_new_project = 0; const char *realm_enroll_id = NULL, *realm_label = NULL; const char *peer_dial_spec = NULL, *parliament_instance = NULL; const char *new_project_id = NULL; int peer_listen_port = 0, discover_wait_ms = 3000, discover_peer_port = 9100; /* WebUI (--web): a second front end on the same engine. */ int web_mode = 0; cst_webui_opts_t web; memset(&web, 0, sizeof(web)); web.port = CST_WEBUI_DEFAULT_PORT; /* 0 would mean "pick a free one" */ const char *env = getenv("CST_WEB_PORT"); if (env && env[0] && parse_port(env, &web.port) != 0) { fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env); return 1; } web.host = getenv("CST_WEB_HOST"); web.token = getenv("CST_WEB_TOKEN"); web.root = getenv("CST_WEB_ROOT"); 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'}, {"provider", required_argument, 0, 'P'}, {"protocol", required_argument, 0, 'r'}, {"max-steps", required_argument, 0, 'n'}, {"tools", required_argument, 0, 'T'}, {"headless", no_argument, 0, 'H'}, {"prompt", required_argument, 0, 'p'}, {"demo-ctl", no_argument, 0, 'd'}, {"ask-key", no_argument, 0, 'a'}, {"new", no_argument, 0, 'N'}, {"continue", no_argument, 0, 'c'}, {"resume", required_argument, 0, 'R'}, {"list-sessions", no_argument, 0, 'L'}, {"migrate-store", no_argument, 0, 0x107}, {"yolo", no_argument, 0, 'y'}, {"approval", required_argument, 0, 'A'}, {"confirm", required_argument, 0, 'C'}, {"effort", required_argument, 0, 'e'}, {"mode", required_argument, 0, 'M'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'V'}, {"list-models", optional_argument, 0, 0x100}, {"web", no_argument, 0, 0x101}, {"web-port", required_argument, 0, 0x102}, {"web-host", required_argument, 0, 0x103}, {"web-token", required_argument, 0, 0x104}, {"web-dev", required_argument, 0, 0x105}, {"web-open", no_argument, 0, 0x106}, {"orchestrate", no_argument, 0, 0x108}, {"no-orchestrate", no_argument, 0, 0x109}, {"realm-new", no_argument, 0, 0x110}, {"realm-enroll", required_argument, 0, 0x111}, {"realm-label", required_argument, 0, 0x112}, {"peer-listen", optional_argument, 0, 0x113}, {"peer-dial", required_argument, 0, 0x114}, {"discover-wait", required_argument, 0, 0x115}, {"instance", required_argument, 0, 0x116}, {"peer-port", required_argument, 0, 0x117}, {"realm-password-init", no_argument, 0, 0x118}, {"login", no_argument, 0, 0x119}, {"new-project", required_argument, 0, 0x11a}, {0, 0, 0, 0}}; int c; while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:", opts, NULL)) != -1) { switch (c) { case 0x108: cfg.orchestrate_override = 1; break; case 0x109: cfg.orchestrate_override = -1; break; case 0x110: do_realm_new = 1; break; case 0x111: do_realm_enroll = 1; realm_enroll_id = optarg; break; case 0x112: realm_label = optarg; break; case 0x113: do_peer_listen = 1; peer_listen_port = optarg && optarg[0] ? atoi(optarg) : 0; break; case 0x114: do_peer_dial = 1; peer_dial_spec = optarg; break; case 0x115: do_discover = 1; discover_wait_ms = atoi(optarg); if (discover_wait_ms <= 0) discover_wait_ms = 3000; break; case 0x116: parliament_instance = optarg; break; case 0x117: discover_peer_port = atoi(optarg); if (discover_peer_port <= 0) discover_peer_port = 9100; break; case 0x118: do_realm_pw_init = 1; break; case 0x119: do_login = 1; break; case 0x11a: do_new_project = 1; new_project_id = optarg; break; 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); break; case 'm': snprintf(cfg.model, sizeof(cfg.model), "%s", optarg); break; case 'P': provider_name = optarg; break; case 'r': if (cst_protocol_parse(optarg, &cfg.protocol) != 0) { fprintf(stderr, "unknown protocol: %s\n", optarg); curl_global_cleanup(); return 1; } break; case 'n': cfg.max_steps = atoi(optarg); if (cfg.max_steps <= 0) cfg.max_steps = CST_DEFAULT_MAX_STEPS; break; case 'T': if (cst_tool_profile_parse(optarg, &cfg.tool_profile) != 0) { fprintf(stderr, "unknown tool profile: %s (default | minimal)\n", optarg); curl_global_cleanup(); return 1; } 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 'N': new_session = 1; break; case 'c': new_session = 0; resume_id = NULL; break; case 'R': resume_id = optarg; break; case 'L': do_list = 1; break; case 'y': approval = CST_APPROVAL_YOLO; break; case 'A': if (cst_approval_parse(optarg, &approval) != 0) { fprintf(stderr, "bad --approval %s (ask | auto | yolo)\n", optarg); curl_global_cleanup(); return 1; } break; case 'C': if (parse_confirm(optarg, &cfg) != 0) { fprintf(stderr, "bad --confirm %s (shell,write,net | none)\n", optarg); curl_global_cleanup(); return 1; } break; case 'e': if (!cst_effort_valid(optarg)) { fprintf(stderr, "bad --effort %s (low | medium | high | max)\n", optarg); curl_global_cleanup(); return 1; } snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg); break; case 'M': if (!cst_mode_valid(optarg)) { fprintf(stderr, "bad --mode %s (coding | general)\n", optarg); curl_global_cleanup(); return 1; } snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg); break; case 'h': usage(argv[0]); curl_global_cleanup(); return 0; case 0x100: { const char *only = optarg; if (!only && optind < argc && argv[optind][0] != '-') only = argv[optind++]; int rc = list_models(only); curl_global_cleanup(); return rc; } case 0x101: web_mode = 1; break; case 0x102: if (parse_port(optarg, &web.port) != 0) { fprintf(stderr, "--web-port %s is not a port (0-65535)\n", optarg); curl_global_cleanup(); return 1; } web_mode = 1; break; case 0x103: web.host = optarg; web_mode = 1; break; case 0x104: web.token = optarg; web_mode = 1; break; case 0x105: web.root = optarg; web_mode = 1; break; case 0x106: web.open_browser = 1; web_mode = 1; break; case 0x107: do_migrate = 1; break; case 'V': printf("cst-agent %s\n", CST_VERSION); curl_global_cleanup(); return 0; default: usage(argv[0]); curl_global_cleanup(); return 1; } } if (do_migrate) { int rc = migrate_store(cfg.workspace_root); curl_global_cleanup(); return rc; } if (do_realm_new) { int rc = cst_parliament_cli_realm_new(); curl_global_cleanup(); return rc; } if (do_realm_pw_init) { const char *pw = getenv("CST_REALM_PASSWORD"); int rc = cst_parliament_cli_realm_password_init(pw); curl_global_cleanup(); return rc; } if (do_login) { const char *pw = getenv("CST_REALM_PASSWORD"); const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID"); if (!iid || !iid[0]) iid = "chair-local"; int rc = cst_parliament_cli_login(pw, iid, realm_label); curl_global_cleanup(); return rc; } if (do_new_project) { int rc = cst_parliament_cli_new_project(new_project_id); curl_global_cleanup(); return rc; } if (do_realm_enroll) { int rc = cst_parliament_cli_realm_enroll(realm_enroll_id, realm_label); curl_global_cleanup(); return rc; } if (do_peer_listen) { const char *iid = parliament_instance ? parliament_instance : realm_enroll_id; if (!iid) iid = getenv("CST_INSTANCE_ID"); int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid); curl_global_cleanup(); return rc; } if (do_peer_dial) { const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID"); char host[256]; int port = 0; host[0] = 0; if (peer_dial_spec) { const char *colon = strrchr(peer_dial_spec, ':'); if (colon && colon > peer_dial_spec) { size_t hl = (size_t)(colon - peer_dial_spec); if (hl >= sizeof(host)) hl = sizeof(host) - 1; memcpy(host, peer_dial_spec, hl); host[hl] = 0; port = atoi(colon + 1); } } int rc = cst_parliament_cli_peer_dial(host, port, iid); curl_global_cleanup(); return rc; } if (do_discover) { const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID"); int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms); curl_global_cleanup(); return rc; } if (do_list) { int rc = list_sessions(cfg.workspace_root); curl_global_cleanup(); return rc; } /* Explicit option > environment > last used (settings.json) > preset. */ cst_settings_t saved; cst_settings_load(&saved); if ((!provider_name || !provider_name[0]) && cst_provider_find(saved.provider)) provider_name = saved.provider; const cst_provider_t *prov = cst_provider_default(); if (provider_name && provider_name[0]) { prov = cst_provider_find(provider_name); if (!prov) { fprintf(stderr, "unknown provider: %s\n", provider_name); curl_global_cleanup(); return 1; } } snprintf(cfg.provider, sizeof(cfg.provider), "%s", prov->name); if (!cfg.effort[0]) { const char *e = getenv("CST_EFFORT"); if (!cst_effort_valid(e)) e = saved.effort; snprintf(cfg.effort, sizeof(cfg.effort), "%s", cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT); } if (!cfg.mode[0]) { const char *m = getenv("CST_MODE"); if (!cst_mode_valid(m)) m = saved.mode; snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m)); } if (!cfg.api_base[0]) { 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"); snprintf(cfg.api_base, sizeof(cfg.api_base), "%s", env_base && env_base[0] ? env_base : prov->api_base); } if (!cfg.model[0]) { const char *env_model = getenv("CST_MODEL"); if (!env_model || !env_model[0]) env_model = getenv("OPENAI_MODEL"); if (!env_model || !env_model[0]) env_model = cst_settings_model(&saved, prov->name); snprintf(cfg.model, sizeof(cfg.model), "%s", env_model && env_model[0] ? env_model : prov->model); } /* --api-key > saved key > provider env > CST_API_KEY; the TUI asks for * a missing key itself. */ if (!cfg.api_key[0] && !force_ask_key && cst_creds_resolve(prov->name, cfg.api_key, sizeof(cfg.api_key), NULL) != 0) { const char *k = getenv("CST_API_KEY"); if (k && k[0]) snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", k); } /* 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; /* Option > environment > last picked (settings.json) > auto. */ if (approval < 0 && cst_approval_parse(saved.approval, &approval) != 0) approval = CST_DEFAULT_APPROVAL; cfg.approval = approval; cfg.auto_approve = approval == CST_APPROVAL_YOLO; /* Headless defaults to auto-approve shell/write (no interactive confirm). */ if (cfg.headless) cfg.auto_approve = 1; /* Default session under /.agent/sessions/ (OpenCode-like). */ if (!session_explicit && (new_session || resume_id)) { char id[128]; int rc = 0; if (new_session) { rc = cst_agent_store_ensure(cfg.workspace_root) != 0 || cst_agent_store_new_id(id, sizeof(id)) != 0 ? -1 : 0; } else { rc = resolve_session_id(cfg.workspace_root, resume_id, id, sizeof(id)); if (rc != 0) fprintf(stderr, "%s session: %s (see --list-sessions)\n", rc == -2 ? "ambiguous" : "no such", resume_id); } if (rc != 0 || cst_agent_store_path_for_id(cfg.workspace_root, id, cfg.session_path, sizeof(cfg.session_path)) != 0) { curl_global_cleanup(); return 1; } cst_agent_store_set_current(cfg.workspace_root, id); fprintf(stderr, "session %s%s\n", id, new_session ? " (new)" : ""); } else 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; } int need_console_key = force_ask_key || (!cfg.api_key[0] && cfg.headless && isatty(0)); if (need_console_key) { if (cst_creds_prompt_console(cfg.provider, cfg.api_key, sizeof(cfg.api_key)) != 0) { fprintf(stderr, "no API key entered\n"); curl_global_cleanup(); return 1; } char dir[1024] = "the config directory"; os_config_dir(dir, sizeof(dir)); if (cst_creds_save(cfg.provider, cfg.api_key) != 0) fprintf(stderr, "warning: failed to save credentials\n"); else fprintf(stderr, "saved the %s API key to %s/credentials\n", cfg.provider, dir); /* --ask-key alone: save and exit */ if (force_ask_key && !cfg.headless && !prompt) { curl_global_cleanup(); return 0; } } if (!cfg.api_key[0] && (cfg.headless || web_mode)) { fprintf(stderr, "no API key for %s: start cst-agent once to enter it, or run " "cst-agent --ask-key --provider %s\n", cfg.provider, cfg.provider); curl_global_cleanup(); return 1; } /* Router presets have no fixed model: take the first one the API lists * (the TUI does the same after its /models probe). */ if (!cfg.model[0] && (cfg.headless || web_mode)) { static cst_model_meta_t list[512]; char err[256] = ""; long age = 0; int n = cst_models_cache_load(cfg.provider, list, 1, &age); if (n <= 0 || age >= CST_MODELS_CACHE_TTL) { int r = cst_llm_list_models_meta(cfg.api_base, cfg.api_key, list, 512, err, sizeof(err)); if (r >= 0) cst_models_cache_save(cfg.provider, cfg.api_base, list, r); if (r >= 0 || n <= 0) n = r; } if (n > 0) { snprintf(cfg.model, sizeof(cfg.model), "%s", list[0].id); } else { fprintf(stderr, "no model for %s: pass --model (%s)\n", cfg.provider, err[0] ? err : "the API listed none"); curl_global_cleanup(); return 1; } } fprintf(stderr, "using %s model=%s effort=%s protocol=%s tools=%s max_steps=%d\n", cfg.api_base, cfg.model, cfg.effort, cst_protocol_name(cfg.protocol), cst_tool_profile_name(cfg.tool_profile), cfg.max_steps); { char ext[256]; cst_extensions_load(&cfg, ext, sizeof(ext)); if (ext[0] && cfg.headless) fprintf(stderr, "extensions: %s\n", ext); if (cfg.headless) cst_extensions_wait(stderr); } 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 (web_mode) { web.prompt = prompt; /* --web -p "...": run one turn at startup */ rc = cst_webui_run(agent, &web); } else 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); cst_extensions_unload(); curl_global_cleanup(); return rc; }