2026-10-03 09:15:02 +03:00
/*
Solo-C: A C port of the Solo coding agent
Dependencies: json-c, libreadline, libcurl
2026-10-04 17:00:39 +03:00
Usage: solo [prompt] - no prompt opens the REPL, a prompt runs it
non-interactively: one task, then exit
2026-10-03 09:15:02 +03:00
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>
2026-10-04 17:00:39 +03:00
#include <ctype.h>
2026-10-03 09:15:02 +03:00
#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 17:00:39 +03:00
static char * EP , * KEY , * MOD , * RSN , * SF , * TMP , * SYS , * SELF , SH [ 256 ] = "/bin/sh" ;
2026-10-03 09:15:02 +03:00
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 ; }
2026-10-04 06:06:06 +00:00
/* 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 */
2026-10-03 09:15:02 +03:00
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 ; }
2026-10-04 06:06:06 +00:00
if ( dl > 0 && now () >= dl ) { * st = 2 ; break ; }
2026-10-03 09:15:02 +03:00
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 );
2026-10-04 06:06:06 +00:00
{ 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 );
2026-10-03 09:15:02 +03:00
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 ); }
2026-10-04 06:06:06 +00:00
close ( f [ 1 ]); n = rp ( f [ 0 ], & buf , 0 , & st ); close ( f [ 0 ]);
2026-10-03 09:15:02 +03:00
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 ); }
2026-10-04 06:06:06 +00:00
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 ]);
2026-10-03 09:15:02 +03:00
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 ) {
2026-10-04 17:00:39 +03:00
json_object * a , * o , * f , * p , * pr , * cp , * ms , * sy ; char * sb , tb [ 512 ]; size_t nb ;
2026-10-03 09:15:02 +03:00
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" ));
2026-10-04 06:06:06 +00:00
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 ));
2026-10-03 09:15:02 +03:00
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 {
2026-10-04 17:00:39 +03:00
nb = 4096 + strlen ( TMP ) + strlen ( SELF ) + 3 * strlen ( EP ); /* the prompt expands those paths in */
sb = malloc ( nb );
snprintf ( sb , nb ,
2026-10-03 09:15:02 +03:00
"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 "
2026-10-04 17:00:39 +03:00
"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 "
2026-10-03 09:15:02 +03:00
"When writing code: two-space indent, no tabs (Makefiles excepted); no space between a keyword and `(` in C-like languages; "
2026-10-04 17:00:39 +03:00
"fewest third-party dependencies; always test; no emojis; respect AGENTS.md. \n Reply in human-formatted plain text, not Markdown." ,
TMP , TMP , EP , SELF , TMP );
2026-10-03 09:15:02 +03:00
A ( sy , "content" , S ( sb ));
2026-10-04 17:00:39 +03:00
free ( sb );
2026-10-03 09:15:02 +03:00
}
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 )) {
2026-10-04 17:00:39 +03:00
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 */
2026-10-03 09:15:02 +03:00
if ( p ) json_object_put ( p );
if ( d ) json_object_put ( d );
if ( r ) return r ;
continue ;
}
2026-10-04 17:00:39 +03:00
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 );
2026-10-03 09:15:02 +03:00
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 "
2026-10-04 17:00:39 +03:00
"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." ;
2026-10-03 09:15:02 +03:00
static void setstr ( char ** slot , const char * v ) { free ( * slot ); * slot = strdup ( v ); }
2026-10-04 17:00:39 +03:00
static int dispatch ( const char * l ) { /* runs one line and returns the exit status for it */
2026-10-03 09:15:02 +03:00
char * k , * v , * out , mk [ 80 ]; const char * s ; json_object * ms , * nm ; int rc ;
2026-10-04 17:00:39 +03:00
if ( ! strcmp ( l , "/help" )) { puts ( HELP ); return 0 ; }
if ( ! strcmp ( l , "/quit" )) { QUIT = 1 ; return 0 ; }
2026-10-03 09:15:02 +03:00
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 */
2026-10-04 17:00:39 +03:00
A ( SSN , "messages" , nm ); sv (); puts ( "Session cleared." ); return 0 ;
2026-10-03 09:15:02 +03:00
}
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 );
}
2026-10-04 17:00:39 +03:00
return 0 ;
2026-10-03 09:15:02 +03:00
}
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" );
2026-10-04 17:00:39 +03:00
return 0 ;
2026-10-03 09:15:02 +03:00
}
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 ; }
2026-10-04 17:00:39 +03:00
printf ( "endpoint = %s \n " , EP ); return 0 ;
2026-10-03 09:15:02 +03:00
}
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 : "" );
2026-10-04 17:00:39 +03:00
marker ( mk , sizeof mk , rc ); printf ( "%s%s%s \n " , CLR ? GRY : "" , mk , CLR ? OF : "" ); free ( out ); return rc ;
2026-10-03 09:15:02 +03:00
}
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 ));
2026-10-04 06:06:06 +00:00
else if ( strcmp ( v , "null" )) {
2026-10-03 09:15:02 +03:00
char * e ; d = strtod ( v , & e );
2026-10-04 06:06:06 +00:00
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 */
2026-10-03 09:15:02 +03:00
}
}
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" ); }
}
2026-10-04 17:00:39 +03:00
free ( k ); return 0 ;
2026-10-03 09:15:02 +03:00
}
2026-10-04 17:00:39 +03:00
return ask ( l );
2026-10-03 09:15:02 +03:00
}
int main ( int argc , char ** argv ) {
2026-10-04 17:00:39 +03:00
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" );
2026-10-03 09:15:02 +03:00
if ( ! ( e = getenv ( "SOLO_TMPDIR" ))) { e = getenv ( "TMPDIR" ); e = e ? e : "/tmp" ; TMP = malloc ( strlen ( e ) + 8 ); sprintf ( TMP , "%s/solo" , e ); }
2026-10-04 06:06:06 +00:00
else TMP = strdup ( e );
if ( ! ( e = getenv ( "SOLO_ENDPOINT" ))) EP = strdup ( "https://api.kilo.ai/api/openrouter" ); else EP = strdup ( e );
2026-10-04 17:00:39 +03:00
for ( char * z = EP + strlen ( EP ); z > EP && z [ - 1 ] == '/' ; z -- ) * z = 0 ;
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TIMEOUT" )) && * e ) TO = atoi ( e );
2026-10-04 06:06:06 +00:00
if ( ! ( e = getenv ( "SOLO_MODEL" ))) MOD = strdup ( "kilo-auto/free" ); else MOD = strdup ( e );
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TEMP" )) && * e ) TEMP = atof ( e );
2026-10-04 06:06:06 +00:00
if ( ! ( e = getenv ( "SOLO_REASON" )) || !* e ) RSN = strdup ( "high" ); else RSN = strdup ( e );
2026-10-03 09:15:02 +03:00
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 );
create (); load ();
2026-10-04 06:06:06 +00:00
if (( e = getenv ( "SOLO_MODEL" )) && * e ) { setstr ( & MOD , e ); A ( SSN , "model" , S ( e )); }
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TEMP" )) && * e ) { TEMP = atof ( e ); A ( SSN , "temperature" , D ( TEMP )); }
2026-10-04 06:06:06 +00:00
if (( e = getenv ( "SOLO_REASON" )) && * e ) { setstr ( & RSN , e ); A ( SSN , "reasoning_effort" , S ( e )); }
2026-10-03 09:15:02 +03:00
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 ;
2026-10-04 17:00:39 +03:00
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 );
2026-10-03 09:15:02 +03:00
while ( ! QUIT ) {
2026-10-03 19:18:12 +03:00
snprintf ( pb , sizeof pb , " \001 %s \002 > \001 %s \002 " , CLR ? BLD : "" , CLR ? OF : "" );
2026-10-03 09:15:02 +03:00
if ( ! ( ln = readline ( pb ))) { puts ( "" ); break ; }
if ( IN ) { IN = 0 ; free ( ln ); continue ; }
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 );
2026-10-04 17:00:39 +03:00
( void ) dispatch ( ac ); free ( ac );
2026-10-03 09:15:02 +03:00
}
free ( EP ); free ( KEY ); free ( MOD ); free ( RSN ); free ( SF ); free ( TMP );
if ( SSN ) json_object_put ( SSN );
return 0 ;
}