742 lines · 21.6 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <errno.h>#include <fcntl.h>#include <getopt.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/stat.h>#include <time.h>#include <unistd.h>#include <uv.h> #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 "mcp_setup.h"#include "model_spec.h"#include "templates.h"#include "xdg.h" static voidusage(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] "	    "[-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"	    "  -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");} 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_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 turn_done;	int turn_status;}; static voidcb_mcp_status(const char *msg, void *user){	(void)user;	fprintf(stderr, "%s\n", msg);} static voidcb_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 voidcb_reasoning(const char *text, void *user){	(void)user;	printf("\033[2m%s\033[0m", text); /* dim "thinking" channel */	fflush(stdout);} static voidcb_finish_reason(enum clm_finish_reason reason, void *user){	(void)user;	if (reason == CLM_FINISH_LENGTH)		printf("\n\033[33m[truncated: hit token limit]\033[0m\n");	else if (reason == CLM_FINISH_CONTENT_FILTER)		printf("\n\033[31m[stopped: content filter]\033[0m\n");	fflush(stdout);} static voidcb_usage(const struct clm_usage *usage, void *user){	(void)user;	printf("\033[2m[%d+%d tok", usage->prompt_tokens, usage->completion_tokens);	if (usage->tokens_per_sec > 0)		printf(", %.1f tok/s", usage->tokens_per_sec);	printf("]\033[0m\n");	fflush(stdout);} static voidcb_tool_begin(const char *name, const char *args, void *user){	(void)user;	printf("[tool: %s %s]\n", name, args ? args : "");	fflush(stdout);} static voidcb_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("\033[31m[x %s]\033[0m %s\n", name, content ? content : "");		break;	case CLM_TOOL_TIMEDOUT:		printf("\033[33m[timeout %s]\033[0m %s\n", name, content ? content : "");		break;	}	fflush(stdout);} static voidcb_tool_batch(size_t completed, size_t total, void *user){	(void)user;	printf("[tools %zu/%zu]\n", completed, total);	fflush(stdout);} static voidcb_state(enum clm_agent_state state, void *user){} static voidcb_notice(const char *text, void *user){	(void)user;	fprintf(stderr, "notice: %s\n", text ? text : "");} static voidcb_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->turn_done = 1;		state->turn_status = status;		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 voidcb_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 voidon_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;} static voidon_stdin_close(uv_handle_t *handle){	struct cli_state *state = (struct cli_state *)handle->data;	uv_stop(state->loop);} static voidon_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 intwrite_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). */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 intrun_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;} intmain(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 *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), process-lifetime like	 * plenty else in this function -- cfg.provider_name below may alias	 * it, and cfg is still read as late as tui_run()'s call at the very	 * end of main(), well past any block-scoped cleanup. */	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; 	if (argc >= 2 && strcmp(argv[1], "setup") == 0)		return run_setup(); 	int resume = 0;	const char *resume_id = NULL; 	enum { OPT_PROVIDER = 256 };	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'},		{"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 '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;

Lines 1–500 of 742.