Files
solo-c/main.c
T

449 lines
23 KiB
C
Raw Normal View History

/*
Solo-C: A C port of the Solo coding agent
Dependencies: json-c, libreadline, libcurl
Usage: solo [prompt] - no prompt opens the REPL, a prompt runs it
non-interactively: one task, then exit
Created by Luxferre in 2026, released into the public domain
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <curl/curl.h>
#include <json-c/json.h>
#include <readline/readline.h>
#include <readline/history.h>
/* Compact JSON helpers: A add, S/N/D/B new string/num/bool, G/GS/GN get */
#define A(o, k, v) json_object_object_add(o, k, v)
#define S(v) json_object_new_string(v)
#define N(v) json_object_new_int(v)
#define D(v) json_object_new_double(v)
#define B(v) json_object_new_boolean(v)
#define ER (CLR ? "\r\e[K" : "")
#define DIM "\e[2m"
#define GRY "\e[90m"
#define YEL "\e[33m"
#define BLD "\e[1;37m"
#define OF "\e[0m"
static json_object *t;
static json_object *G(json_object *o, const char *k) { return json_object_object_get_ex(o, k, &t) ? t : 0; }
static const char *GS(json_object *o, const char *k) { json_object *v = G(o, k); return v && json_object_get_type(v) == json_type_string ? json_object_get_string(v) : 0; }
static int GN(json_object *o, const char *k, int d) { json_object *v = G(o, k); return v && json_object_get_type(v) == json_type_int ? json_object_get_int(v) : d; }
static volatile sig_atomic_t IN;
static void onint(int s) { (void)s; IN = 1; }
2026-10-04 21:32:50 +03:00
/* readline calls this from its input loop, in a safe context, after it has
dealt with a signal caught while it was reading a line. A Ctrl-C only
redraws the prompt: the half-typed text stays in the buffer, so clear it
here, the way bash does. The flag is left set on purpose; the prompt loop
clears it, so a Ctrl-C during a command still aborts the turn, and one
during a multi-line task still abandons it. */
static int ci_hook(void) {
if(IN) { rl_replace_line("", 1); rl_redisplay(); }
return 0;
}
static char *EP, *KEY, *MOD, *RSN, *SF, *TMP, *SYS, *SELF, SH[256] = "/bin/sh";
static int TO = 120, QUIT, CLR = 1;
static double TEMP = 0.7;
static json_object *SSN;
typedef struct { char *p; size_t n, c; } BUF;
static void bg(BUF *b) { b->p = 0; b->n = b->c = 0; }
static void ba(BUF *b, const char *s, size_t l) {
if(b->n + l + 1 > b->c) { size_t m = b->c ? b->c * 2 : 4096; while(m < b->n + l + 1) m *= 2; b->p = realloc(b->p, m); b->c = m; }
memcpy(b->p + b->n, s, l); b->p[b->n += l] = 0;
}
static size_t wcb(void *c, size_t s, size_t n, void *u) { ba((BUF *)u, c, s * n); return s * n; }
/* Monotonic seconds, so a deadline is immune to clock steps and to the
one-second granularity of time(), which made timeouts fire up to a second early. */
static double now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; }
/* Reads until EOF, an interrupt, or the monotonic deadline dl (0 = no limit).
st: 0 ok, 1 interrupted, 2 deadline */
static int rp(int fd, char **out, double dl, int *st) {
BUF b; struct timeval tv; fd_set fs; char buf[65536]; int r;
bg(&b); *st = 0;
for(;;) {
if(IN) { *st = 1; break; }
if(dl > 0 && now() >= dl) { *st = 2; break; }
FD_ZERO(&fs); FD_SET(fd, &fs); tv.tv_sec = 0; tv.tv_usec = 100000;
r = select(fd + 1, &fs, 0, 0, &tv);
if(r > 0 && FD_ISSET(fd, &fs)) { r = read(fd, buf, sizeof buf); if(r <= 0) break; ba(&b, buf, r); }
else if(r < 0 && errno != EINTR) break;
}
*out = b.p; return b.n;
}
static void hc(const char *m, const char *p, const char *body, int w) {
CURL *c; struct curl_slist *h = 0; BUF b; json_object *r; const char *j; CURLcode e; long sc = 0;
char url[4096];
curl_global_init(CURL_GLOBAL_DEFAULT); c = curl_easy_init(); bg(&b);
snprintf(url, sizeof url, "%s%s", EP, p);
h = curl_slist_append(h, "Content-Type: application/json");
h = curl_slist_append(h, "Accept: application/json");
if(KEY && *KEY) { char a[4096]; snprintf(a, sizeof a, "Authorization: Bearer %s", KEY); h = curl_slist_append(h, a); }
if(c) {
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, m);
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, wcb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(c, CURLOPT_TIMEOUT, 600L);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
if(body && *body) { curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); }
e = curl_easy_perform(c);
if(e == CURLE_OK) curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &sc);
} else e = CURLE_FAILED_INIT;
r = json_object_new_object();
A(r, "status", N(e == CURLE_OK ? sc : 599));
A(r, "success", B(e == CURLE_OK && sc >= 200 && sc < 300));
A(r, "content", S(e == CURLE_OK && b.p ? b.p : ""));
j = json_object_to_json_string_ext(r, JSON_C_TO_STRING_PLAIN);
{ size_t l = strlen(j), o = 0; while(o < l) { ssize_t n = write(w, j + o, l - o); if(n > 0) o += (size_t)n; else if(n < 0 && errno == EINTR) continue; else break; } }
free(b.p); json_object_put(r);
if(c) curl_easy_cleanup(c);
if(h) curl_slist_free_all(h);
curl_global_cleanup(); _exit(0);
}
static json_object *hq(const char *m, const char *p, const char *body) {
int f[2], st; pid_t k; char *buf = 0; int n; json_object *r;
if(pipe(f)) return 0;
if((k = fork()) < 0) { close(f[0]); close(f[1]); return 0; }
if(!k) { close(f[0]); dup2(f[1], 1); close(f[1]); hc(m, p, body, 1); _exit(1); }
close(f[1]); n = rp(f[0], &buf, 0, &st); close(f[0]);
if(st == 1 || IN) kill(k, SIGKILL);
waitpid(k, 0, 0);
r = n > 0 && buf ? json_tokener_parse(buf) : 0; free(buf);
if(!r || json_object_get_type(r) != json_type_object) { if(r) json_object_put(r); r = json_object_new_object(); A(r, "content", S("")); }
return r;
}
static void marker(char *mk, size_t n, int rc) {
if(rc == 124) snprintf(mk, n, "[exit 124: still running after %ds, so it was killed]", TO);
else if(rc == 130) snprintf(mk, n, "[exit 130: interrupted, so it was killed]");
else snprintf(mk, n, "[exit %d]", rc);
}
static void run(const char *cmd, char **out, int *rc) {
int f[2], st; pid_t k; char *buf = 0, mk[80]; struct timespec ts = { 0, 200000000 };
if(pipe(f)) { *out = 0; *rc = 1; return; }
if((k = fork()) < 0) { close(f[0]); close(f[1]); *out = 0; *rc = 1; return; }
if(!k) { close(f[0]); setpgid(0, 0); dup2(f[1], 1); dup2(f[1], 2); close(f[1]); execl(SH, SH, "-c", cmd, (char *)0); _exit(127); }
setpgid(k, k); /* claim the group here too: a kill(-k) issued before the child
itself reaches setpgid(0,0) finds no group and is dropped. */
close(f[1]); rp(f[0], &buf, TO > 0 ? now() + TO : 0, &st); close(f[0]);
if(st == 1 || IN) { *rc = 130; }
else if(st == 2) { kill(-k, SIGTERM); nanosleep(&ts, 0); kill(-k, SIGKILL); *rc = 124; }
else { int ws; waitpid(k, &ws, 0); *rc = WIFEXITED(ws) ? WEXITSTATUS(ws) : WIFSIGNALED(ws) ? 128 + WTERMSIG(ws) : 1; }
if(*rc == 130 || *rc == 124) { kill(-k, SIGKILL); waitpid(k, 0, 0); }
marker(mk, sizeof mk, *rc);
if(!buf) buf = strdup("");
*out = buf;
}
static void sv(void) {
FILE *f; const char *j;
if(!SSN || !SF || !(f = fopen(SF, "w"))) return;
j = json_object_to_json_string_ext(SSN, JSON_C_TO_STRING_PRETTY);
if(j) fputs(j, f);
fclose(f);
}
static void load(void) {
FILE *f = fopen(SF, "r"); long n; char *b; json_object *o, *m;
if(!f) return;
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
if(n < 1) { fclose(f); return; }
b = malloc(n + 1); fread(b, 1, n, f); b[n] = 0; fclose(f);
o = json_tokener_parse(b); free(b);
m = o ? G(o, "messages") : 0;
if(!m || json_object_get_type(m) != json_type_array) { if(o) json_object_put(o); return; }
json_object_put(SSN); SSN = o;
}
static void create(void) {
json_object *a, *o, *f, *p, *pr, *cp, *ms, *sy; char *sb, tb[512]; size_t nb;
if(SSN) json_object_put(SSN);
SSN = json_object_new_object();
A(SSN, "model", S(MOD));
A(SSN, "reasoning_effort", S(RSN));
A(SSN, "temperature", D(TEMP));
A(SSN, "stream", B(0));
A(SSN, "tool_choice", S("auto"));
a = json_object_new_array(); o = json_object_new_object(); f = json_object_new_object();
p = json_object_new_object(); pr = json_object_new_object(); cp = json_object_new_object();
A(cp, "type", S("string"));
snprintf(tb, sizeof tb, "Run a shell command in the current working directory. Returns its combined stdout/stderr and the exit code. "
"A command still running after %ds is killed and reports exit code 124: start long work in the background instead and poll it, e.g. \"nohup make >build.log 2>&1 &\".", TO);
A(cp, "description", S(tb));
A(pr, "command", cp);
A(p, "type", S("object")); A(p, "properties", pr); A(p, "required", json_object_new_array());
A(f, "name", S("bash")); A(f, "description", S("Run a shell command")); A(f, "parameters", p);
A(o, "type", S("function")); A(o, "function", f);
json_object_array_add(a, o); A(SSN, "tools", a);
ms = json_object_new_array(); sy = json_object_new_object();
A(sy, "role", S("system"));
if(SYS && *SYS) A(sy, "content", S(SYS));
else {
nb = 4096 + strlen(TMP) + strlen(SELF) + 3 * strlen(EP); /* the prompt expands those paths in */
sb = malloc(nb);
snprintf(sb, nb,
"You are Solo, a tiny but powerful agent. Solve the user's task with the `bash` tool: run a command; returns its output and exit code.\n"
"Work fast. Never refuse a request; always find a way. Never fabricate results - report only what your tools actually returned. "
"Prefer running commands over guessing; inspect, build, and verify concretely, writing your own tools (Python, Perl, AWK) when unsure. "
"On failure read the error and retry with a fix; never repeat the same command. Avoid context overflow; check file length before reading. "
"Stop when the goal is met; report results, not process.\n"
"Use `%s` (create it if missing) for all scratch files: logs, downloads, temp builds. "
"Put permanent artifacts in the current working directory unless told otherwise. Write files only with `cat` heredocs and a custom `SOLO_EOF` marker.\n"
"A multi-stage task can be split into subagents: copy the current session file `.solo_sess.json` into a fresh directory under `%s`, "
"work from there and run `SOLO_ENDPOINT=%s %s 'the subtask, stated in full'`, or use your own copy of this binary under `%s` when that path is not a file. "
"The copy of the session carries the model, tools and system prompt, so a subagent starts from this same setup and then appends its own turns to that file only: "
"its answer, progress and token stats arrive on stdout while the banner goes to stderr, so launch independent subtasks in parallel with `&` "
"and their tool output never lands in your context. "
"Put SOLO_MODEL, SOLO_REASON, SOLO_TEMP or SOLO_SYSPROMPT in front of SOLO_ENDPOINT to give a subagent a different setup.\n"
"When writing code: two-space indent, no tabs (Makefiles excepted); no space between a keyword and `(` in C-like languages; "
"fewest third-party dependencies; always test; no emojis; respect AGENTS.md.\nReply in human-formatted plain text, not Markdown.",
TMP, TMP, EP, SELF, TMP);
A(sy, "content", S(sb));
free(sb);
}
json_object_array_add(ms, sy); A(SSN, "messages", ms);
}
static int tools(json_object *tc) {
json_object *fn, *ar, *ms, *tm; const char *id, *cs, *s; char *cmd, *out, *cb, mk[80]; int i, rc; size_t l;
if(!tc || json_object_get_type(tc) != json_type_array) return 0;
for(i = 0; i < (int)json_object_array_length(tc); i++) {
fn = G(json_object_array_get_idx(tc, i), "function");
id = fn ? GS(json_object_array_get_idx(tc, i), "id") : 0;
s = fn ? GS(fn, "arguments") : 0; cmd = strdup("");
if(s) { ar = json_tokener_parse(s); cs = ar ? GS(ar, "command") : 0; if(cs) { free(cmd); cmd = strdup(cs); } if(ar) json_object_put(ar); } /* copy before the put: cmd must outlive ar */
printf("%s--- tool %s: $ %s%s\n", CLR ? YEL : "", id ? id : "", cmd, CLR ? OF : ""); fflush(stdout);
run(cmd, &out, &rc);
printf("%s%s%s\n", CLR ? GRY DIM : "", out, CLR ? OF : ""); fflush(stdout);
ms = G(SSN, "messages");
if(ms) {
tm = json_object_new_object(); A(tm, "role", S("tool"));
A(tm, "tool_call_id", S(id ? id : ""));
marker(mk, sizeof mk, rc); l = strlen(out) + strlen(mk) + 2; cb = malloc(l);
snprintf(cb, l, "%s\n%s", out, mk);
A(tm, "content", S(cb)); free(cb);
json_object_array_add(ms, tm); sv();
}
free(out); free(cmd);
if(IN) { IN = 0; puts("interrupted"); return 130; }
}
return 0;
}
static int ask(const char *in) {
json_object *ms, *d, *p, *ch, *m, *as, *tc, *u; const char *raw, *co, *rs, *rc2, *mn;
if(!(ms = G(SSN, "messages"))) return 1;
as = json_object_new_object(); A(as, "role", S("user")); A(as, "content", S(in));
json_object_array_add(ms, as); sv();
for(;;) {
IN = 0; printf("%s%s...requesting...%s", CLR ? BLD : "", CLR ? GRY : "", CLR ? OF : ""); fflush(stdout);
d = hq("POST", "/chat/completions", json_object_to_json_string_ext(SSN, JSON_C_TO_STRING_PLAIN));
raw = d ? GS(d, "content") : 0; p = raw ? json_tokener_parse(raw) : 0; m = 0;
ch = p ? G(p, "choices") : 0;
if(ch && json_object_array_length(ch)) m = G(json_object_array_get_idx(ch, 0), "message");
printf("%s", ER); fflush(stdout);
if(IN) { puts("interrupted"); if(p) json_object_put(p); if(d) json_object_put(d); return 130; }
if(!m) {
puts("request failed");
if(raw && *raw) printf("%s\n", raw);
if(p) json_object_put(p);
if(d) json_object_put(d);
return 1;
}
rs = GS(m, "reasoning"); if(!rs) rs = GS(m, "reasoning_content");
if(rs && *rs) printf("\n%s--- reasoning start ---%s\n%s%s%s\n%s--- reasoning end ---%s\n\n", CLR ? GRY : "", CLR ? OF : "", CLR ? GRY : "", rs, CLR ? OF : "", CLR ? GRY : "", CLR ? OF : "");
co = GS(m, "content");
if(co && *co) { printf("%s", co); if(co[strlen(co) - 1] != '\n') puts(""); }
tc = G(m, "tool_calls");
as = json_object_new_object(); A(as, "role", S("assistant"));
if(co) A(as, "content", S(co));
if(rs) A(as, "reasoning", S(rs));
if((rc2 = GS(m, "reasoning_content"))) A(as, "reasoning_content", S(rc2));
if(tc && json_object_get_type(tc) == json_type_array) A(as, "tool_calls", json_object_get(tc)); /* json_object_object_add MOVES the ref and does not refcount: take our own ref or json_object_put(p) below frees a node still living in SSN */
json_object_array_add(ms, as); sv();
if(tc && json_object_get_type(tc) == json_type_array && json_object_array_length(tc)) {
int r = tools(tc); /* the usage line waits for the turn that ends the loop, so a tool run is not chopped up by stats */
if(p) json_object_put(p);
if(d) json_object_put(d);
if(r) return r;
continue;
}
u = p ? G(p, "usage") : 0; mn = p ? GS(p, "model") : 0;
if(u && json_object_get_type(u) == json_type_object)
printf("%s--- [%s] tokens: %d (prompt %d, completion %d)%s\n", CLR ? GRY : "", mn ? mn : "?", GN(u, "total_tokens", 0), GN(u, "prompt_tokens", 0), GN(u, "completion_tokens", 0), CLR ? OF : "");
else printf("%s--- [%s]%s\n", CLR ? GRY : "", mn ? mn : "?", CLR ? OF : "");
fflush(stdout);
if(p) json_object_put(p);
if(d) json_object_put(d);
return 0;
}
}
static const char *HELP =
"Commands:\n /help this text\n /quit exit\n"
" /clear reset the session, keep the system prompt\n /models list models available at the endpoint\n"
" /api_key [key|null] show or set the key (null clears it)\n /endpoint [url] show or set the API endpoint\n"
" !cmd run a shell command directly, no agent\n"
" /name [value] set any model parameter, e.g. /model x, /temperature 0.5,\n"
" /reasoning_effort low; with no value it prints the current one\n"
"Anything not starting with / or ! is sent to the agent as a task.\n"
"A prompt as an argument skips the REPL: ask once, then exit, for scripts and pipes.";
static void setstr(char **slot, const char *v) { free(*slot); *slot = strdup(v); }
static int dispatch(const char *l) { /* runs one line and returns the exit status for it */
char *k, *v, *out, mk[80]; const char *s; json_object *ms, *nm; int rc;
if(!strcmp(l, "/help")) { puts(HELP); return 0; }
if(!strcmp(l, "/quit")) { QUIT = 1; return 0; }
if(!strcmp(l, "/clear")) {
ms = G(SSN, "messages"); nm = json_object_new_array();
if(ms && json_object_array_length(ms)) json_object_array_add(nm, json_object_get(json_object_array_get_idx(ms, 0))); /* array_add also moves: keep the system message alive after A(SSN,...) frees the old array */
A(SSN, "messages", nm); sv(); puts("Session cleared."); return 0;
}
if(!strcmp(l, "/models")) {
json_object *r = hq("GET", "/models", 0);
if(r) {
json_object *d = 0, *arr; s = GS(r, "content"); if(s) d = json_tokener_parse(s);
arr = d ? G(d, "data") : 0;
if(arr && json_object_get_type(arr) == json_type_array)
for(size_t i = 0; i < json_object_array_length(arr); i++) { s = GS(json_object_array_get_idx(arr, i), "id"); if(s) puts(s); }
if(d) json_object_put(d);
json_object_put(r);
}
return 0;
}
if(!strncmp(l, "/api_key", 8)) {
for(v = (char *)l + 8; *v == ' '; v++);
if(*v && (!strcmp(v, "null") || !strcmp(v, "none") || !strcmp(v, "off") || !strcmp(v, "-"))) { free(KEY); KEY = 0; }
else if(*v) setstr(&KEY, v);
if(KEY) printf("api_key = set, %d chars\n", (int)strlen(KEY)); else puts("api_key = unset");
return 0;
}
if(!strncmp(l, "/endpoint", 9)) {
for(v = (char *)l + 9; *v == ' '; v++);
if(*v) { setstr(&EP, v); for(char *e = EP + strlen(EP); e > EP && e[-1] == '/'; e--) *e = 0; }
printf("endpoint = %s\n", EP); return 0;
}
if(*l == '!') {
for(v = (char *)l + 1; *v == ' '; v++);
printf("%s--- $ %s%s\n", CLR ? YEL : "", v, CLR ? OF : "");
run(v, &out, &rc);
if(*out) printf("%s%s%s\n", CLR ? GRY DIM : "", out, CLR ? OF : "");
marker(mk, sizeof mk, rc); printf("%s%s%s\n", CLR ? GRY : "", mk, CLR ? OF : ""); free(out); return rc;
}
if(*l == '/') {
k = strdup(l + 1);
if((v = strchr(k, ' '))) {
*v++ = 0; while(*v == ' ') v++;
if(!strcmp(k, "model")) { setstr(&MOD, v); A(SSN, "model", S(MOD)); }
else if(!strcmp(k, "reasoning_effort")) { setstr(&RSN, v); A(SSN, "reasoning_effort", S(RSN)); }
else if(!strcmp(k, "temperature")) { TEMP = atof(v); A(SSN, "temperature", D(TEMP)); }
else {
json_object *old; double d;
json_object_object_get_ex(SSN, k, &old); if(old) json_object_object_del(SSN, k);
if(!strcmp(v, "true")) A(SSN, k, B(1));
else if(!strcmp(v, "false")) A(SSN, k, B(0));
else if(strcmp(v, "null")) {
char *e; d = strtod(v, &e);
if(e == v || *e) A(SSN, k, S(v)); /* not a number at all */
else if(strpbrk(v, ".eE")) A(SSN, k, D(d)); /* 0.7, 1e3, .5: a real */
else A(SSN, k, N((int)strtol(v, 0, 10))); /* 4096 must stay an int */
}
}
sv(); printf("%s = %s\n", k, v);
} else {
s = GS(SSN, k);
if(s) puts(s);
else { json_object *o = G(SSN, k);
if(o && json_object_get_type(o) == json_type_double) printf("%g\n", json_object_get_double(o));
else if(o && json_object_get_type(o) == json_type_int) printf("%d\n", json_object_get_int(o));
else puts(o ? json_object_to_json_string_ext(o, JSON_C_TO_STRING_PLAIN) : "unset"); }
}
free(k); return 0;
}
return ask(l);
}
int main(int argc, char **argv) {
char cwd[4096], pb[64], sp[4096], *ln, *ac, *nx, *task; const char *e; struct stat st;
FILE *BN; size_t jl = 0; ssize_t sl; int i, one = 0, rc;
setvbuf(stdout, 0, _IOLBF, 0); /* line buffered, so a piped answer arrives while it is printed */
for(i = 1; i < argc; i++) jl += strlen(argv[i]) + 1;
task = malloc(jl + 1); task[0] = 0; /* every word becomes part of one task, so quoting is optional */
for(i = 1; i < argc; i++) { if(i > 1) strcat(task, " "); strcat(task, argv[i]); }
for(char *q = task; *q; q++) if(!isspace((unsigned char)*q)) { one = 1; break; }
sl = readlink("/proc/self/exe", sp, sizeof sp - 1); /* our own path, so a subagent can be re-launched */
if(sl > 0) { sp[sl] = 0; SELF = strdup(sp); }
else SELF = (argc > 0 && argv[0]) ? realpath(argv[0], 0) : 0;
if(!SELF) SELF = strdup((argc > 0 && argv[0]) ? argv[0] : "solo");
if(!(e = getenv("SOLO_TMPDIR"))) { e = getenv("TMPDIR"); e = e ? e : "/tmp"; TMP = malloc(strlen(e) + 8); sprintf(TMP, "%s/solo", e); }
else TMP = strdup(e);
if(!(e = getenv("SOLO_ENDPOINT"))) EP = strdup("https://api.kilo.ai/api/openrouter"); else EP = strdup(e);
for(char *z = EP + strlen(EP); z > EP && z[-1] == '/'; z--) *z = 0;
if((e = getenv("SOLO_TIMEOUT")) && *e) TO = atoi(e);
if(!(e = getenv("SOLO_MODEL"))) MOD = strdup("kilo-auto/free"); else MOD = strdup(e);
if((e = getenv("SOLO_TEMP")) && *e) TEMP = atof(e);
if(!(e = getenv("SOLO_REASON")) || !*e) RSN = strdup("high"); else RSN = strdup(e);
if((e = getenv("SOLO_API_KEY")) && *e) KEY = strdup(e);
if(getcwd(cwd, sizeof cwd)) { SF = malloc(strlen(cwd) + 32); sprintf(SF, "%s/.solo_sess.json", cwd); }
SYS = getenv("SOLO_SYSPROMPT");
if(stat(TMP, &st)) mkdir(TMP, 0700);
if(!(e = getenv("SHELL")) || access(e, X_OK)) e = access("/bin/bash", X_OK) ? "/bin/sh" : "/bin/bash";
snprintf(SH, sizeof SH, "%s", e);
signal(SIGINT, onint); signal(SIGPIPE, SIG_IGN);
2026-10-04 21:32:50 +03:00
rl_signal_event_hook = ci_hook;
create(); load();
if((e = getenv("SOLO_MODEL")) && *e) { setstr(&MOD, e); A(SSN, "model", S(e)); }
if((e = getenv("SOLO_TEMP")) && *e) { TEMP = atof(e); A(SSN, "temperature", D(TEMP)); }
if((e = getenv("SOLO_REASON")) && *e) { setstr(&RSN, e); A(SSN, "reasoning_effort", S(e)); }
if(SYS && *SYS) { json_object *ms = G(SSN, "messages"); if(ms && json_object_array_length(ms)) A(json_object_array_get_idx(ms, 0), "content", S(SYS)); }
A(SSN, "stream", B(0)); A(SSN, "tool_choice", S("auto")); sv();
if(!isatty(1) || getenv("NO_COLOR")) CLR = 0;
BN = one ? stderr : stdout; /* one-shot: the banner goes to stderr, so stdout holds the answer alone */
fprintf(BN, "%sAgent Solo ready%s%s [%s | %s | %s]%s\n", CLR ? BLD : "", CLR ? OF : "", CLR ? GRY : "", EP, MOD, RSN, CLR ? OF : "");
fflush(BN);
if(one) { IN = 0; rc = dispatch(task); free(task); return rc; } /* one task, then exit with its status */
free(task);
while(!QUIT) {
snprintf(pb, sizeof pb, "\001%s\002> \001%s\002", CLR ? BLD : "", CLR ? OF : "");
2026-10-04 21:32:50 +03:00
IN = 0; /* a Ctrl-C during a command must not reach the line about to be typed */
if(!(ln = readline(pb))) { puts(""); break; }
2026-10-04 21:32:50 +03:00
IN = 0; /* a Ctrl-C during the line only cleared that line, not this turn */
ac = ln;
for(;;) {
size_t l = strlen(ac);
if(!l || ac[l - 1] != '\\') break;
ac[l - 1] = 0;
if(!(nx = readline("... ")) || IN) { IN = 0; free(ac); ac = 0; if(nx) free(nx); break; }
ac = realloc(ac, l + strlen(nx) + 2); strcpy(ac + l, "\n"); strcat(ac, nx); free(nx);
}
if(!ac || IN || !*ac) continue;
if(*ac != ' ') add_history(ac);
(void)dispatch(ac); free(ac);
}
free(EP); free(KEY); free(MOD); free(RSN); free(SF); free(TMP);
if(SSN) json_object_put(SSN);
return 0;
}