// SPDX-License-Identifier: ISC #include #include #include #include #include #include #include #include #include #include #include "clm/clm.h" #include "clm/host_uv.h" #include "clm/provider.h" #include "frontend.h" #include "version.h" #include "clm/lua_plugin.h" #include "seed_plugins.h" #include "clm/cleanup.h" #include "clm/session.h" #include "clm/peer.h" #include "mcp_setup.h" #include "sysinfo.h" /* How long a session log outlives its last write, unless config says * otherwise (session_keep_days = 0 keeps everything). */ #define CLM_SESSION_KEEP_DAYS 90 #include "cfg_tuning.h" #include "loop_drain.h" #include "model_spec.h" #include "templates.h" #include "xdg.h" static void usage(const char *prog) { fprintf(stderr, "usage: %s setup\n" " %s [-o|--oneshot PROMPT] [-f|--forever PROMPT] " "[-H|--headless] [-u|--url BASE] " "[-m|--model PROVIDER/MODEL-ID] [--provider NAME] " "[-p|--plugins DIR] [-S|--no-stream] [--allow-all-tools] " "[-r|--resume [ID]] " "[-V|--version] [-h|--help]\n", prog, prog); fprintf(stderr, " setup write a starter config.lua and seed " "builtin plugins\n" " -o, --oneshot PROMPT run one prompt headless and exit\n" " -f, --forever PROMPT TUI mode: submit PROMPT, then " "auto-resubmit it\n" " every time a turn completes with nothing " "queued,\n" " so the agent keeps going without a human " "re-prompting it\n" " -H, --headless force the plain stdio REPL\n" " -u, --url BASE base API endpoint " "(default http://127.0.0.1:8081/v1);\n" " \"/messages\" is appended for an " "anthropic " "provider,\n" " \"/chat/completions\" otherwise\n" " -m, --model PROVIDER/MODEL-ID\n" " a config.lua providers[PROVIDER].models" "[MODEL-ID] entry,\n" " or a literal model id (no \"/\") to " "request " "on whatever\n" " connection is otherwise active\n" " --provider NAME name of an entry in config.lua's " "`providers` table,\n" " overriding the connection --model's " "provider half names\n" " -p, --plugins DIR plugin directory " "(default $XDG_CONFIG_HOME/clm/plugins)\n" " -S, --no-stream disable streamed (SSE) responses\n" " --allow-all-tools run every tool call without asking; for\n" " an agent nobody is watching, in a\n" " directory you can afford to lose\n" " -r, --resume [ID] resume a saved session in the TUI; with " "no ID,\n" " pick from a list of saved sessions\n" " -V, --version print version and exit\n" " -h, --help show this help\n" "\n" " With no options it runs the interactive ncurses UI on a " "terminal.\n" " Set CLM_API_KEY in the environment to send a bearer token.\n"); } /* * Escapes for the non-TUI output paths, resolved once in main(): colour, or * attributes only under NO_COLOR, or nothing when stdout is not a terminal. */ static const char *esc_warn = "\033[33m"; static const char *esc_err = "\033[31m"; static const char *esc_dim = "\033[2m"; static const char *esc_reset = "\033[0m"; static void init_escapes(void) { const char *term = getenv("TERM"); if (!isatty(STDOUT_FILENO) || term == NULL || strcmp(term, "dumb") == 0) { esc_warn = esc_err = esc_dim = esc_reset = ""; return; } if (getenv("NO_COLOR") != NULL) { esc_warn = "\033[1m"; esc_err = "\033[1;7m"; } } struct cli_state { uv_loop_t *loop; struct clm_host *host; struct clm_agent *agent; struct clm_lua_env *lua_env; struct clm_lua_cfg *lua_cfg; struct clm_peer *peer; struct clm_mcp_client **mcp_clients; size_t mcp_client_count; uv_pipe_t stdin_pipe; char prompt_line[1024]; size_t prompt_len; int oneshot; int batch; int turn_done; int turn_status; }; static void cb_mcp_status(const char *msg, void *user) { (void)user; fprintf(stderr, "%s\n", msg); } static void cb_assistant_text(const char *text, void *user) { struct cli_state *state = (struct cli_state *)user; if (state->oneshot) printf("%s", text); else printf("assistant> %s", text); fflush(stdout); } static void cb_reasoning(const char *text, void *user) { (void)user; printf("%s%s%s", esc_dim, text, esc_reset); fflush(stdout); } static void cb_finish_reason(enum clm_finish_reason reason, void *user) { (void)user; if (reason == CLM_FINISH_LENGTH) printf("\n%s[truncated: hit token limit]%s\n", esc_warn, esc_reset); else if (reason == CLM_FINISH_CONTENT_FILTER) printf("\n%s[stopped: content filter]%s\n", esc_err, esc_reset); fflush(stdout); } static void cb_usage(const struct clm_usage *usage, void *user) { (void)user; printf("%s[%d+%d tok", esc_dim, usage->prompt_tokens, usage->completion_tokens); if (usage->cache_read_tokens > 0) printf(", %d cached", usage->cache_read_tokens); if (usage->tokens_per_sec > 0) printf(", %.1f tok/s", usage->tokens_per_sec); printf("]%s\n", esc_reset); fflush(stdout); } static void cb_tool_begin(const char *name, const char *args, void *user) { (void)user; printf("[tool: %s %s]\n", name, args ? args : ""); fflush(stdout); } static void cb_tool_result(const char *name, const char *content, enum clm_tool_outcome outcome, void *user) { (void)user; switch (outcome) { case CLM_TOOL_OK: printf("[result: %s]\n%s\n", name, content ? content : ""); break; case CLM_TOOL_FAILED: printf("%s[x %s]%s %s\n", esc_err, name, esc_reset, content ? content : ""); break; case CLM_TOOL_TIMEDOUT: printf("%s[timeout %s]%s %s\n", esc_warn, name, esc_reset, content ? content : ""); break; } fflush(stdout); } static void cb_tool_batch(size_t completed, size_t total, void *user) { (void)user; printf("[tools %zu/%zu]\n", completed, total); fflush(stdout); } static void cb_state(enum clm_agent_state state, void *user) { } static void cb_notice(const char *text, void *user) { (void)user; fprintf(stderr, "notice: %s\n", text ? text : ""); } static void cb_turn_done(int status, void *user) { struct cli_state *state = (struct cli_state *)user; if (status != 0) { fprintf(stderr, "error: %s\n", clm_agent_get_last_error(state->agent)); } if (state->oneshot || state->batch) { state->turn_done = 1; state->turn_status = status; } if (state->oneshot) return; printf("\nuser> "); fflush(stdout); } /* * Auto-allow permission handler: preserves the CLI's pre-gate behaviour under * the library's default-deny. A future refinement could gate this behind an * explicit --yes/--allow flag for unattended runs. */ static void cb_permission(const struct clm_permission_req *req, void *user) { struct cli_state *state = (struct cli_state *)user; clm_tool_permission_respond(state->agent, req, CLM_PERM_ALLOW_ONCE); } static const struct clm_callbacks cli_callbacks = { .on_assistant_text = cb_assistant_text, .on_reasoning = cb_reasoning, .on_tool_begin = cb_tool_begin, .on_permission = cb_permission, .on_tool_result = cb_tool_result, .on_tool_batch = cb_tool_batch, .on_finish_reason = cb_finish_reason, .on_usage = cb_usage, .on_state = cb_state, .on_turn_done = cb_turn_done, .on_notice = cb_notice, }; static void on_alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) { buf->base = malloc(suggested_size); if (buf->base == NULL) { buf->len = 0; return; } buf->len = suggested_size; } /* * Line-at-a-time REPL over a stdin that epoll cannot watch: a regular file, * /dev/null, anything uv_guess_handle() reports as UV_FILE. uv_pipe_open() * takes such a descriptor, then libuv aborts the process when the kernel * refuses to poll it. Read with stdio, run the loop only during a turn. */ static void run_stdin_batch(struct cli_state *state, uv_loop_t *loop) { state->batch = 1; while (fgets(state->prompt_line, sizeof(state->prompt_line), stdin) != NULL) { size_t n = strcspn(state->prompt_line, "\r\n"); /* Longer than the buffer: submit what fits and drop the * rest of the line, as the streaming path does. */ if (state->prompt_line[n] == '\0') { int c; while ((c = fgetc(stdin)) != EOF && c != '\n') ; } state->prompt_line[n] = '\0'; if (state->prompt_line[0] == '\0') continue; if (strcmp(state->prompt_line, "quit") == 0 || strcmp(state->prompt_line, "exit") == 0) break; state->turn_done = 0; if (clm_agent_submit(state->agent, state->prompt_line) < 0) { fprintf(stderr, "error: %s\n", clm_agent_get_last_error(state->agent)); continue; } while (!state->turn_done) uv_run(loop, UV_RUN_ONCE); } printf("\n"); fflush(stdout); } static void on_stdin_close(uv_handle_t *handle) { struct cli_state *state = (struct cli_state *)handle->data; uv_stop(state->loop); } static void on_stdin_read(uv_stream_t *stream, ssize_t n_read, const uv_buf_t *buf) { struct cli_state *state = (struct cli_state *)stream->data; if (n_read < 0) { if (n_read == UV_ENOBUFS) return; if (n_read == UV_EOF) { free(buf->base); printf("\n"); fflush(stdout); uv_close((uv_handle_t *)stream, on_stdin_close); return; } fprintf(stderr, "read error: %s\n", uv_err_name(n_read)); uv_close((uv_handle_t *)stream, on_stdin_close); return; } if (n_read == 0) { free(buf->base); return; } for (ssize_t i = 0; i < n_read; i++) { if (state->prompt_len < sizeof(state->prompt_line) - 1) { state->prompt_line[state->prompt_len++] = buf->base[i]; } if (buf->base[i] == '\n' || buf->base[i] == '\r') { if (state->prompt_len > 0 && state->prompt_line[state->prompt_len - 1] == '\r') state->prompt_len--; if (state->prompt_len > 0 && state->prompt_line[state->prompt_len - 1] == '\n') state->prompt_len--; state->prompt_line[state->prompt_len] = '\0'; if (state->prompt_len > 0) { if (strcmp(state->prompt_line, "quit") == 0 || strcmp(state->prompt_line, "exit") == 0) { free(buf->base); uv_close((uv_handle_t *)stream, on_stdin_close); return; } int r = clm_agent_submit( state->agent, state->prompt_line); if (r < 0) { fprintf(stderr, "error: %s\n", clm_agent_get_last_error( state->agent)); } } state->prompt_len = 0; break; } } free(buf->base); } /* Writes content to path unless it already exists. Returns 0 if written, * 1 if it already existed (left untouched), or -errno on failure. */ static int write_new_file(const char *path, const char *content, mode_t mode) { int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, mode); if (fd < 0) return errno == EEXIST ? 1 : -errno; size_t len = strlen(content); size_t off = 0; while (off < len) { ssize_t n = write(fd, content + off, len - off); if (n < 0) { int err = -errno; close(fd); (void)unlink(path); return err; } off += (size_t)n; } close(fd); return 0; } /* * --resume with no id: print a numbered listing of saved sessions and read * a choice from stdin. Returns a malloc'd session id, or NULL (no sessions, * bad choice, or EOF). */ /* * Expand a fragment of a session id into the full one. Matches any session * whose id contains the fragment, so the 8-hex tail shown in the status bar * is enough. Returns NULL (after reporting) when nothing or more than one * session matches. */ /* Drop scratch directories whose session log no longer exists, and any * left behind past the retention window. */ static void sweep_scratch(int64_t keep_days) { struct clm_session_info *infos = NULL; const char **live = NULL; size_t n = 0, i; if (clm_session_list(NULL, &infos, &n) != 0) return; if (n > 0) { live = calloc(n, sizeof(*live)); if (live == NULL) { clm_session_list_free(infos, n); return; } for (i = 0; i < n; i++) live[i] = infos[i].id; } (void)clm_cli_scratch_gc( live, n, (unsigned)(keep_days > 0 ? keep_days : 0)); free(live); clm_session_list_free(infos, n); } /* * Give the agent a private directory for throwaway files: exported to tools * as CLM_SCRATCH and named in the system prompt, since a model that is not * told where to put scratch files puts them in the user's tree. */ static char * attach_scratch( const char *key, const char *sysinfo, bool is_session, char **dir_out) { char *dir = clm_cli_scratch_dir(key); char *block = NULL; *dir_out = NULL; if (dir == NULL) return NULL; (void)setenv("CLM_SCRATCH", dir, 1); if (asprintf(&block, "%s\n\nscratch directory for this session: %s\n" "Put working files, downloads, and intermediate output there " "rather than in the user's tree or /tmp. It is also " "$CLM_SCRATCH in any command you run.%s%s", sysinfo != NULL ? sysinfo : "", dir, is_session ? "\n\nthis session's id: " : "", is_session ? key : "") < 0) { free(dir); return NULL; } *dir_out = dir; return block; } static char * resolve_session_prefix(const char *frag) { struct clm_session_info *infos = NULL; size_t n = 0, i, hits = 0; char *match = NULL; if (frag == NULL || clm_session_list(NULL, &infos, &n) != 0) return NULL; for (i = 0; i < n; i++) { if (strstr(infos[i].id, frag) == NULL) continue; hits++; if (hits == 1) match = strdup(infos[i].id); } if (hits > 1) { fprintf( stderr, "error: %s matches %zu sessions:\n", frag, hits); for (i = 0; i < n; i++) if (strstr(infos[i].id, frag) != NULL) fprintf(stderr, " %s\n", infos[i].id); free(match); match = NULL; } else if (hits == 0) { fprintf(stderr, "error: no session matching %s\n", frag); } clm_session_list_free(infos, n); return match; } static char * pick_session(void) { struct clm_session_info *infos = NULL; size_t n = 0, i; char line[32], *end, *id = NULL; unsigned long choice; int r; r = clm_session_list(NULL, &infos, &n); if (r < 0) { fprintf(stderr, "error: listing sessions: %s\n", strerror(-r)); return NULL; } if (n == 0) { fprintf(stderr, "no saved sessions\n"); return NULL; } for (i = 0; i < n; i++) { char when[32] = "?"; time_t created = (time_t)infos[i].created; struct tm tm; if (created > 0 && localtime_r(&created, &tm) != NULL) (void)strftime( when, sizeof(when), "%Y-%m-%d %H:%M", &tm); printf("%3zu) %s %s %s\n %s\n", i + 1, infos[i].id, when, infos[i].model != NULL ? infos[i].model : "", infos[i].first_user != NULL ? infos[i].first_user : "(empty)"); } printf("resume which session? [1-%zu] ", n); fflush(stdout); if (fgets(line, sizeof(line), stdin) == NULL) { clm_session_list_free(infos, n); return NULL; } errno = 0; choice = strtoul(line, &end, 10); if (errno != 0 || end == line || choice < 1 || choice > n) { fprintf(stderr, "no such session\n"); clm_session_list_free(infos, n); return NULL; } id = strdup(infos[choice - 1].id); clm_session_list_free(infos, n); return id; } /* `clm setup`: writes a starter config.lua and secrets.lua, and seeds the * builtin plugins, without silently doing any of that on a normal run. * Safe to re-run: never overwrites a config, secrets, or plugin file * that's already there. */ static int run_setup(void) { autofree char *cfg_path = xdg_config_path("clm/config.lua"); autofree char *secrets_path = xdg_config_path("clm/secrets.lua"); autofree char *agents_dir = xdg_config_path("clm/agents"); autofree char *plugins_dir = xdg_config_path("clm/plugins"); if (cfg_path == NULL || secrets_path == NULL || agents_dir == NULL || plugins_dir == NULL) { fprintf(stderr, "setup: could not determine config path " "($XDG_CONFIG_HOME or $HOME not set)\n"); return 1; } /* Also creates the parent clm/ dir. */ int sr = clm_seed_default_plugins(plugins_dir); if (sr < 0) { fprintf(stderr, "setup: seeding %s: %s\n", plugins_dir, strerror(-sr)); return 1; } printf("plugins in %s\n", plugins_dir); if (mkdir(agents_dir, 0755) != 0 && errno != EEXIST) { fprintf(stderr, "setup: mkdir %s: %s\n", agents_dir, strerror(errno)); return 1; } printf("agent profiles in %s\n", agents_dir); /* 0600: secrets.lua holds API keys, unlike config.lua. */ int xr = write_new_file( secrets_path, (const char *)secrets_lua_tpl_data, 0600); if (xr < 0) { fprintf(stderr, "setup: writing %s: %s\n", secrets_path, strerror(-xr)); return 1; } printf("%s %s\n", xr == 1 ? "kept existing" : "wrote", secrets_path); int cr = write_new_file(cfg_path, (const char *)config_lua_tpl_data, 0644); if (cr < 0) { fprintf( stderr, "setup: writing %s: %s\n", cfg_path, strerror(-cr)); return 1; } printf("%s %s\n", cr == 1 ? "kept existing" : "wrote", cfg_path); return 0; } int main(int argc, char *argv[]) { const char *api_base = NULL; const char *model_name = NULL; /* -m/--model: a "provider/model-id" spec, or a literal wire id */ const char *effort = NULL; bool allow_all = false; char **volatile_tools = NULL; const char *provider_name = NULL; /* --provider: a config providers[] name override */ /* Owned copy of a "provider/model-id" spec's provider half (see * split_provider_model() in model_spec.h). cfg.provider_name may * alias it and cfg is read as late as tui_run(), so it is released * on the way out rather than at any block scope. */ char *spec_provider = NULL; const char *plugin_dir = NULL; const char *agent_name = NULL; char *oneshot = NULL; char *forever_prompt = NULL; int stream = 1; int headless = 0; struct clm_cfg cfg = {0}; struct cli_state *state; uv_loop_t *loop; char endpoint[256]; int opt, r; struct clm_lua_cfg *lcfg = NULL; autofree char *config_load_err = NULL; init_escapes(); if (argc >= 2 && strcmp(argv[1], "setup") == 0) return run_setup(); int resume = 0; const char *resume_id = NULL; enum { OPT_PROVIDER = 256, OPT_ALLOW_ALL }; const struct option opts[] = { {"oneshot", required_argument, NULL, 'o'}, {"forever", required_argument, NULL, 'f'}, {"url", required_argument, NULL, 'u'}, {"model", required_argument, NULL, 'm'}, {"provider", required_argument, NULL, OPT_PROVIDER}, {"plugins", required_argument, NULL, 'p'}, {"agent", required_argument, NULL, 'a'}, {"resume", optional_argument, NULL, 'r'}, {"headless", no_argument, NULL, 'H'}, {"no-stream", no_argument, NULL, 'S'}, {"allow-all-tools", no_argument, NULL, OPT_ALLOW_ALL}, {"version", no_argument, NULL, 'V'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0}, }; while ((opt = getopt_long( argc, argv, "a:o:f:u:m:p:r::HSVh", opts, NULL)) != -1) { switch (opt) { case OPT_ALLOW_ALL: allow_all = true; break; case 'a': agent_name = optarg; break; case 'r': resume = 1; /* getopt's optional_argument only binds --resume=ID / * -rID; also accept a detached "--resume ID". */ if (optarg != NULL) resume_id = optarg; else if (optind < argc && argv[optind][0] != '-') resume_id = argv[optind++]; break; case 'o': oneshot = optarg; break; case 'f': forever_prompt = optarg; break; case 'u': api_base = optarg; break; case 'm': model_name = optarg; break; case OPT_PROVIDER: provider_name = optarg; break; case 'p': plugin_dir = optarg; break; case 'H': headless = 1; break; case 'V': printf("clm %s\n", CLM_VERSION); return 0; case 'S': stream = 0; break; case 'h': usage(argv[0]); return 0; default: usage(argv[0]); return 1; } } /* Load config early so agent profile + model/provider inform cfg. */ { autofree char *cpath = xdg_config_path("clm/config.lua"); if (cpath != NULL) lcfg = clm_lua_cfg_load(cpath, &config_load_err); } /* A NULL lcfg with a non-NULL config_load_err means config.lua exists * but failed to load (syntax error, runtime error, wrong return * type -- see clm_lua_cfg_load's doc comment) -- as opposed to no * config.lua at all, which is silent and falls back to defaults. * That distinction used to be lost entirely (both looked like * lcfg == NULL to every caller), so a config.lua with e.g. `local = * {...}` as a table key -- a Lua syntax error, "local" being * reserved -- failed to load with zero indication anything was * wrong: no error, no warning, just every setting in it silently * absent. Surfaced here loudly (headless paths only -- the TUI path * shows its own banner once curses is up, see tui_run). */ if (lcfg == NULL && config_load_err != NULL) fprintf(stderr, "%serror: config.lua failed to load: %s%s\n", esc_err, config_load_err, esc_reset); if (lcfg != NULL) { autofree char *adir = xdg_config_path("clm/agents"); clm_lua_cfg_load_agent(lcfg, adir, agent_name); /* Resolve the model (agent profile, or top-level default, * unless -m gave one directly) -- a "provider/model-id" spec, * see src/model_spec.h -- down to a provider connection. * --provider overrides which provider entry supplies the * connection (e.g. failing over to a mirror endpoint) * without changing which wire model id is requested. */ if (model_name == NULL) model_name = clm_lua_cfg_get_str(lcfg, "model"); const char *spec_model = NULL; split_provider_model(model_name, &spec_provider, &spec_model); const char *prov_name = provider_name != NULL ? provider_name : spec_provider; if (prov_name != NULL) { const char *purl = clm_lua_cfg_provider_str(lcfg, prov_name, "url"); const char *pkey = clm_lua_cfg_provider_str( lcfg, prov_name, "api_key"); const char *pkind = clm_lua_cfg_provider_str(lcfg, prov_name, "kind"); if (api_base == NULL && purl != NULL) api_base = purl; if (pkey != NULL && getenv("CLM_API_KEY") == NULL) cfg.api_key = pkey; cfg.provider = clm_provider_from_str(pkind); cfg.provider_name = prov_name; cfg.rate_tokens_per_sec = clm_lua_cfg_provider_int( lcfg, prov_name, "rate_tokens_per_sec", 0); cfg.rate_burst = clm_lua_cfg_provider_int( lcfg, prov_name, "rate_burst", 0); cfg.disable_parallel_tool_calls = clm_lua_cfg_provider_int(lcfg, prov_name, "disable_parallel_tool_calls", 0) != 0; } clm_cfg_apply_tuning(lcfg, spec_provider != NULL ? spec_provider : prov_name, spec_model, &cfg); if (spec_model != NULL) model_name = spec_model; /* Model entry wins over the provider-wide default. */ if (prov_name != NULL) effort = clm_lua_cfg_provider_str(lcfg, prov_name, "effort"); if (spec_provider != NULL && spec_model != NULL) { const char *me = clm_lua_cfg_provider_model_str( lcfg, spec_provider, spec_model, "effort"); if (me != NULL) effort = me; } } /* Defaults for anything not set by config or CLI. */ if (api_base == NULL) api_base = "http://127.0.0.1:8081/v1"; if (model_name == NULL) model_name = "local-model"; /* -u takes the base endpoint; the provider-specific path is appended * by clm_provider_build_url() -- see clm/provider.h's endpoint_path * for why this must be the one place that logic lives. */ clm_provider_build_url( endpoint, sizeof(endpoint), api_base, cfg.provider); /* API key: env > config > none. An empty string is a valid, explicit * "this connection needs no key" (e.g. config.lua's api_key = * clm.secrets.llm7 resolving to "" for a real free-tier provider), * not the same as unset -- checked here too, not just == NULL, so * CLM_API_KEY can still override an explicitly-empty config value. */ if (cfg.api_key == NULL || cfg.api_key[0] == '\0') { const char *key = getenv("CLM_API_KEY"); cfg.api_key = (key != NULL && key[0] != '\0') ? key : ""; } cfg.base_url = endpoint; cfg.model = model_name; cfg.max_iterations = 0; cfg.stream = stream; autofree char *sysinfo = clm_cli_sysinfo(); /* Grown below with the scratch directory, once the session that * names it exists. */ autofree char *scratch = NULL; autofree char *host_block = NULL; cfg.system_prompt_suffix = sysinfo; if (lcfg != NULL) { cfg.system_prompt = clm_lua_cfg_get_str(lcfg, "system_prompt"); /* Agent policy: fnmatch patterns for tools whose old results * get stubbed once a newer one lands (see clm_cfg). The agent * borrows this, so it outlives every agent and is released * once they are all gone. */ volatile_tools = clm_lua_cfg_get_str_list(lcfg, "volatile_tools"); cfg.volatile_tools = (const char *const *)volatile_tools; } /* * Default to the ncurses UI when we're interactive: no oneshot, not * forced headless, and stdin+stdout are both a terminal. Otherwise fall * through to the plain stdio path (works for pipes and --oneshot). */ if (resume && (oneshot != NULL || headless)) { fprintf(stderr, "error: --resume needs the interactive TUI " "(not --oneshot/--headless)\n"); return 1; } if (oneshot == NULL && !headless && isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) { struct clm_session *sess = NULL; struct clm_history restore; struct clm_history *restorep = NULL; autofree char *picked = NULL; int rc; int repaired_tool_calls = 0; clm_history_init(&restore); if (resume) { if (resume_id == NULL) { picked = pick_session(); if (picked == NULL) return 1; resume_id = picked; } /* Accept a tail of the id ("8ed1d645", or any * unambiguous piece of one) so nobody has to type * the whole timestamp. */ if (clm_session_load(NULL, resume_id, &restore, NULL) != 0) { autofree char *full = resolve_session_prefix(resume_id); if (full == NULL) return 1; free(picked); picked = full; full = NULL; resume_id = picked; } r = clm_session_load(NULL, resume_id, &restore, NULL); /* Patch any dangling tool call left by a crash before * it reaches a provider (see clm_history_repair_ * dangling_tool_calls's doc comment); tui_run() reports * the count once curses is up. */ if (r == 0) repaired_tool_calls = clm_history_repair_dangling_tool_calls( &restore); if (r == 0) r = clm_session_open(NULL, resume_id, &sess); if (r < 0) { fprintf(stderr, "error: resuming %s: %s\n", resume_id, strerror(-r)); clm_history_free(&restore); return 1; } restorep = &restore; } else { /* Sweep stale logs before starting a new one, so the * directory does not grow forever. Config decides how * long to keep them; 0 keeps everything. */ int64_t keep_days = lcfg != NULL ? clm_lua_cfg_get_int(lcfg, "session_keep_days", CLM_SESSION_KEEP_DAYS) : CLM_SESSION_KEEP_DAYS; if (keep_days > 0) (void)clm_session_gc( NULL, (unsigned)keep_days, NULL); sweep_scratch(keep_days); /* Session logging is best-effort: a read-only HOME * shouldn't keep the TUI from running at all. */ r = clm_session_create(NULL, model_name, cfg.provider_name, agent_name, &sess); if (r < 0) fprintf(stderr, "warning: session logging " "disabled: %s\n", strerror(-r)); } /* Named after the session so the sweep below can tell a * directory whose session is gone from one still in use. */ if (sess != NULL) { host_block = attach_scratch( clm_session_id(sess), sysinfo, true, &scratch); if (host_block != NULL) cfg.system_prompt_suffix = host_block; } rc = tui_run(&cfg, plugin_dir, lcfg, config_load_err, forever_prompt, sess, restorep, repaired_tool_calls, allow_all); clm_history_free(&restore); clm_lua_cfg_free_str_list(volatile_tools); clm_lua_cfg_free(lcfg); free(spec_provider); return rc; } /* Heap-allocated so the 1 KB input buffer stays off main's stack frame. */ state = calloc(1, sizeof(*state)); if (state == NULL) { fprintf(stderr, "error: out of memory\n"); return 1; } loop = uv_default_loop(); state->loop = loop; state->oneshot = (oneshot != NULL); /* No session here to name the directory, so key it by pid; the empty * ones are removed on the way out and the sweep takes the rest. */ { char key[32]; (void)snprintf(key, sizeof(key), "run-%ld", (long)getpid()); host_block = attach_scratch(key, sysinfo, false, &scratch); if (host_block != NULL) cfg.system_prompt_suffix = host_block; } r = clm_host_uv_new(loop, &state->host); if (r < 0) { fprintf(stderr, "error: failed to create host (%d)\n", r); free(state); return 1; } r = clm_agent_new( &cfg, state->host, &cli_callbacks, state, &state->agent); if (r < 0) { fprintf(stderr, "error: failed to create agent (%d)\n", r); clm_host_uv_free(state->host); (void)clm_drain_loop(loop); free(state); return 1; } if (effort != NULL && clm_agent_set_effort(state->agent, effort) < 0) fprintf( stderr, "warning: could not set effort \"%s\"\n", effort); /* Client-only peer messaging: a headless run can find and message the * agents that are running, but does not advertise itself -- it exits * as soon as its turn does. */ if (clm_peer_start(state->agent, loop, NULL, "clm", model_name, NULL, NULL, &state->peer) == 0) (void)clm_peer_register_tools(state->agent); /* Desktop uv layer: add the shell_exec/bg_exec tools (not in the * portable core). */ clm_tools_register_shell(state->agent); clm_tools_register_bg(state->agent); if (clm_lua_env_new(state->agent, &state->lua_env) == 0) { if (lcfg != NULL) clm_lua_env_set_config_from(state->lua_env, lcfg); if (plugin_dir != NULL) { clm_lua_load_plugins(state->lua_env, plugin_dir); } else { autofree char *ppath = xdg_config_path("clm/plugins"); if (ppath != NULL) { clm_seed_default_plugins(ppath); clm_lua_load_plugins(state->lua_env, ppath); } } } state->mcp_clients = clm_cli_connect_mcp_servers(state->agent, loop, lcfg, cb_mcp_status, state, &state->mcp_client_count); clm_cli_wait_mcp_ready(loop, CLM_MCP_READY_WAIT_MS); if (oneshot != NULL) { r = clm_agent_submit(state->agent, oneshot); if (r < 0) { fprintf(stderr, "error: %s\n", clm_agent_get_last_error(state->agent)); clm_lua_env_free(state->lua_env); clm_cli_free_mcp_servers( state->mcp_clients, state->mcp_client_count); clm_peer_free(state->peer); clm_agent_free(state->agent); clm_host_uv_free(state->host); (void)clm_drain_loop(loop); free(state); return 1; } while (!state->turn_done) uv_run(loop, UV_RUN_ONCE); printf("\n"); clm_lua_env_free(state->lua_env); clm_cli_free_mcp_servers( state->mcp_clients, state->mcp_client_count); clm_peer_free(state->peer); clm_agent_free(state->agent); clm_host_uv_free(state->host); (void)clm_drain_loop(loop); r = state->turn_status; free(state); if (scratch != NULL) (void)rmdir(scratch); /* only if nothing was left */ clm_lua_cfg_free_str_list(volatile_tools); clm_lua_cfg_free(lcfg); free(spec_provider); return r == 0 ? 0 : 1; } printf("clm agent. api: %s\n", endpoint); printf("type 'quit' or 'exit' to stop.\n\n"); printf("user> "); fflush(stdout); if (uv_guess_handle(fileno(stdin)) == UV_FILE) { run_stdin_batch(state, loop); } else { uv_pipe_init(loop, &state->stdin_pipe, 0); uv_pipe_open(&state->stdin_pipe, fileno(stdin)); state->stdin_pipe.data = (uv_handle_t *)state; uv_read_start((uv_stream_t *)&state->stdin_pipe, on_alloc_buffer, on_stdin_read); uv_run(loop, UV_RUN_DEFAULT); clm_settle_turn(state->agent, loop); /* Ours to close: nothing else does, and it must be down * before the drain runs the loop again. */ if (!uv_is_closing((uv_handle_t *)&state->stdin_pipe)) uv_close((uv_handle_t *)&state->stdin_pipe, NULL); } clm_lua_env_free(state->lua_env); clm_cli_free_mcp_servers(state->mcp_clients, state->mcp_client_count); clm_peer_free(state->peer); clm_agent_free(state->agent); clm_host_uv_free(state->host); (void)clm_drain_loop(loop); free(state); if (scratch != NULL) (void)rmdir(scratch); /* only if nothing was left */ clm_lua_cfg_free_str_list(volatile_tools); clm_lua_cfg_free(lcfg); free(spec_provider); return 0; }