Initial CST-Agent: portable Linux/Termux/Windows coding agent.

Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-09-07 22:42:32 +08:00
co-authored by Cursor
commit 1f1fcba7ad
75 changed files with 14940 additions and 0 deletions
+1182
View File
File diff suppressed because it is too large Load Diff
+189
View File
@@ -0,0 +1,189 @@
#define _DEFAULT_SOURCE
#include "cst_agent.h"
#include "os_compat.h"
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#ifdef _WIN32
#include <io.h>
#define OS_ACCESS _access
#define OS_F_OK 0
#else
#include <unistd.h>
#define OS_ACCESS access
#define OS_F_OK F_OK
#endif
int cst_agent_store_dir(const char *workspace, char *out, size_t out_sz) {
if (!workspace || !out || out_sz < 16)
return -1;
return os_path_join(out, out_sz, workspace, ".agent/sessions");
}
int cst_agent_store_ensure(const char *workspace) {
char base[OS_PATH_MAX];
char sess[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, sess, sizeof(sess)) != 0)
return -1;
if (os_path_join(base, sizeof(base), workspace, ".agent") != 0)
return -1;
if (os_mkdir(base, 0755) != 0)
return -1;
if (os_mkdir(sess, 0755) != 0)
return -1;
return 0;
}
static void make_id(char *out, size_t n) {
time_t now = time(NULL);
struct tm tm;
os_localtime(&now, &tm);
unsigned r = (unsigned)(os_getpid() ^ (unsigned)now);
snprintf(out, n, "%04d%02d%02d-%02d%02d%02d-%04x", tm.tm_year + 1900,
tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec,
r & 0xffff);
}
int cst_agent_store_new_id(char *id_out, size_t id_sz) {
if (!id_out || id_sz < 24)
return -1;
make_id(id_out, id_sz);
return 0;
}
int cst_agent_store_path_for_id(const char *workspace, const char *id,
char *out, size_t out_sz) {
if (!workspace || !id || !out)
return -1;
/* Accept bare id or full path. */
if (strchr(id, '/') || strchr(id, '\\')) {
snprintf(out, out_sz, "%s", id);
return 0;
}
char dir[OS_PATH_MAX];
char leaf[160];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return -1;
snprintf(leaf, sizeof(leaf), "%s.jsonl", id);
return os_path_join(out, out_sz, dir, leaf);
}
int cst_agent_store_set_current(const char *workspace, const char *id) {
if (cst_agent_store_ensure(workspace) != 0)
return -1;
char dir[OS_PATH_MAX];
char path[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return -1;
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
return -1;
FILE *fp = fopen(path, "w");
if (!fp)
return -1;
fprintf(fp, "%s\n", id);
fclose(fp);
return 0;
}
int cst_agent_store_get_current(const char *workspace, char *id_out,
size_t id_sz) {
char dir[OS_PATH_MAX];
char path[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return -1;
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
return -1;
FILE *fp = fopen(path, "r");
if (!fp)
return -1;
if (!fgets(id_out, (int)id_sz, fp)) {
fclose(fp);
return -1;
}
fclose(fp);
size_t n = strlen(id_out);
while (n > 0 && (id_out[n - 1] == '\n' || id_out[n - 1] == '\r'))
id_out[--n] = 0;
return id_out[0] ? 0 : -1;
}
int cst_agent_store_resolve_default(const char *workspace, char *path_out,
size_t path_sz, char *id_out,
size_t id_sz) {
if (cst_agent_store_ensure(workspace) != 0)
return -1;
char id[128];
if (cst_agent_store_get_current(workspace, id, sizeof(id)) == 0) {
char p[OS_PATH_MAX];
if (cst_agent_store_path_for_id(workspace, id, p, sizeof(p)) == 0 &&
OS_ACCESS(p, OS_F_OK) == 0) {
snprintf(path_out, path_sz, "%s", p);
if (id_out && id_sz)
snprintf(id_out, id_sz, "%s", id);
return 0;
}
}
if (cst_agent_store_new_id(id, sizeof(id)) != 0)
return -1;
if (cst_agent_store_path_for_id(workspace, id, path_out, path_sz) != 0)
return -1;
cst_agent_store_set_current(workspace, id);
if (id_out && id_sz)
snprintf(id_out, id_sz, "%s", id);
return 0;
}
int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
if (!workspace || !ids || max_ids <= 0)
return 0;
char dir[OS_PATH_MAX];
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
return 0;
DIR *d = opendir(dir);
if (!d)
return 0;
typedef struct {
char id[128];
time_t mtime;
} ent_t;
ent_t *ents = calloc((size_t)max_ids * 4, sizeof(ent_t));
int n = 0, cap = max_ids * 4;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
size_t len = strlen(de->d_name);
if (len < 7 || strcmp(de->d_name + len - 6, ".jsonl") != 0)
continue;
if (n >= cap)
break;
snprintf(ents[n].id, sizeof(ents[n].id), "%.*s", (int)(len - 6),
de->d_name);
char full[OS_PATH_MAX];
if (os_path_join(full, sizeof(full), dir, de->d_name) != 0)
continue;
struct stat st;
ents[n].mtime = (stat(full, &st) == 0) ? st.st_mtime : 0;
n++;
}
closedir(d);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (ents[j].mtime > ents[i].mtime) {
ent_t tmp = ents[i];
ents[i] = ents[j];
ents[j] = tmp;
}
}
}
int out_n = n < max_ids ? n : max_ids;
for (int i = 0; i < out_n; i++)
snprintf(ids[i], 128, "%s", ents[i].id);
free(ents);
return out_n;
}
+140
View File
@@ -0,0 +1,140 @@
#define _DEFAULT_SOURCE
#include "cst_agent.h"
#include "os_compat.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
/* Config dir via os_compat: ~/.config/cst-agent or %APPDATA%\cst-agent */
static int credentials_path(char *out, size_t n) {
char dir[OS_PATH_MAX];
if (os_config_dir(dir, sizeof(dir)) != 0)
return -1;
return os_path_join(out, n, dir, "credentials");
}
int cst_creds_load_api_key(char *out, size_t out_sz) {
if (!out || out_sz < 2)
return -1;
out[0] = 0;
char path[OS_PATH_MAX];
if (credentials_path(path, sizeof(path)) != 0)
return -1;
FILE *fp = fopen(path, "r");
if (!fp)
return -1;
char line[1024];
while (fgets(line, sizeof(line), fp)) {
if (strncmp(line, "api_key=", 8) != 0)
continue;
char *v = line + 8;
size_t len = strlen(v);
while (len > 0 && (v[len - 1] == '\n' || v[len - 1] == '\r'))
v[--len] = 0;
if (len == 0 || len + 1 > out_sz) {
fclose(fp);
return -1;
}
memcpy(out, v, len + 1);
fclose(fp);
return 0;
}
fclose(fp);
return -1;
}
int cst_creds_save_api_key(const char *key) {
if (!key || !key[0])
return -1;
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
if (os_config_dir(dir, sizeof(dir)) != 0)
return -1;
if (os_mkdir(dir, 0700) != 0)
return -1;
if (credentials_path(path, sizeof(path)) != 0)
return -1;
FILE *fp = fopen(path, "w");
if (!fp)
return -1;
fprintf(fp, "api_key=%s\n", key);
fprintf(fp, "api_base=https://api.minimax.cn/v1\n");
fprintf(fp, "model=MiniMax-M2.7\n");
fclose(fp);
(void)os_chmod_private(path);
return 0;
}
/* Zenity on Unix; stdin prompt elsewhere / when zenity missing. */
int cst_creds_prompt_api_key(char *out, size_t out_sz) {
if (!out || out_sz < 2)
return -1;
out[0] = 0;
#ifndef _WIN32
FILE *fp = popen(
"zenity --password --title='CST-Agent / MiniMax' "
"--text='请输入 MiniMax API Key(将保存到配置目录 credentials)' "
"2>/dev/null",
"r");
if (fp) {
if (fgets(out, (int)out_sz, fp)) {
int st = pclose(fp);
size_t len = strlen(out);
while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r'))
out[--len] = 0;
if (st == 0 && len > 0)
return 0;
} else {
pclose(fp);
}
out[0] = 0;
}
#endif
/* Fallback: console prompt (headless / Windows / no zenity). */
fprintf(stderr, "CST-Agent: enter MiniMax API Key (saved to config dir): ");
fflush(stderr);
if (!fgets(out, (int)out_sz, stdin)) {
out[0] = 0;
return -1;
}
size_t len = strlen(out);
while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r'))
out[--len] = 0;
if (len == 0) {
out[0] = 0;
return -1;
}
return 0;
}
int cst_creds_ensure_api_key(char *out, size_t out_sz) {
const char *env = getenv("CST_API_KEY");
if (!env || !env[0])
env = getenv("MINIMAX_API_KEY");
if (!env || !env[0])
env = getenv("OPENAI_API_KEY");
if (env && env[0]) {
snprintf(out, out_sz, "%s", env);
return 0;
}
if (cst_creds_load_api_key(out, out_sz) == 0 && out[0])
return 0;
if (cst_creds_prompt_api_key(out, out_sz) != 0)
return -1;
if (cst_creds_save_api_key(out) != 0)
fprintf(stderr, "warning: failed to save credentials file\n");
else
fprintf(stderr, "saved API key to config directory\n");
return 0;
}
+419
View File
@@ -0,0 +1,419 @@
#include "ctl.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef enum {
ST_TEXT = 0, /* outside a call; accumulate text lines */
ST_LINE, /* collecting a line (call/arg/end or text) */
ST_ARG_BODY, /* reading nbytes of arg body */
ST_ERROR
} ctl_state_t;
struct ctl_parser {
ctl_on_event_fn on_event;
void *userdata;
ctl_state_t state;
char line[CTL_MAX_LINE];
size_t line_len;
ctl_tool_call_t call;
int in_call;
/* current arg being filled */
char arg_key[CTL_MAX_KEY];
char *arg_buf;
size_t arg_cap;
size_t arg_got;
size_t arg_need;
size_t pending;
size_t pending_peak;
char errbuf[256];
};
static void bump_pending(ctl_parser_t *p, size_t n) {
p->pending = n;
if (n > p->pending_peak)
p->pending_peak = n;
}
static void emit(ctl_parser_t *p, ctl_event_t *ev) {
if (p->on_event)
p->on_event(ev, p->userdata);
}
static void fail(ctl_parser_t *p, const char *msg) {
snprintf(p->errbuf, sizeof(p->errbuf), "%s", msg);
p->state = ST_ERROR;
ctl_event_t ev = {0};
ev.type = CTL_EV_ERROR;
ev.error = p->errbuf;
emit(p, &ev);
}
/* free args in current call */
static void clear_call_args(ctl_tool_call_t *c) {
for (int i = 0; i < c->nargs; i++) {
free(c->args[i].value);
c->args[i].value = NULL;
c->args[i].value_len = 0;
}
c->nargs = 0;
c->id[0] = 0;
c->name[0] = 0;
}
static void reset_arg_body(ctl_parser_t *p) {
free(p->arg_buf);
p->arg_buf = NULL;
p->arg_cap = 0;
p->arg_got = 0;
p->arg_need = 0;
p->arg_key[0] = 0;
}
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata) {
ctl_parser_t *p = calloc(1, sizeof(*p));
if (!p)
return NULL;
p->on_event = on_event;
p->userdata = userdata;
p->state = ST_TEXT;
return p;
}
void ctl_parser_destroy(ctl_parser_t *p) {
if (!p)
return;
clear_call_args(&p->call);
reset_arg_body(p);
free(p);
}
void ctl_parser_reset(ctl_parser_t *p) {
if (!p)
return;
clear_call_args(&p->call);
reset_arg_body(p);
p->state = ST_TEXT;
p->line_len = 0;
p->in_call = 0;
p->pending = 0;
/* keep pending_peak for experiment */
}
size_t ctl_parser_pending(const ctl_parser_t *p) {
return p ? p->pending : 0;
}
size_t ctl_parser_pending_peak(const ctl_parser_t *p) {
return p ? p->pending_peak : 0;
}
static int starts_with(const char *s, const char *pfx) {
return strncmp(s, pfx, strlen(pfx)) == 0;
}
static int parse_call_line(ctl_parser_t *p, const char *line) {
/* call <id> <name> */
const char *s = line + 5;
while (*s == ' ' || *s == '\t')
s++;
if (!*s)
return -1;
const char *sp = strchr(s, ' ');
if (!sp)
return -1;
size_t idlen = (size_t)(sp - s);
if (idlen == 0 || idlen >= CTL_MAX_ID)
return -1;
while (*sp == ' ' || *sp == '\t')
sp++;
if (!*sp)
return -1;
size_t nlen = strlen(sp);
if (nlen == 0 || nlen >= CTL_MAX_NAME)
return -1;
for (size_t i = 0; i < nlen; i++) {
if (!(isalnum((unsigned char)sp[i]) || sp[i] == '_' || sp[i] == '-'))
return -1;
}
clear_call_args(&p->call);
memcpy(p->call.id, s, idlen);
p->call.id[idlen] = 0;
memcpy(p->call.name, sp, nlen);
p->call.name[nlen] = 0;
p->in_call = 1;
ctl_event_t ev = {0};
ev.type = CTL_EV_CALL_OPEN;
ev.call = &p->call;
emit(p, &ev);
return 0;
}
static int finish_arg(ctl_parser_t *p);
static int parse_arg_header(ctl_parser_t *p, const char *line) {
/* Forms:
* arg <key> <nbytes> → length-prefixed body follows
* arg <key> <value...> → value is rest of line (line-mode, confirmed now)
*/
const char *s = line + 4;
while (*s == ' ' || *s == '\t')
s++;
if (!*s)
return -1;
const char *sp = strchr(s, ' ');
if (!sp)
return -1;
size_t klen = (size_t)(sp - s);
if (klen == 0 || klen >= CTL_MAX_KEY)
return -1;
while (*sp == ' ' || *sp == '\t')
sp++;
if (!*sp)
return -1;
if (p->call.nargs >= CTL_MAX_ARGS)
return -1;
char *end = NULL;
unsigned long nbytes = strtoul(sp, &end, 10);
int length_prefixed = (end != sp && (*end == '\0' || *end == ' ' || *end == '\t'));
if (length_prefixed && end && *end != '\0') {
while (*end == ' ' || *end == '\t')
end++;
if (*end != '\0')
length_prefixed = 0; /* e.g. "9foo" or value starting with digits+text */
}
memcpy(p->arg_key, s, klen);
p->arg_key[klen] = 0;
if (!length_prefixed) {
/* line-mode: entire remainder is the value */
size_t vlen = strlen(sp);
if (vlen > CTL_MAX_ARG_BYTES)
return -1;
free(p->arg_buf);
p->arg_buf = malloc(vlen + 1);
if (!p->arg_buf)
return -2;
memcpy(p->arg_buf, sp, vlen);
p->arg_buf[vlen] = 0;
p->arg_need = vlen;
p->arg_got = vlen;
p->arg_cap = vlen;
finish_arg(p);
return 0;
}
if (nbytes > CTL_MAX_ARG_BYTES)
return -1;
p->arg_need = (size_t)nbytes;
p->arg_got = 0;
free(p->arg_buf);
p->arg_buf = NULL;
p->arg_cap = 0;
if (p->arg_need > 0) {
p->arg_buf = malloc(p->arg_need + 1);
if (!p->arg_buf)
return -2;
p->arg_cap = p->arg_need;
}
p->state = ST_ARG_BODY;
bump_pending(p, p->arg_need);
return 0;
}
static int finish_arg(ctl_parser_t *p) {
int i = p->call.nargs;
snprintf(p->call.args[i].key, CTL_MAX_KEY, "%s", p->arg_key);
p->call.args[i].value = p->arg_buf;
p->call.args[i].value_len = p->arg_got;
if (p->arg_buf)
p->arg_buf[p->arg_got] = 0;
p->arg_buf = NULL; /* ownership moved */
p->call.nargs++;
p->arg_need = 0;
p->arg_got = 0;
p->arg_key[0] = 0;
p->state = ST_LINE;
bump_pending(p, 0);
ctl_event_t ev = {0};
ev.type = CTL_EV_ARG;
ev.call = &p->call;
emit(p, &ev);
return 0;
}
static void emit_text_line(ctl_parser_t *p, const char *line, size_t len) {
/* include newline as confirmed text */
char tmp[CTL_MAX_LINE + 1];
if (len + 1 >= sizeof(tmp))
len = sizeof(tmp) - 2;
memcpy(tmp, line, len);
tmp[len] = '\n';
tmp[len + 1] = 0;
ctl_event_t ev = {0};
ev.type = CTL_EV_TEXT;
ev.text = tmp;
ev.text_len = len + 1;
emit(p, &ev);
}
static int is_fence_line(const char *line) {
if (strncmp(line, "```", 3) == 0)
return 1;
if (strncmp(line, "~~~", 3) == 0)
return 1;
return 0;
}
static int handle_line(ctl_parser_t *p, const char *line) {
/* Ignore markdown fences so models can wrap CTL in code blocks. */
if (is_fence_line(line)) {
bump_pending(p, 0);
return 0;
}
/* Ignore blank lines between CTL frames. */
if (line[0] == '\0') {
bump_pending(p, 0);
return 0;
}
if (!p->in_call) {
if (starts_with(line, "call ")) {
if (parse_call_line(p, line) != 0) {
fail(p, "invalid call line");
return -1;
}
bump_pending(p, 0);
return 0;
}
/* confirmed text line */
emit_text_line(p, line, strlen(line));
bump_pending(p, 0);
return 0;
}
/* in call */
if (starts_with(line, "arg ")) {
int rc = parse_arg_header(p, line);
if (rc == -2) {
fail(p, "oom");
return -1;
}
if (rc != 0) {
fail(p, "invalid arg header");
return -1;
}
if (p->arg_need == 0)
finish_arg(p);
return 0;
}
if (strcmp(line, "end") == 0) {
ctl_event_t ev = {0};
ev.type = CTL_EV_CALL_READY;
ev.call = &p->call;
emit(p, &ev);
clear_call_args(&p->call);
p->in_call = 0;
bump_pending(p, 0);
p->state = ST_LINE;
return 0;
}
fail(p, "unexpected line inside call");
return -1;
}
ctl_status_t ctl_parser_feed(ctl_parser_t *p, const char *data, size_t len) {
if (!p)
return CTL_ERR_CONTRACT;
if (p->state == ST_ERROR)
return CTL_ERR_CONTRACT;
size_t i = 0;
while (i < len) {
if (p->state == ST_ARG_BODY) {
size_t left = p->arg_need - p->arg_got;
size_t take = len - i;
if (take > left)
take = left;
if (take > 0) {
memcpy(p->arg_buf + p->arg_got, data + i, take);
p->arg_got += take;
i += take;
bump_pending(p, p->arg_need - p->arg_got);
}
if (p->arg_got == p->arg_need) {
finish_arg(p);
/* continue with remaining bytes in LINE mode */
}
continue;
}
/* line accumulation */
p->state = ST_LINE;
unsigned char c = (unsigned char)data[i++];
if (c == '\r')
continue; /* normalize CRLF */
if (c == '\n') {
p->line[p->line_len] = 0;
if (handle_line(p, p->line) != 0)
return CTL_ERR_CONTRACT;
p->line_len = 0;
if (p->state == ST_ARG_BODY && p->arg_need == 0) {
/* already finished empty arg */
}
continue;
}
if (p->line_len + 1 >= CTL_MAX_LINE) {
fail(p, "line too long");
return CTL_ERR_LIMIT;
}
p->line[p->line_len++] = (char)c;
bump_pending(p, p->line_len);
}
return CTL_OK;
}
ctl_status_t ctl_parser_finish(ctl_parser_t *p) {
if (!p)
return CTL_ERR_CONTRACT;
if (p->state == ST_ERROR)
return CTL_ERR_CONTRACT;
/* Mid-argument body cannot be confirmed at EOF. */
if (p->state == ST_ARG_BODY) {
fail(p, "incomplete arg body at EOF");
return CTL_ERR_CONTRACT;
}
/* Trailing text without final newline is still confirmed text. */
if (!p->in_call && p->line_len > 0) {
p->line[p->line_len] = 0;
emit_text_line(p, p->line, p->line_len);
p->line_len = 0;
}
/* EOF confirms an open call whose args are complete (implicit end).
* Matches paper EOF adequacy: stream end closes the frontier. */
if (p->in_call) {
if (p->line_len > 0) {
p->line[p->line_len] = 0;
/* leftover line must be end or fence/blank */
if (strcmp(p->line, "end") == 0 || is_fence_line(p->line) ||
p->line[0] == '\0') {
p->line_len = 0;
} else {
fail(p, "unexpected trailing line in call at EOF");
return CTL_ERR_CONTRACT;
}
}
ctl_event_t ev = {0};
ev.type = CTL_EV_CALL_READY;
ev.call = &p->call;
emit(p, &ev);
clear_call_args(&p->call);
p->in_call = 0;
}
bump_pending(p, 0);
return CTL_OK;
}
+145
View File
@@ -0,0 +1,145 @@
#include "cst_agent.h"
#include <stdio.h>
#include <string.h>
/* Minimal JSON tool object: {"name":"...","arguments":{...}}
* Buffer-until-valid baseline (NOT bounded CST). */
static const char *skip_ws(const char *p, const char *end) {
while (p < end && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t'))
p++;
return p;
}
/* Find matching closing brace/bracket starting at open pos. Returns end ptr or
* NULL if incomplete. Handles strings/escapes. */
static const char *match_obj(const char *start, const char *end) {
if (start >= end || *start != '{')
return NULL;
int depth = 0;
int in_str = 0;
for (const char *p = start; p < end; p++) {
if (in_str) {
if (*p == '\\' && p + 1 < end) {
p++;
continue;
}
if (*p == '"')
in_str = 0;
continue;
}
if (*p == '"') {
in_str = 1;
continue;
}
if (*p == '{')
depth++;
else if (*p == '}') {
depth--;
if (depth == 0)
return p + 1;
}
}
return NULL; /* incomplete */
}
static int extract_string(const char *obj, const char *end, const char *key,
char *out, size_t out_sz) {
char pat[128];
snprintf(pat, sizeof(pat), "\"%s\"", key);
const char *k = obj;
while (k && k < end) {
k = strstr(k, pat);
if (!k || k >= end)
return -1;
/* ensure key boundary */
const char *colon = k + strlen(pat);
colon = skip_ws(colon, end);
if (colon >= end || *colon != ':') {
k++;
continue;
}
colon++;
colon = skip_ws(colon, end);
if (colon >= end || *colon != '"')
return -1;
colon++;
size_t o = 0;
for (const char *p = colon; p < end && o + 1 < out_sz; p++) {
if (*p == '\\' && p + 1 < end) {
p++;
if (*p == 'n')
out[o++] = '\n';
else
out[o++] = *p;
continue;
}
if (*p == '"') {
out[o] = 0;
return 0;
}
out[o++] = *p;
}
return -1;
}
return -1;
}
int cst_json_try_parse_tool(const char *buf, size_t len, cst_json_tool_t *out) {
if (!buf || !out)
return 0;
memset(out, 0, sizeof(*out));
const char *end = buf + len;
const char *p = buf;
while (p < end) {
p = skip_ws(p, end);
if (p >= end)
break;
if (*p != '{') {
p++;
continue;
}
const char *obj_end = match_obj(p, end);
if (!obj_end)
return 0; /* incomplete JSON — starvation failure mode */
if (extract_string(p, obj_end, "name", out->name, sizeof(out->name)) !=
0) {
p++;
continue;
}
/* arguments: object as raw substring */
const char *akey = strstr(p, "\"arguments\"");
if (!akey || akey >= obj_end) {
p = obj_end;
continue;
}
const char *colon = strchr(akey + 11, ':');
if (!colon || colon >= obj_end)
return 0;
colon++;
colon = skip_ws(colon, obj_end);
if (colon >= obj_end)
return 0;
if (*colon == '{') {
const char *ae = match_obj(colon, obj_end);
if (!ae)
return 0;
size_t n = (size_t)(ae - colon);
if (n >= sizeof(out->arguments_json))
return 0;
memcpy(out->arguments_json, colon, n);
out->arguments_json[n] = 0;
} else if (*colon == '"') {
if (extract_string(akey, obj_end, "arguments", out->arguments_json,
sizeof(out->arguments_json)) != 0)
return 0;
} else {
p = obj_end;
continue;
}
out->ok = 1;
return 1;
}
return 0;
}
+786
View File
@@ -0,0 +1,786 @@
#include "cst_agent.h"
#include <ctype.h>
#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TC_SLOTS 8
#define TC_ID_MAX 128
#define TC_NAME_MAX 128
#define TC_ARGS_MAX 65536
typedef struct {
int used;
int index;
char id[TC_ID_MAX];
char name[TC_NAME_MAX];
char *args;
size_t args_len;
size_t args_cap;
} tc_slot_t;
typedef struct {
cst_llm_on_delta_fn on_delta;
cst_llm_on_native_tool_fn on_native_tool;
void *userdata;
char *buf;
size_t len;
size_t cap;
int in_data;
int in_reason; /* wrapping reasoning_content as <think> for the agent */
tc_slot_t slots[TC_SLOTS];
} sse_acc_t;
static void emit_raw(sse_acc_t *a, const char *s, size_t n) {
if (a && a->on_delta && s && n)
a->on_delta(s, n, a->userdata);
}
static const char *find_json_string_key(const char *payload, const char *key) {
/* Find "key" that is a full JSON key, not a suffix of another key
* (e.g. avoid matching content inside audio_content / reasoning_content). */
size_t klen = strlen(key);
const char *p = payload;
while ((p = strstr(p, key)) != NULL) {
if (p > payload) {
unsigned char prev = (unsigned char)p[-1];
if (prev == '_' || isalnum(prev)) {
p += klen;
continue;
}
}
return p;
}
return NULL;
}
/* Decode one JSON string value starting after the opening quote into out. */
static size_t decode_json_string(const char *colon_quote, char *out,
size_t out_sz) {
size_t o = 0;
for (const char *p = colon_quote; *p && o + 4 < out_sz; p++) {
if (*p == '\\' && p[1]) {
p++;
if (*p == 'n')
out[o++] = '\n';
else if (*p == 'r')
out[o++] = '\r';
else if (*p == 't')
out[o++] = '\t';
else if (*p == '"' || *p == '\\' || *p == '/')
out[o++] = *p;
else if (*p == 'u' && p[1] && p[2] && p[3] && p[4]) {
unsigned cp = 0;
int ok = 1;
for (int i = 1; i <= 4; i++) {
char h = p[i];
cp <<= 4;
if (h >= '0' && h <= '9')
cp |= (unsigned)(h - '0');
else if (h >= 'a' && h <= 'f')
cp |= (unsigned)(h - 'a' + 10);
else if (h >= 'A' && h <= 'F')
cp |= (unsigned)(h - 'A' + 10);
else {
ok = 0;
break;
}
}
p += 4;
if (!ok) {
out[o++] = '?';
continue;
}
if (cp < 0x80)
out[o++] = (char)cp;
else if (cp < 0x800) {
out[o++] = (char)(0xC0 | (cp >> 6));
out[o++] = (char)(0x80 | (cp & 0x3F));
} else {
out[o++] = (char)(0xE0 | (cp >> 12));
out[o++] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[o++] = (char)(0x80 | (cp & 0x3F));
}
} else {
out[o++] = *p;
}
continue;
}
if (*p == '"')
break;
out[o++] = *p;
}
return o;
}
static int extract_json_string_value(const char *payload, const char *key,
char *out, size_t out_sz, size_t *out_len) {
const char *keyp = find_json_string_key(payload, key);
if (!keyp)
return 0;
const char *colon = strchr(keyp + strlen(key), ':');
if (!colon)
return 0;
colon++;
while (*colon == ' ')
colon++;
if (*colon == 'n' && strncmp(colon, "null", 4) == 0)
return 0;
if (*colon != '"')
return 0;
if (colon[1] == '"')
return 0; /* empty */
size_t n = decode_json_string(colon + 1, out, out_sz);
if (n == 0)
return 0;
*out_len = n;
return 1;
}
static int json_brace_balanced(const char *s) {
if (!s || *s != '{')
return 0;
int depth = 0;
int in_str = 0;
for (const char *p = s; *p; p++) {
if (in_str) {
if (*p == '\\' && p[1]) {
p++;
continue;
}
if (*p == '"')
in_str = 0;
continue;
}
if (*p == '"') {
in_str = 1;
continue;
}
if (*p == '{')
depth++;
else if (*p == '}') {
depth--;
if (depth == 0)
return p[1] == 0;
if (depth < 0)
return 0;
}
}
return 0;
}
static tc_slot_t *slot_for_index(sse_acc_t *a, int index) {
for (int i = 0; i < TC_SLOTS; i++) {
if (a->slots[i].used && a->slots[i].index == index)
return &a->slots[i];
}
for (int i = 0; i < TC_SLOTS; i++) {
if (!a->slots[i].used) {
a->slots[i].used = 1;
a->slots[i].index = index;
a->slots[i].id[0] = 0;
a->slots[i].name[0] = 0;
a->slots[i].args = NULL;
a->slots[i].args_len = 0;
a->slots[i].args_cap = 0;
return &a->slots[i];
}
}
return NULL;
}
static int slot_append_args(tc_slot_t *s, const char *frag, size_t n) {
if (!s || !frag || !n)
return 0;
if (s->args_len + n + 1 > s->args_cap) {
size_t nc = s->args_cap ? s->args_cap * 2 : 1024;
while (nc < s->args_len + n + 1)
nc *= 2;
if (nc > TC_ARGS_MAX)
nc = TC_ARGS_MAX;
if (s->args_len + n + 1 > nc)
return -1;
char *nb = realloc(s->args, nc);
if (!nb)
return -1;
s->args = nb;
s->args_cap = nc;
}
memcpy(s->args + s->args_len, frag, n);
s->args_len += n;
s->args[s->args_len] = 0;
return 0;
}
/* Parse one tool_calls[] element object for index/id/name/arguments fragments. */
static void parse_one_tool_call_obj(sse_acc_t *a, const char *obj) {
if (!obj || *obj != '{')
return;
int index = 0;
const char *ip = strstr(obj, "\"index\"");
if (ip) {
const char *colon = strchr(ip, ':');
if (colon) {
colon++;
while (*colon == ' ')
colon++;
index = (int)strtol(colon, NULL, 10);
}
}
tc_slot_t *slot = slot_for_index(a, index);
if (!slot)
return;
char tmp[4096];
size_t tlen = 0;
if (extract_json_string_value(obj, "\"id\"", tmp, sizeof(tmp), &tlen)) {
if (tlen >= TC_ID_MAX)
tlen = TC_ID_MAX - 1;
memcpy(slot->id, tmp, tlen);
slot->id[tlen] = 0;
}
/* function.name / function.arguments may be nested under "function":{...} */
const char *fn = strstr(obj, "\"function\"");
const char *scope = fn ? fn : obj;
tlen = 0;
if (extract_json_string_value(scope, "\"name\"", tmp, sizeof(tmp), &tlen)) {
if (tlen >= TC_NAME_MAX)
tlen = TC_NAME_MAX - 1;
memcpy(slot->name, tmp, tlen);
slot->name[tlen] = 0;
}
tlen = 0;
if (extract_json_string_value(scope, "\"arguments\"", tmp, sizeof(tmp),
&tlen)) {
slot_append_args(slot, tmp, tlen);
}
}
static void parse_tool_calls(sse_acc_t *a, const char *payload) {
const char *key = find_json_string_key(payload, "\"tool_calls\"");
if (!key)
return;
const char *p = strchr(key, '[');
if (!p)
return;
p++;
while (*p) {
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',')
p++;
if (*p == ']')
break;
if (*p != '{')
break;
/* find matching close brace for this object */
int depth = 0;
int in_str = 0;
const char *start = p;
const char *end = NULL;
for (; *p; p++) {
if (in_str) {
if (*p == '\\' && p[1]) {
p++;
continue;
}
if (*p == '"')
in_str = 0;
continue;
}
if (*p == '"') {
in_str = 1;
continue;
}
if (*p == '{')
depth++;
else if (*p == '}') {
depth--;
if (depth == 0) {
end = p;
p++;
break;
}
}
}
if (!end)
break;
size_t n = (size_t)(end - start + 1);
if (n >= 8192)
n = 8191;
char obj[8192];
memcpy(obj, start, n);
obj[n] = 0;
parse_one_tool_call_obj(a, obj);
}
}
static void flush_native_tools(sse_acc_t *a) {
if (!a->on_native_tool)
return;
for (int i = 0; i < TC_SLOTS; i++) {
tc_slot_t *s = &a->slots[i];
if (!s->used || !s->name[0] || !s->args || !s->args_len)
continue;
if (!json_brace_balanced(s->args))
continue;
const char *id = s->id[0] ? s->id : "";
a->on_native_tool(id, s->name, s->args, a->userdata);
/* Mark consumed so end-of-stream flush is idempotent after [DONE]. */
free(s->args);
s->args = NULL;
s->args_len = 0;
s->used = 0;
}
}
static void free_slots(sse_acc_t *a) {
for (int i = 0; i < TC_SLOTS; i++) {
free(a->slots[i].args);
a->slots[i].args = NULL;
a->slots[i].used = 0;
}
}
static void emit_data_payload(sse_acc_t *a, const char *payload) {
/* OpenAI SSE: data: {...} or data: [DONE] */
if (strncmp(payload, "[DONE]", 6) == 0) {
if (a->in_reason) {
emit_raw(a, "</think>", 8);
a->in_reason = 0;
}
flush_native_tools(a);
return;
}
char out[8192];
size_t olen = 0;
/* MiniMax reasoning_split: route reasoning_content through <think> so the
* agent UI can show it, without polluting the final answer. */
if (extract_json_string_value(payload, "\"reasoning_content\"", out,
sizeof(out), &olen)) {
if (!a->in_reason) {
emit_raw(a, "<think>", 7);
a->in_reason = 1;
}
emit_raw(a, out, olen);
}
olen = 0;
if (extract_json_string_value(payload, "\"content\"", out, sizeof(out),
&olen)) {
if (a->in_reason) {
emit_raw(a, "</think>", 8);
a->in_reason = 0;
}
emit_raw(a, out, olen);
}
parse_tool_calls(a, payload);
}
static void process_sse_chunk(sse_acc_t *a, const char *data, size_t n) {
for (size_t i = 0; i < n; i++) {
if (a->len + 1 >= a->cap) {
size_t nc = a->cap ? a->cap * 2 : 4096;
char *nb = realloc(a->buf, nc);
if (!nb)
return;
a->buf = nb;
a->cap = nc;
}
a->buf[a->len++] = data[i];
if (data[i] == '\n') {
a->buf[a->len] = 0;
/* trim CR */
if (a->len >= 2 && a->buf[a->len - 2] == '\r')
a->buf[a->len - 2] = 0;
else
a->buf[a->len - 1] = 0;
if (strncmp(a->buf, "data:", 5) == 0) {
const char *p = a->buf + 5;
while (*p == ' ')
p++;
emit_data_payload(a, p);
}
a->len = 0;
}
}
}
static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
sse_acc_t *a = userdata;
size_t n = size * nmemb;
process_sse_chunk(a, ptr, n);
return n;
}
static int xferinfo_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
curl_off_t ultotal, curl_off_t ulnow) {
(void)dltotal;
(void)dlnow;
(void)ultotal;
(void)ulnow;
volatile int *flag = (volatile int *)clientp;
if (flag && *flag != 0)
return 1; /* abort */
return 0;
}
static int json_escape_append(char **buf, size_t *len, size_t *cap,
const char *s) {
if (!buf || !len || !cap)
return -1;
if (!*buf || *cap == 0) {
*cap = 4096;
*buf = malloc(*cap);
if (!*buf)
return -1;
*len = 0;
(*buf)[0] = 0;
}
if (!s)
s = "";
for (; *s; s++) {
char tmp[8];
const char *piece = tmp;
size_t pn = 1;
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\') {
tmp[0] = '\\';
tmp[1] = (char)c;
pn = 2;
} else if (c == '\n') {
piece = "\\n";
pn = 2;
} else if (c == '\r') {
piece = "\\r";
pn = 2;
} else if (c == '\t') {
piece = "\\t";
pn = 2;
} else if (c < 0x20) {
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
pn = 6;
} else {
tmp[0] = (char)c;
pn = 1;
}
if (*len + pn + 1 > *cap) {
size_t nc = *cap ? *cap * 2 : 4096;
while (nc < *len + pn + 1)
nc *= 2;
char *nb = realloc(*buf, nc);
if (!nb)
return -1;
*buf = nb;
*cap = nc;
}
memcpy(*buf + *len, piece, pn);
*len += pn;
}
(*buf)[*len] = 0;
return 0;
}
static int body_ensure(char **body, size_t *blen, size_t *bcap, size_t need) {
if (*blen + need + 1 <= *bcap)
return 0;
size_t nc = *bcap ? *bcap * 2 : 4096;
while (nc < *blen + need + 1)
nc *= 2;
char *nb = realloc(*body, nc);
if (!nb)
return -1;
*body = nb;
*bcap = nc;
return 0;
}
static int body_append_raw(char **body, size_t *blen, size_t *bcap,
const char *s) {
size_t n = strlen(s);
if (body_ensure(body, blen, bcap, n) != 0)
return -1;
memcpy(*body + *blen, s, n);
*blen += n;
(*body)[*blen] = 0;
return 0;
}
static const char *role_api(cst_role_t r) {
switch (r) {
case CST_ROLE_SYSTEM:
return "system";
case CST_ROLE_USER:
return "user";
case CST_ROLE_ASSISTANT:
return "assistant";
case CST_ROLE_TOOL:
return "tool";
}
return "user";
}
static const char *TOOLS_JSON =
",\"tools\":["
"{\"type\":\"function\",\"function\":{\"name\":\"read_file\","
"\"description\":\"Read a text file under the workspace\","
"\"parameters\":{\"type\":\"object\",\"properties\":{"
"\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}},"
"{\"type\":\"function\",\"function\":{\"name\":\"run_shell\","
"\"description\":\"Run a shell command in the workspace root\","
"\"parameters\":{\"type\":\"object\",\"properties\":{"
"\"cmd\":{\"type\":\"string\"}},\"required\":[\"cmd\"]}}},"
"{\"type\":\"function\",\"function\":{\"name\":\"write_file\","
"\"description\":\"Write full file contents under the workspace\","
"\"parameters\":{\"type\":\"object\",\"properties\":{"
"\"path\":{\"type\":\"string\"},\"content\":{\"type\":\"string\"}},"
"\"required\":[\"path\",\"content\"]}}},"
"{\"type\":\"function\",\"function\":{\"name\":\"apply_patch\","
"\"description\":\"Apply a unified diff or Update File patch\","
"\"parameters\":{\"type\":\"object\",\"properties\":{"
"\"patch\":{\"type\":\"string\"}},\"required\":[\"patch\"]}}}"
"]";
int cst_llm_chat_stream(const cst_llm_request_t *req, char *err, size_t err_sz) {
if (err && err_sz)
err[0] = 0;
if (!req || !req->api_base || !req->model) {
if (err)
snprintf(err, err_sz, "bad request");
return -1;
}
char *body = NULL;
size_t blen = 0;
size_t bcap = 8192;
body = malloc(bcap);
if (!body)
return -1;
/* MiniMax: keep think+answer in content (no reasoning_split). Split mode
* often leaves content empty while the useful text is only in
* reasoning_content, which made the agent exit with blank turns. */
blen = (size_t)snprintf(body, bcap,
"{\"model\":\"%s\",\"stream\":true,\"messages\":[",
req->model);
for (int i = 0; i < req->nmsgs; i++) {
const cst_message_t *m = &req->msgs[i];
char prefix[256];
int first = i ? 0 : 1;
if (m->role == CST_ROLE_ASSISTANT && m->tool_calls_json &&
m->tool_calls_json[0]) {
/* OpenCode/OpenAI: tool-only assistant uses content:null */
int empty =
!m->content || !m->content[0] ||
(m->content[0] == '\0');
if (empty) {
snprintf(prefix, sizeof(prefix),
"%s{\"role\":\"assistant\",\"content\":null,\"tool_calls\":",
first ? "" : ",");
if (body_append_raw(&body, &blen, &bcap, prefix) != 0 ||
body_append_raw(&body, &blen, &bcap, m->tool_calls_json) != 0 ||
body_append_raw(&body, &blen, &bcap, "}") != 0) {
free(body);
return -1;
}
} else {
snprintf(prefix, sizeof(prefix),
"%s{\"role\":\"assistant\",\"content\":\"", first ? "" : ",");
size_t pl = strlen(prefix);
if (blen + pl + 8 > bcap) {
bcap = (blen + pl) * 2 + 1024;
body = realloc(body, bcap);
if (!body)
return -1;
}
memcpy(body + blen, prefix, pl);
blen += pl;
body[blen] = 0;
if (json_escape_append(&body, &blen, &bcap, m->content) != 0) {
free(body);
return -1;
}
if (body_append_raw(&body, &blen, &bcap, "\",\"tool_calls\":") != 0 ||
body_append_raw(&body, &blen, &bcap, m->tool_calls_json) != 0 ||
body_append_raw(&body, &blen, &bcap, "}") != 0) {
free(body);
return -1;
}
}
continue;
}
if (m->role == CST_ROLE_TOOL && m->tool_call_id[0]) {
/* Include name when known (OpenAI-compatible gateways accept it). */
if (m->tool_name[0]) {
snprintf(prefix, sizeof(prefix),
"%s{\"role\":\"%s\",\"tool_call_id\":\"%s\",\"name\":\"%s\","
"\"content\":\"",
first ? "" : ",", role_api(m->role), m->tool_call_id,
m->tool_name);
} else {
snprintf(prefix, sizeof(prefix),
"%s{\"role\":\"%s\",\"tool_call_id\":\"%s\",\"content\":\"",
first ? "" : ",", role_api(m->role), m->tool_call_id);
}
} else {
snprintf(prefix, sizeof(prefix), "%s{\"role\":\"%s\",\"content\":\"",
first ? "" : ",", role_api(m->role));
}
size_t pl = strlen(prefix);
if (blen + pl + 8 > bcap) {
bcap = (blen + pl) * 2 + 1024;
body = realloc(body, bcap);
if (!body)
return -1;
}
memcpy(body + blen, prefix, pl);
blen += pl;
body[blen] = 0;
/* Keep full tool output in session/TUI; shrink only the wire payload so
* large write diffs don't blow MiniMax/JSON limits. */
{
const char *raw = m->content ? m->content : "";
char *scratch = NULL;
const char *send = raw;
enum { API_TOOL_CAP = 3500 };
if (m->role == CST_ROLE_TOOL && strlen(raw) > API_TOOL_CAP) {
scratch = malloc(API_TOOL_CAP + 80);
if (scratch) {
snprintf(scratch, API_TOOL_CAP + 80,
"%.*s\n…(truncated for model; full diff in UI/session)",
API_TOOL_CAP, raw);
send = scratch;
}
}
if (json_escape_append(&body, &blen, &bcap, send) != 0) {
free(scratch);
free(body);
return -1;
}
free(scratch);
}
if (blen + 3 > bcap) {
bcap *= 2;
body = realloc(body, bcap);
if (!body)
return -1;
}
body[blen++] = '"';
body[blen++] = '}';
body[blen] = 0;
}
if (body_append_raw(&body, &blen, &bcap, "]") != 0) {
free(body);
return -1;
}
if (req->enable_tools) {
if (body_append_raw(&body, &blen, &bcap, TOOLS_JSON) != 0) {
free(body);
return -1;
}
if (req->tool_choice_none) {
if (body_append_raw(&body, &blen, &bcap, ",\"tool_choice\":\"none\"") !=
0) {
free(body);
return -1;
}
} else {
if (body_append_raw(&body, &blen, &bcap, ",\"tool_choice\":\"auto\"") !=
0) {
free(body);
return -1;
}
}
}
if (body_append_raw(&body, &blen, &bcap, "}") != 0) {
free(body);
return -1;
}
char url[1024];
snprintf(url, sizeof(url), "%s/chat/completions", req->api_base);
CURL *curl = curl_easy_init();
if (!curl) {
free(body);
if (err)
snprintf(err, err_sz, "curl init failed");
return -1;
}
sse_acc_t acc = {0};
acc.on_delta = req->on_delta;
acc.on_native_tool = req->on_native_tool;
acc.userdata = req->userdata;
struct curl_slist *hdrs = NULL;
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
char auth[600];
if (req->api_key && req->api_key[0]) {
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", req->api_key);
hdrs = curl_slist_append(hdrs, auth);
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &acc);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L);
if (req->cancel_flag) {
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferinfo_cb);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, (void *)req->cancel_flag);
}
CURLcode rc = curl_easy_perform(curl);
long http = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http);
if (acc.in_reason) {
emit_raw(&acc, "</think>", 8);
acc.in_reason = 0;
}
/* Stream ended without [DONE] — still flush completed native tools. */
if (rc == CURLE_OK)
flush_native_tools(&acc);
curl_slist_free_all(hdrs);
curl_easy_cleanup(curl);
if (rc != CURLE_OK) {
if (err) {
if (rc == CURLE_ABORTED_BY_CALLBACK)
snprintf(err, err_sz, "cancelled");
else
snprintf(err, err_sz, "curl: %s", curl_easy_strerror(rc));
}
free_slots(&acc);
free(acc.buf);
free(body);
return -1;
}
if (http >= 400) {
if (err) {
/* show a snippet of body if present */
if (acc.buf && acc.len) {
acc.buf[acc.len < 200 ? acc.len : 200] = 0;
snprintf(err, err_sz, "http %ld: %.180s", http, acc.buf);
} else {
snprintf(err, err_sz, "http %ld", http);
}
}
free_slots(&acc);
free(acc.buf);
free(body);
return -1;
}
free_slots(&acc);
free(acc.buf);
free(body);
return 0;
}
+283
View File
@@ -0,0 +1,283 @@
#include "cst_agent.h"
#include "os_compat.h"
#include <curl/curl.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <direct.h>
#else
#include <unistd.h>
#endif
static void usage(const char *argv0) {
fprintf(stderr,
"Usage: %s [--workspace DIR] [--session FILE] [--api-base URL]\n"
" [--api-key KEY] [--model NAME] [--headless]\n"
" [--prompt TEXT] [--demo-ctl] [--ask-key]\n"
"\n"
"Defaults: api-base=https://api.minimax.cn/v1 model=MiniMax-M2.7\n"
"Session: <workspace>/.agent/sessions/<id>.jsonl (CURRENT tracked)\n"
"API key: CST_API_KEY / OPENAI_API_KEY / MINIMAX_API_KEY\n"
"Base: CST_API_BASE / OPENAI_BASE_URL (OpenAI-compatible …/v1)\n"
"Model: CST_MODEL / OPENAI_MODEL\n"
"Also: CST_MODELS, CST_AUTO_APPROVE=1, CST_SHELL, CST_EXE_DIR\n"
"TUI: /new /session /model /compact /details /help\n"
" Enter send · Ctrl+N newline · ↑↓ history · Esc cancel\n"
" busy Enter queues · tool perm y/n/a=always · wheel scroll\n",
argv0);
}
static int g_ready;
static ctl_tool_call_t g_call;
static void demo_on_ev(const ctl_event_t *ev, void *ud) {
(void)ud;
if (ev->type == CTL_EV_TEXT)
fwrite(ev->text, 1, ev->text_len, stdout);
if (ev->type == CTL_EV_CALL_READY) {
snprintf(g_call.id, sizeof(g_call.id), "%s", ev->call->id);
snprintf(g_call.name, sizeof(g_call.name), "%s", ev->call->name);
for (int i = 0; i < g_call.nargs; i++) {
free(g_call.args[i].value);
g_call.args[i].value = NULL;
}
g_call.nargs = 0;
for (int i = 0; i < ev->call->nargs; i++) {
snprintf(g_call.args[i].key, sizeof(g_call.args[i].key), "%s",
ev->call->args[i].key);
g_call.args[i].value_len = ev->call->args[i].value_len;
g_call.args[i].value = malloc(ev->call->args[i].value_len + 1);
memcpy(g_call.args[i].value, ev->call->args[i].value,
ev->call->args[i].value_len);
g_call.args[i].value[ev->call->args[i].value_len] = 0;
g_call.nargs++;
}
g_ready = 1;
}
}
static int demo_ctl_impl(const char *workspace) {
printf("=== demo CTL (byte-at-a-time feed) ===\n");
g_ready = 0;
memset(&g_call, 0, sizeof(g_call));
ctl_parser_t *p = ctl_parser_create(demo_on_ev, NULL);
const char *stream =
"Reading README.\n"
"call t1 read_file\n"
"arg path 9\n"
"README.md"
"end\n";
for (size_t i = 0; stream[i]; i++)
ctl_parser_feed(p, &stream[i], 1);
if (ctl_parser_finish(p) != CTL_OK || !g_ready) {
fprintf(stderr, "demo: CTL failed\n");
ctl_parser_destroy(p);
return 1;
}
printf("\npending_peak=%zu\n", ctl_parser_pending_peak(p));
ctl_parser_destroy(p);
cst_tool_result_t r = {0};
cst_tool_dispatch(workspace, &g_call, &r);
printf("[tool result exit=%d]\n%.*s\n", r.exit_code, (int)r.out_len,
r.out ? r.out : "");
cst_tool_result_free(&r);
for (int i = 0; i < g_call.nargs; i++)
free(g_call.args[i].value);
return 0;
}
int main(int argc, char **argv) {
os_set_prog_path(argv[0]);
curl_global_init(CURL_GLOBAL_DEFAULT);
cst_agent_config_t cfg = {0};
char cwd[4096];
#ifdef _WIN32
if (!_getcwd(cwd, (int)sizeof(cwd)))
#else
if (!getcwd(cwd, sizeof(cwd)))
#endif
snprintf(cwd, sizeof(cwd), ".");
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", cwd);
cfg.session_path[0] = 0; /* filled after workspace opts via .agent/store */
/* OpenAI-compatible base URL aliases */
const char *env_base = getenv("CST_API_BASE");
if (!env_base || !env_base[0])
env_base = getenv("OPENAI_BASE_URL");
if (!env_base || !env_base[0])
env_base = getenv("OPENAI_API_BASE");
const char *env_model = getenv("CST_MODEL");
if (!env_model || !env_model[0])
env_model = getenv("OPENAI_MODEL");
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s",
env_base && env_base[0] ? env_base : "https://api.minimax.cn/v1");
cfg.api_key[0] = 0;
snprintf(cfg.model, sizeof(cfg.model), "%s",
env_model && env_model[0] ? env_model : "MiniMax-M2.7");
cfg.headless = 0;
cfg.confirm_shell = 1;
cfg.confirm_write = 1;
cfg.auto_approve = 0;
if (getenv("CST_AUTO_APPROVE"))
cfg.auto_approve = 1;
int do_demo = 0;
int force_ask_key = 0;
int session_explicit = 0;
const char *prompt = NULL;
int key_from_cli = 0;
static struct option opts[] = {
{"workspace", required_argument, 0, 'w'},
{"session", required_argument, 0, 's'},
{"api-base", required_argument, 0, 'b'},
{"api-key", required_argument, 0, 'k'},
{"model", required_argument, 0, 'm'},
{"headless", no_argument, 0, 'H'},
{"prompt", required_argument, 0, 'p'},
{"demo-ctl", no_argument, 0, 'd'},
{"ask-key", no_argument, 0, 'a'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}};
int c;
while ((c = getopt_long(argc, argv, "w:s:b:k:m:Hp:dah", opts, NULL)) != -1) {
switch (c) {
case 'w':
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
break;
case 's':
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s", optarg);
session_explicit = 1;
break;
case 'b':
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s", optarg);
break;
case 'k':
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", optarg);
key_from_cli = 1;
break;
case 'm':
snprintf(cfg.model, sizeof(cfg.model), "%s", optarg);
break;
case 'H':
cfg.headless = 1;
break;
case 'p':
prompt = optarg;
break;
case 'd':
do_demo = 1;
break;
case 'a':
force_ask_key = 1;
break;
case 'h':
usage(argv[0]);
curl_global_cleanup();
return 0;
default:
usage(argv[0]);
curl_global_cleanup();
return 1;
}
}
/* trim trailing slash on api_base */
size_t alen = strlen(cfg.api_base);
while (alen > 0 && cfg.api_base[alen - 1] == '/')
cfg.api_base[--alen] = 0;
/* Headless defaults to auto-approve shell/write (no interactive confirm). */
if (cfg.headless)
cfg.auto_approve = 1;
/* Default session under <workspace>/.agent/sessions/ (OpenCode-like). */
if (!session_explicit) {
char id[128];
if (cst_agent_store_resolve_default(cfg.workspace_root, cfg.session_path,
sizeof(cfg.session_path), id,
sizeof(id)) != 0) {
fprintf(stderr, "failed to init .agent/sessions under %s\n",
cfg.workspace_root);
curl_global_cleanup();
return 1;
}
fprintf(stderr, "session %s\n", id);
}
if (do_demo) {
int rc = demo_ctl_impl(cfg.workspace_root);
curl_global_cleanup();
return rc;
}
if (force_ask_key || !key_from_cli) {
if (force_ask_key) {
if (cst_creds_prompt_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
fprintf(stderr, "API key prompt cancelled\n");
curl_global_cleanup();
return 1;
}
if (cst_creds_save_api_key(cfg.api_key) != 0)
fprintf(stderr, "warning: failed to save credentials\n");
else
fprintf(stderr, "saved API key to ~/.config/cst-agent/credentials\n");
/* --ask-key alone: save and exit unless running a turn/TUI */
if (!cfg.headless && !prompt) {
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
curl_global_cleanup();
return 0;
}
} else if (!cfg.api_key[0]) {
if (cst_creds_ensure_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
fprintf(stderr, "no API key (set CST_API_KEY or use --ask-key)\n");
curl_global_cleanup();
return 1;
}
}
} else if (key_from_cli) {
/* persist CLI-provided key so next launch can reuse */
if (cst_creds_save_api_key(cfg.api_key) != 0)
fprintf(stderr, "warning: failed to save credentials\n");
}
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
cst_agent_t *agent = cst_agent_create(&cfg);
if (!agent) {
fprintf(stderr, "failed to create agent (session path?)\n");
curl_global_cleanup();
return 1;
}
int rc = 0;
if (cfg.headless) {
if (!prompt) {
fprintf(stderr, "--headless requires --prompt\n");
rc = 1;
} else {
char err[512];
if (cst_agent_run_turn(agent, prompt, err, sizeof(err)) != 0) {
fprintf(stderr, "turn failed: %s\n", err);
rc = 1;
} else {
cst_session_t *s = cst_agent_session(agent);
for (int i = 0; i < cst_session_count(s); i++) {
const cst_message_t *m = cst_session_get(s, i);
printf("[%d role=%d] %s\n", i, (int)m->role,
m->content ? m->content : "");
}
}
}
} else {
rc = cst_tui_run(agent);
}
cst_agent_destroy(agent);
curl_global_cleanup();
return rc;
}
+410
View File
@@ -0,0 +1,410 @@
#include "mm_tool.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MM_OPEN "<minimax:tool_call>"
#define MM_CLOSE "</minimax:tool_call>"
#define MM_OPEN_LEN 19
#define MM_CLOSE_LEN 20
struct mm_scan {
mm_on_event_fn on_event;
void *userdata;
char *buf;
size_t len;
size_t cap;
size_t pending_peak;
int in_tool; /* 1 if last open is unmatched */
char idbuf[CTL_MAX_ID];
unsigned id_seq;
};
static void emit(mm_scan_t *s, ctl_event_t *ev) {
if (s->on_event)
s->on_event(ev, s->userdata);
}
static void emit_text(mm_scan_t *s, const char *t, size_t n) {
if (!t || !n)
return;
ctl_event_t ev = {0};
ev.type = CTL_EV_TEXT;
ev.text = t;
ev.text_len = n;
emit(s, &ev);
}
static int ensure(mm_scan_t *s, size_t need) {
if (s->len + need + 1 <= s->cap)
return 0;
size_t nc = s->cap ? s->cap * 2 : 4096;
while (nc < s->len + need + 1)
nc *= 2;
char *nb = realloc(s->buf, nc);
if (!nb)
return -1;
s->buf = nb;
s->cap = nc;
return 0;
}
static void bump_pending(mm_scan_t *s) {
if (s->len > s->pending_peak)
s->pending_peak = s->len;
}
mm_scan_t *mm_scan_create(mm_on_event_fn on_event, void *userdata) {
mm_scan_t *s = calloc(1, sizeof(*s));
if (!s)
return NULL;
s->on_event = on_event;
s->userdata = userdata;
s->id_seq = 1;
return s;
}
void mm_scan_destroy(mm_scan_t *s) {
if (!s)
return;
free(s->buf);
free(s);
}
void mm_scan_reset(mm_scan_t *s) {
if (!s)
return;
s->len = 0;
s->in_tool = 0;
if (s->buf)
s->buf[0] = 0;
}
size_t mm_scan_pending(const mm_scan_t *s) {
return s ? (s->in_tool ? s->len : 0) : 0;
}
static void clear_call(ctl_tool_call_t *c) {
for (int i = 0; i < c->nargs; i++) {
free(c->args[i].value);
c->args[i].value = NULL;
}
memset(c, 0, sizeof(*c));
}
static int add_arg(ctl_tool_call_t *c, const char *key, const char *val,
size_t vlen) {
if (c->nargs >= CTL_MAX_ARGS)
return -1;
if (strlen(key) >= CTL_MAX_KEY)
return -1;
snprintf(c->args[c->nargs].key, CTL_MAX_KEY, "%s", key);
c->args[c->nargs].value = malloc(vlen + 1);
if (!c->args[c->nargs].value)
return -1;
memcpy(c->args[c->nargs].value, val, vlen);
c->args[c->nargs].value[vlen] = 0;
c->args[c->nargs].value_len = vlen;
c->nargs++;
return 0;
}
/* Extract name="..." or name='...' from a tag start. */
static int extract_name_attr(const char *tag, char *out, size_t out_sz) {
const char *p = strstr(tag, "name=");
if (!p)
return -1;
p += 5;
char quote = 0;
if (*p == '"' || *p == '\'') {
quote = *p++;
}
size_t i = 0;
while (*p && i + 1 < out_sz) {
if (quote) {
if (*p == quote)
break;
} else {
if (*p == ' ' || *p == '>' || *p == '/')
break;
}
out[i++] = *p++;
}
out[i] = 0;
return i > 0 ? 0 : -1;
}
static void emit_call(mm_scan_t *s, ctl_tool_call_t *call) {
/* Only emit allowlisted tools — prevents "cmd" etc. from becoming tools. */
if (!call->name[0])
return;
if (strcmp(call->name, "read_file") != 0 &&
strcmp(call->name, "run_shell") != 0 &&
strcmp(call->name, "write_file") != 0 &&
strcmp(call->name, "apply_patch") != 0)
return;
ctl_event_t ev = {0};
ev.type = CTL_EV_CALL_OPEN;
ev.call = call;
emit(s, &ev);
ev.type = CTL_EV_CALL_READY;
ev.call = call;
emit(s, &ev);
}
/*
* Parse one complete tool_call inner body (between open/close tags).
* Emits one CALL_READY per invoke.
*/
static void parse_tool_block(mm_scan_t *s, const char *inner, size_t n) {
char *block = malloc(n + 1);
if (!block)
return;
memcpy(block, inner, n);
block[n] = 0;
/* Normalize: support both <invoke name="x"> and bare "invoke x" lines. */
char *p = block;
while (*p) {
/* Skip whitespace */
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
p++;
if (!*p)
break;
ctl_tool_call_t call;
memset(&call, 0, sizeof(call));
snprintf(call.id, sizeof(call.id), "mm%u", s->id_seq++);
if (strncmp(p, "<invoke", 7) == 0) {
char *gt = strchr(p, '>');
if (!gt) {
break;
}
char tag[256];
size_t tlen = (size_t)(gt - p) + 1;
if (tlen >= sizeof(tag))
tlen = sizeof(tag) - 1;
memcpy(tag, p, tlen);
tag[tlen] = 0;
if (extract_name_attr(tag, call.name, sizeof(call.name)) != 0) {
p = gt + 1;
continue;
}
p = gt + 1;
char *end_inv = strstr(p, "</invoke>");
if (!end_inv) {
/* unclosed invoke — treat rest as parameters until end */
end_inv = block + n;
}
/* parameters / args inside */
char *q = p;
while (q < end_inv) {
char *arg = strstr(q, "<parameter");
char *arg2 = strstr(q, "<arg");
char *use = NULL;
int is_param = 0;
if (arg && (!arg2 || arg <= arg2) && arg < end_inv) {
use = arg;
is_param = 1;
} else if (arg2 && arg2 < end_inv) {
use = arg2;
is_param = 0;
} else {
break;
}
char *agt = strchr(use, '>');
if (!agt || agt >= end_inv)
break;
char atag[256];
size_t al = (size_t)(agt - use) + 1;
if (al >= sizeof(atag))
al = sizeof(atag) - 1;
memcpy(atag, use, al);
atag[al] = 0;
char key[CTL_MAX_KEY];
if (extract_name_attr(atag, key, sizeof(key)) != 0) {
q = agt + 1;
continue;
}
const char *close = is_param ? "</parameter>" : "</arg>";
char *aend = strstr(agt + 1, close);
if (!aend || aend > end_inv) {
q = agt + 1;
continue;
}
size_t vlen = (size_t)(aend - (agt + 1));
add_arg(&call, key, agt + 1, vlen);
q = aend + strlen(close);
}
emit_call(s, &call);
clear_call(&call);
if (end_inv < block + n && strncmp(end_inv, "</invoke>", 9) == 0)
p = end_inv + 9;
else
p = end_inv;
continue;
}
/* Bare: invoke NAME\n */
if (strncmp(p, "invoke ", 7) == 0) {
p += 7;
while (*p == ' ' || *p == '\t')
p++;
size_t ni = 0;
while (p[ni] && p[ni] != '\n' && p[ni] != '\r' && p[ni] != ' ' &&
p[ni] != '<' && ni + 1 < sizeof(call.name)) {
call.name[ni] = p[ni];
ni++;
}
call.name[ni] = 0;
p += ni;
while (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t')
p++;
/* Collect <arg>...</arg> until next invoke or end */
while (*p) {
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
p++;
if (strncmp(p, "invoke ", 7) == 0 || strncmp(p, "<invoke", 7) == 0)
break;
if (strncmp(p, "<arg", 4) != 0 && strncmp(p, "<parameter", 10) != 0)
break;
int is_param = (strncmp(p, "<parameter", 10) == 0);
char *agt = strchr(p, '>');
if (!agt)
break;
char atag[256];
size_t al = (size_t)(agt - p) + 1;
if (al >= sizeof(atag))
al = sizeof(atag) - 1;
memcpy(atag, p, al);
atag[al] = 0;
char key[CTL_MAX_KEY];
if (extract_name_attr(atag, key, sizeof(key)) != 0) {
p = agt + 1;
continue;
}
const char *close = is_param ? "</parameter>" : "</arg>";
char *aend = strstr(agt + 1, close);
if (!aend)
break;
size_t vlen = (size_t)(aend - (agt + 1));
add_arg(&call, key, agt + 1, vlen);
p = aend + strlen(close);
}
if (call.name[0]) {
emit_call(s, &call);
}
clear_call(&call);
continue;
}
/* Unknown content — skip one char */
p++;
}
free(block);
}
/* Drop stray close tags that MiniMax sometimes emits outside a tool block. */
static void strip_stray_closes(mm_scan_t *s) {
for (;;) {
char *c = strstr(s->buf, MM_CLOSE);
if (!c)
return;
/* Only strip if it appears before any open (orphan). */
char *o = strstr(s->buf, MM_OPEN);
if (o && o < c)
return;
size_t at = (size_t)(c - s->buf);
size_t rest = s->len - at - MM_CLOSE_LEN;
if (at > 0)
emit_text(s, s->buf, at);
memmove(s->buf, c + MM_CLOSE_LEN, rest);
s->len = rest;
s->buf[s->len] = 0;
}
}
/* Process buffer: emit text before tools, parse complete tool blocks. */
static void drain(mm_scan_t *s) {
for (;;) {
if (!s->in_tool) {
strip_stray_closes(s);
char *open = strstr(s->buf, MM_OPEN);
if (!open) {
/* Keep a short tail that could be a partial open tag. */
size_t keep = MM_OPEN_LEN - 1;
if (s->len <= keep)
return;
size_t emit_n = s->len - keep;
/* Don't split a UTF-8 codepoint across emit/keep. */
while (emit_n > 0 &&
((unsigned char)s->buf[emit_n] & 0xC0) == 0x80)
emit_n--;
if (emit_n == 0)
return;
emit_text(s, s->buf, emit_n);
memmove(s->buf, s->buf + emit_n, s->len - emit_n);
s->len -= emit_n;
s->buf[s->len] = 0;
return;
}
size_t before = (size_t)(open - s->buf);
if (before)
emit_text(s, s->buf, before);
/* drop open tag from buffer, enter tool mode */
size_t rest = s->len - before - MM_OPEN_LEN;
memmove(s->buf, open + MM_OPEN_LEN, rest);
s->len = rest;
s->buf[s->len] = 0;
s->in_tool = 1;
bump_pending(s);
continue;
}
/* in tool: wait for close */
char *close = strstr(s->buf, MM_CLOSE);
if (!close)
return; /* pending */
size_t inner_len = (size_t)(close - s->buf);
parse_tool_block(s, s->buf, inner_len);
size_t rest = s->len - inner_len - MM_CLOSE_LEN;
memmove(s->buf, close + MM_CLOSE_LEN, rest);
s->len = rest;
s->buf[s->len] = 0;
s->in_tool = 0;
}
}
int mm_scan_feed(mm_scan_t *s, const char *data, size_t len) {
if (!s || !data)
return -1;
if (ensure(s, len) != 0)
return -1;
memcpy(s->buf + s->len, data, len);
s->len += len;
s->buf[s->len] = 0;
bump_pending(s);
drain(s);
return 0;
}
int mm_scan_finish(mm_scan_t *s) {
if (!s)
return -1;
drain(s);
if (s->in_tool) {
/* Incomplete tool — do NOT execute; emit as visible text so UI isn't stuck */
emit_text(s, MM_OPEN, MM_OPEN_LEN);
if (s->len)
emit_text(s, s->buf, s->len);
s->len = 0;
s->in_tool = 0;
} else if (s->len) {
emit_text(s, s->buf, s->len);
s->len = 0;
}
return 0;
}
+67
View File
@@ -0,0 +1,67 @@
#include "cst_agent.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MODEL_MAX_LEN 128
static int already(char out[][MODEL_MAX_LEN], int n, const char *name) {
for (int i = 0; i < n; i++) {
if (strcmp(out[i], name) == 0)
return 1;
}
return 0;
}
static int push(char out[][MODEL_MAX_LEN], int *n, int max, const char *name) {
if (!name || !name[0] || *n >= max)
return 0;
if (already(out, *n, name))
return 0;
snprintf(out[*n], MODEL_MAX_LEN, "%s", name);
(*n)++;
return 1;
}
int cst_models_list(const char *current, char out[][128], int max_out) {
if (!out || max_out <= 0)
return 0;
int n = 0;
const char *env = getenv("CST_MODELS");
if (env && env[0]) {
char buf[1024];
snprintf(buf, sizeof(buf), "%s", env);
char *p = buf;
while (*p && n < max_out) {
while (*p == ':' || *p == ' ' || *p == ',')
p++;
if (!*p)
break;
char *start = p;
while (*p && *p != ':' && *p != ',')
p++;
char save = *p;
*p = 0;
/* trim trailing spaces */
char *end = start + strlen(start);
while (end > start && (end[-1] == ' ' || end[-1] == '\t'))
*--end = 0;
push(out, &n, max_out, start);
*p = save;
if (*p)
p++;
}
} else {
push(out, &n, max_out, "MiniMax-M2.7");
push(out, &n, max_out, "MiniMax-M2.5");
push(out, &n, max_out, "gpt-4o");
push(out, &n, max_out, "gpt-4o-mini");
}
if (current && current[0])
push(out, &n, max_out, current);
return n;
}
+584
View File
@@ -0,0 +1,584 @@
#define _DEFAULT_SOURCE
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "os_compat.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <direct.h>
#include <fcntl.h>
#include <io.h>
#include <process.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
void os_run_result_free(os_run_result_t *r) {
if (!r)
return;
free(r->out);
r->out = NULL;
r->out_len = 0;
}
int os_path_is_abs(const char *p) {
if (!p || !p[0])
return 0;
#ifdef _WIN32
if (p[0] == '/' || p[0] == '\\')
return 1;
if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
p[1] == ':' && (p[2] == '/' || p[2] == '\\' || p[2] == '\0'))
return 1;
return 0;
#else
return p[0] == '/';
#endif
}
void os_path_norm_slashes(char *p) {
if (!p)
return;
for (; *p; p++) {
if (*p == '\\')
*p = '/';
}
}
int os_path_join(char *out, size_t out_sz, const char *a, const char *b) {
if (!out || out_sz < 2 || !a)
return -1;
if (!b || !b[0]) {
snprintf(out, out_sz, "%s", a);
return 0;
}
if (os_path_is_abs(b)) {
snprintf(out, out_sz, "%s", b);
return 0;
}
size_t al = strlen(a);
while (al > 0 && (a[al - 1] == '/' || a[al - 1] == '\\'))
al--;
#ifdef _WIN32
int n = snprintf(out, out_sz, "%.*s\\%s", (int)al, a, b);
#else
int n = snprintf(out, out_sz, "%.*s/%s", (int)al, a, b);
#endif
if (n < 0 || (size_t)n >= out_sz)
return -1;
return 0;
}
int os_realpath(const char *path, char *out, size_t out_sz) {
if (!path || !out || out_sz < 2)
return -1;
#ifdef _WIN32
char buf[OS_PATH_MAX];
DWORD n = GetFullPathNameA(path, (DWORD)sizeof(buf), buf, NULL);
if (n == 0 || n >= sizeof(buf))
return -1;
/* Prefer forward slashes internally for sandbox compares. */
os_path_norm_slashes(buf);
if (strlen(buf) + 1 > out_sz)
return -1;
memcpy(out, buf, strlen(buf) + 1);
/* Lowercase drive letter for stable compares */
if (out[0] && out[1] == ':') {
if (out[0] >= 'A' && out[0] <= 'Z')
out[0] = (char)(out[0] - 'A' + 'a');
}
return 0;
#else
char *r = realpath(path, NULL);
if (!r)
return -1;
if (strlen(r) + 1 > out_sz) {
free(r);
return -1;
}
memcpy(out, r, strlen(r) + 1);
free(r);
return 0;
#endif
}
int os_path_under(const char *root, const char *path) {
if (!root || !path)
return 0;
size_t rl = strlen(root);
if (strncmp(path, root, rl) != 0)
return 0;
if (path[rl] == '\0')
return 1;
#ifdef _WIN32
return path[rl] == '/' || path[rl] == '\\';
#else
return path[rl] == '/';
#endif
}
int os_mkdir(const char *path, int mode) {
if (!path)
return -1;
#ifdef _WIN32
(void)mode;
if (_mkdir(path) != 0 && errno != EEXIST)
return -1;
return 0;
#else
if (mkdir(path, (mode_t)mode) != 0 && errno != EEXIST)
return -1;
return 0;
#endif
}
int os_config_dir(char *out, size_t out_sz) {
if (!out || out_sz < 8)
return -1;
#ifdef _WIN32
const char *base = getenv("APPDATA");
if (!base || !base[0])
base = getenv("LOCALAPPDATA");
if (!base || !base[0])
return -1;
int n = snprintf(out, out_sz, "%s\\cst-agent", base);
#else
const char *home = getenv("HOME");
if (!home || !home[0])
return -1;
int n = snprintf(out, out_sz, "%s/.config/cst-agent", home);
#endif
if (n < 0 || (size_t)n >= out_sz)
return -1;
return 0;
}
int os_temp_dir(char *out, size_t out_sz) {
if (!out || out_sz < 2)
return -1;
#ifdef _WIN32
DWORD n = GetTempPathA((DWORD)out_sz, out);
if (n == 0 || n >= out_sz)
return -1;
os_path_norm_slashes(out);
size_t L = strlen(out);
while (L > 0 && out[L - 1] == '/')
out[--L] = 0;
return 0;
#else
const char *t = getenv("TMPDIR");
if (!t || !t[0])
t = "/tmp";
snprintf(out, out_sz, "%s", t);
return 0;
#endif
}
int os_mkstemp(char *out, size_t out_sz, const char *prefix) {
if (!out || out_sz < 16)
return -1;
char dir[OS_PATH_MAX];
if (os_temp_dir(dir, sizeof(dir)) != 0)
return -1;
if (!prefix)
prefix = "cst";
#ifdef _WIN32
char name[MAX_PATH];
if (GetTempFileNameA(dir, prefix, 0, name) == 0)
return -1;
os_path_norm_slashes(name);
if (strlen(name) + 1 > out_sz)
return -1;
memcpy(out, name, strlen(name) + 1);
int fd = _open(out, _O_RDWR | _O_BINARY);
return fd;
#else
int n = snprintf(out, out_sz, "%s/%s-XXXXXX", dir, prefix);
if (n < 0 || (size_t)n >= out_sz)
return -1;
int fd = mkstemp(out);
return fd;
#endif
}
/* Optional: main() may set this for Termux / argv0 fallback when
* /proc/self/exe is unavailable. */
static char g_argv0_dir[OS_PATH_MAX];
void os_set_prog_path(const char *argv0) {
g_argv0_dir[0] = 0;
if (!argv0 || !argv0[0])
return;
char buf[OS_PATH_MAX];
snprintf(buf, sizeof(buf), "%s", argv0);
os_path_norm_slashes(buf);
char *slash = strrchr(buf, '/');
if (!slash)
return;
*slash = 0;
/* Relative "./foo" → keep as-is; caller may pass absolute. */
snprintf(g_argv0_dir, sizeof(g_argv0_dir), "%s", buf);
}
int os_exe_dir(char *out, size_t out_sz) {
if (!out || out_sz < 2)
return -1;
const char *env = getenv("CST_EXE_DIR");
if (env && env[0]) {
snprintf(out, out_sz, "%s", env);
return 0;
}
#ifdef _WIN32
char buf[OS_PATH_MAX];
DWORD n = GetModuleFileNameA(NULL, buf, (DWORD)sizeof(buf));
if (n == 0 || n >= sizeof(buf))
return -1;
os_path_norm_slashes(buf);
char *slash = strrchr(buf, '/');
if (!slash)
return -1;
*slash = 0;
if (strlen(buf) + 1 > out_sz)
return -1;
memcpy(out, buf, strlen(buf) + 1);
return 0;
#else
char buf[OS_PATH_MAX];
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
if (n >= 0) {
buf[n] = 0;
char *slash = strrchr(buf, '/');
if (slash) {
*slash = 0;
if (strlen(buf) + 1 <= out_sz) {
memcpy(out, buf, strlen(buf) + 1);
return 0;
}
}
}
/* Termux / restricted Android: fall back to argv0 dir. */
if (g_argv0_dir[0]) {
snprintf(out, out_sz, "%s", g_argv0_dir);
return 0;
}
return -1;
#endif
}
static int file_exists(const char *p) {
#ifdef _WIN32
DWORD a = GetFileAttributesA(p);
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
#else
return access(p, X_OK) == 0 || access(p, F_OK) == 0;
#endif
}
int os_resolve_shell(char *out, size_t out_sz) {
if (!out || out_sz < 2)
return -1;
const char *env = getenv("CST_SHELL");
if (env && env[0]) {
snprintf(out, out_sz, "%s", env);
return 0;
}
char dir[OS_PATH_MAX];
if (os_exe_dir(dir, sizeof(dir)) == 0) {
#ifdef _WIN32
char cand[OS_PATH_MAX];
if (os_path_join(cand, sizeof(cand), dir, "busybox.exe") == 0 &&
file_exists(cand)) {
snprintf(out, out_sz, "%s", cand);
return 0;
}
#else
char cand[OS_PATH_MAX];
if (os_path_join(cand, sizeof(cand), dir, "busybox") == 0 &&
file_exists(cand)) {
snprintf(out, out_sz, "%s", cand);
return 0;
}
#endif
}
#ifdef _WIN32
snprintf(out, out_sz, "%s", "busybox.exe");
return 0;
#else
/* Termux: $PREFIX/bin/sh */
{
const char *prefix = getenv("PREFIX");
if (prefix && prefix[0]) {
char cand[OS_PATH_MAX];
if (os_path_join(cand, sizeof(cand), prefix, "bin/sh") == 0 &&
file_exists(cand)) {
snprintf(out, out_sz, "%s", cand);
return 0;
}
}
}
if (file_exists("/bin/sh")) {
snprintf(out, out_sz, "%s", "/bin/sh");
return 0;
}
if (file_exists("/system/bin/sh")) {
snprintf(out, out_sz, "%s", "/system/bin/sh");
return 0;
}
snprintf(out, out_sz, "%s", "sh");
return 0;
#endif
}
#ifdef _WIN32
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
os_run_result_t *r) {
if (!argv || !argv[0] || !r)
return -1;
if (max_out == 0)
max_out = 512 * 1024;
memset(r, 0, sizeof(*r));
char cmdline[8192];
size_t pos = 0;
cmdline[0] = 0;
for (int i = 0; argv[i]; i++) {
const char *a = argv[i];
int need_q = strchr(a, ' ') || strchr(a, '\t') || strchr(a, '"');
if (i && pos + 1 < sizeof(cmdline))
cmdline[pos++] = ' ';
if (need_q) {
if (pos + 1 < sizeof(cmdline))
cmdline[pos++] = '"';
for (const char *p = a; *p && pos + 2 < sizeof(cmdline); p++) {
if (*p == '"') {
cmdline[pos++] = '\\';
cmdline[pos++] = '"';
} else {
cmdline[pos++] = *p;
}
}
if (pos + 1 < sizeof(cmdline))
cmdline[pos++] = '"';
} else {
size_t L = strlen(a);
if (pos + L >= sizeof(cmdline))
break;
memcpy(cmdline + pos, a, L);
pos += L;
}
cmdline[pos] = 0;
}
SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE rd = NULL, wr = NULL;
if (!CreatePipe(&rd, &wr, &sa, 0))
return -1;
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOA si = {0};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = wr;
si.hStdError = wr;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION pi = {0};
char *cmd_mut = _strdup(cmdline);
if (!cmd_mut) {
CloseHandle(rd);
CloseHandle(wr);
return -1;
}
BOOL ok = CreateProcessA(NULL, cmd_mut, NULL, NULL, TRUE, 0, NULL,
cwd && cwd[0] ? cwd : NULL, &si, &pi);
free(cmd_mut);
CloseHandle(wr);
if (!ok) {
CloseHandle(rd);
return -1;
}
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) {
CloseHandle(rd);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return -1;
}
for (;;) {
char tmp[1024];
DWORD n = 0;
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
break;
if (len + n + 1 > cap) {
size_t nc = cap * 2;
if (nc < len + n + 1)
nc = len + n + 1;
char *nb = realloc(buf, nc);
if (!nb)
break;
buf = nb;
cap = nc;
}
memcpy(buf + len, tmp, n);
len += n;
if (len >= max_out)
break;
}
CloseHandle(rd);
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
buf[len] = 0;
r->out = buf;
r->out_len = len;
r->exit_code = (int)code;
return 0;
}
#else
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
os_run_result_t *r) {
if (!argv || !argv[0] || !r)
return -1;
if (max_out == 0)
max_out = 512 * 1024;
memset(r, 0, sizeof(*r));
int pipefd[2];
if (pipe(pipefd) != 0)
return -1;
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]);
if (cwd && cwd[0] && chdir(cwd) != 0)
_exit(127);
execvp(argv[0], (char *const *)argv);
_exit(127);
}
close(pipefd[1]);
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) {
close(pipefd[0]);
waitpid(pid, NULL, 0);
return -1;
}
for (;;) {
if (len + 1024 > cap) {
cap *= 2;
char *nb = realloc(buf, cap);
if (!nb)
break;
buf = nb;
}
ssize_t n = read(pipefd[0], buf + len, 1024);
if (n < 0) {
if (errno == EINTR)
continue;
break;
}
if (n == 0)
break;
len += (size_t)n;
if (len >= max_out)
break;
}
close(pipefd[0]);
int status = 0;
waitpid(pid, &status, 0);
buf[len] = 0;
r->out = buf;
r->out_len = len;
r->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
return 0;
}
#endif
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
os_run_result_t *r) {
if (!cmd || !r)
return -1;
char shell[OS_PATH_MAX];
if (os_resolve_shell(shell, sizeof(shell)) != 0)
return -1;
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd */
int is_busybox = 0;
{
const char *base = shell;
const char *s = strrchr(shell, '/');
#ifdef _WIN32
const char *s2 = strrchr(shell, '\\');
if (s2 && (!s || s2 > s))
s = s2;
#endif
if (s)
base = s + 1;
if (strncmp(base, "busybox", 7) == 0)
is_busybox = 1;
}
if (is_busybox) {
const char *argv[] = {shell, "sh", "-c", cmd, NULL};
return os_run_capture(cwd, argv, max_out, r);
} else {
const char *argv[] = {shell, "-c", cmd, NULL};
return os_run_capture(cwd, argv, max_out, r);
}
}
void os_localtime(const time_t *t, struct tm *out) {
if (!t || !out)
return;
#ifdef _WIN32
localtime_s(out, t);
#else
localtime_r(t, out);
#endif
}
int os_getpid(void) {
#ifdef _WIN32
return (int)GetCurrentProcessId();
#else
return (int)getpid();
#endif
}
int os_chmod_private(const char *path) {
if (!path)
return -1;
#ifdef _WIN32
(void)path;
return 0;
#else
return chmod(path, 0600);
#endif
}
+61
View File
@@ -0,0 +1,61 @@
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cst_agent.h"
#include "os_compat.h"
int cst_sandbox_resolve(const char *workspace_root, const char *rel_or_abs,
char *out, size_t out_sz) {
if (!workspace_root || !rel_or_abs || !out || out_sz < 2)
return -1;
char root_real[OS_PATH_MAX];
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
return -1;
os_path_norm_slashes(root_real);
size_t root_len = strlen(root_real);
char candidate[OS_PATH_MAX];
if (os_path_is_abs(rel_or_abs)) {
snprintf(candidate, sizeof(candidate), "%s", rel_or_abs);
} else if (os_path_join(candidate, sizeof(candidate), root_real, rel_or_abs) !=
0) {
return -1;
}
os_path_norm_slashes(candidate);
char resolved[OS_PATH_MAX];
if (os_realpath(candidate, resolved, sizeof(resolved)) == 0) {
os_path_norm_slashes(resolved);
if (!os_path_under(root_real, resolved))
return -1;
if (strlen(resolved) + 1 > out_sz)
return -1;
memcpy(out, resolved, strlen(resolved) + 1);
return 0;
}
/* Parent exists, leaf may be new file. */
char tmp[OS_PATH_MAX];
snprintf(tmp, sizeof(tmp), "%s", candidate);
char *slash = strrchr(tmp, '/');
if (!slash)
return -1;
*slash = 0;
const char *base = slash + 1;
char parent_real[OS_PATH_MAX];
if (os_realpath(tmp, parent_real, sizeof(parent_real)) != 0)
return -1;
os_path_norm_slashes(parent_real);
if (!os_path_under(root_real, parent_real) &&
strcmp(parent_real, root_real) != 0)
return -1;
(void)root_len;
int n = snprintf(out, out_sz, "%s/%s", parent_real, base);
if (n < 0 || (size_t)n >= out_sz)
return -1;
return 0;
}
+363
View File
@@ -0,0 +1,363 @@
#define _DEFAULT_SOURCE
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_agent.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
typedef long long cst_ssize_t;
static cst_ssize_t cst_getline(char **lineptr, size_t *n, FILE *stream) {
if (!lineptr || !n || !stream)
return -1;
if (!*lineptr || *n == 0) {
*n = 256;
*lineptr = (char *)malloc(*n);
if (!*lineptr)
return -1;
}
size_t len = 0;
int c = 0;
while ((c = fgetc(stream)) != EOF) {
if (len + 2 > *n) {
size_t nn = (*n) * 2;
char *nb = (char *)realloc(*lineptr, nn);
if (!nb)
return -1;
*lineptr = nb;
*n = nn;
}
(*lineptr)[len++] = (char)c;
if (c == '\n')
break;
}
if (len == 0 && c == EOF)
return -1;
(*lineptr)[len] = 0;
return (cst_ssize_t)len;
}
#define getline cst_getline
#endif
struct cst_session {
char path[4096];
cst_message_t *msgs;
int nmsgs;
int cap;
FILE *fp;
};
static char *xstrdup(const char *s) {
if (!s)
return NULL;
size_t n = strlen(s);
char *p = malloc(n + 1);
if (!p)
return NULL;
memcpy(p, s, n + 1);
return p;
}
static const char *role_str(cst_role_t r) {
switch (r) {
case CST_ROLE_SYSTEM:
return "system";
case CST_ROLE_USER:
return "user";
case CST_ROLE_ASSISTANT:
return "assistant";
case CST_ROLE_TOOL:
return "tool";
}
return "user";
}
static cst_role_t role_from(const char *s) {
if (strcmp(s, "system") == 0)
return CST_ROLE_SYSTEM;
if (strcmp(s, "assistant") == 0)
return CST_ROLE_ASSISTANT;
if (strcmp(s, "tool") == 0)
return CST_ROLE_TOOL;
return CST_ROLE_USER;
}
static int json_escape_write(FILE *fp, const char *s) {
fputc('"', fp);
for (; s && *s; s++) {
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\') {
fputc('\\', fp);
fputc(c, fp);
} else if (c == '\n') {
fputs("\\n", fp);
} else if (c == '\r') {
fputs("\\r", fp);
} else if (c == '\t') {
fputs("\\t", fp);
} else if (c < 0x20) {
fprintf(fp, "\\u%04x", c);
} else {
fputc(c, fp);
}
}
fputc('"', fp);
return 0;
}
/* Minimal extract "key":"value" from a single JSONL object line. */
static int extract_str(const char *line, const char *key, char *out,
size_t out_sz) {
char pat[64];
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
const char *p = strstr(line, pat);
if (!p)
return -1;
p += strlen(pat);
size_t o = 0;
while (*p && o + 1 < out_sz) {
if (*p == '\\' && p[1]) {
p++;
if (*p == 'n')
out[o++] = '\n';
else if (*p == 'r')
out[o++] = '\r';
else if (*p == 't')
out[o++] = '\t';
else
out[o++] = *p;
p++;
continue;
}
if (*p == '"') {
out[o] = 0;
return 0;
}
out[o++] = *p++;
}
return -1;
}
static int session_grow(cst_session_t *s) {
if (s->nmsgs < s->cap)
return 0;
int ncap = s->cap ? s->cap * 2 : 32;
cst_message_t *nm = realloc(s->msgs, (size_t)ncap * sizeof(cst_message_t));
if (!nm)
return -1;
s->msgs = nm;
s->cap = ncap;
return 0;
}
static int session_push_mem(cst_session_t *s, cst_role_t role, const char *content,
const char *tool_id, const char *tool_name,
const char *tool_calls_json) {
if (session_grow(s) != 0)
return -1;
cst_message_t *d = &s->msgs[s->nmsgs];
memset(d, 0, sizeof(*d));
d->role = role;
d->content = xstrdup(content ? content : "");
if (!d->content)
return -1;
if (tool_id)
snprintf(d->tool_call_id, sizeof(d->tool_call_id), "%s", tool_id);
if (tool_name)
snprintf(d->tool_name, sizeof(d->tool_name), "%s", tool_name);
if (tool_calls_json && tool_calls_json[0])
d->tool_calls_json = xstrdup(tool_calls_json);
s->nmsgs++;
return 0;
}
static void load_existing(cst_session_t *s) {
FILE *rp = fopen(s->path, "r");
if (!rp)
return;
char *line = NULL;
size_t cap = 0;
while (getline(&line, &cap, rp) != -1) {
char role[32] = {0};
char *content = malloc(cap > 8 ? cap : 4096);
char tid[CTL_MAX_ID] = {0};
char tname[CTL_MAX_NAME] = {0};
if (!content)
continue;
content[0] = 0;
if (extract_str(line, "role", role, sizeof(role)) != 0) {
free(content);
continue;
}
/* content may be large — reuse line extract into growing buffer */
free(content);
content = malloc(strlen(line) + 1);
if (!content)
continue;
if (extract_str(line, "content", content, strlen(line) + 1) != 0) {
free(content);
continue;
}
extract_str(line, "tool_call_id", tid, sizeof(tid));
extract_str(line, "tool_name", tname, sizeof(tname));
char *tcj = malloc(strlen(line) + 1);
const char *tcjp = NULL;
if (tcj) {
tcj[0] = 0;
if (extract_str(line, "tool_calls_json", tcj, strlen(line) + 1) == 0)
tcjp = tcj;
}
session_push_mem(s, role_from(role), content, tid[0] ? tid : NULL,
tname[0] ? tname : NULL, tcjp);
free(tcj);
free(content);
}
free(line);
fclose(rp);
}
cst_session_t *cst_session_open(const char *path) {
cst_session_t *s = calloc(1, sizeof(*s));
if (!s)
return NULL;
snprintf(s->path, sizeof(s->path), "%s", path ? path : "session.jsonl");
load_existing(s);
s->fp = fopen(s->path, "a+");
if (!s->fp) {
for (int i = 0; i < s->nmsgs; i++) {
free(s->msgs[i].content);
free(s->msgs[i].tool_calls_json);
}
free(s->msgs);
free(s);
return NULL;
}
if (!s->msgs) {
s->cap = 32;
s->msgs = calloc((size_t)s->cap, sizeof(cst_message_t));
if (!s->msgs) {
fclose(s->fp);
free(s);
return NULL;
}
}
return s;
}
void cst_session_close(cst_session_t *s) {
if (!s)
return;
for (int i = 0; i < s->nmsgs; i++) {
free(s->msgs[i].content);
free(s->msgs[i].tool_calls_json);
}
free(s->msgs);
if (s->fp)
fclose(s->fp);
free(s);
}
int cst_session_append(cst_session_t *s, const cst_message_t *msg) {
if (!s || !msg)
return -1;
if (session_push_mem(s, msg->role, msg->content, msg->tool_call_id,
msg->tool_name, msg->tool_calls_json) != 0)
return -1;
fprintf(s->fp, "{\"role\":");
json_escape_write(s->fp, role_str(msg->role));
fprintf(s->fp, ",\"content\":");
json_escape_write(s->fp, msg->content ? msg->content : "");
if (msg->tool_call_id[0]) {
fprintf(s->fp, ",\"tool_call_id\":");
json_escape_write(s->fp, msg->tool_call_id);
}
if (msg->tool_name[0]) {
fprintf(s->fp, ",\"tool_name\":");
json_escape_write(s->fp, msg->tool_name);
}
if (msg->tool_calls_json && msg->tool_calls_json[0]) {
fprintf(s->fp, ",\"tool_calls_json\":");
json_escape_write(s->fp, msg->tool_calls_json);
}
fprintf(s->fp, "}\n");
fflush(s->fp);
return 0;
}
int cst_session_count(const cst_session_t *s) {
return s ? s->nmsgs : 0;
}
const cst_message_t *cst_session_get(const cst_session_t *s, int i) {
if (!s || i < 0 || i >= s->nmsgs)
return NULL;
return &s->msgs[i];
}
const char *cst_session_path(const cst_session_t *s) {
return s ? s->path : NULL;
}
int cst_session_clear(cst_session_t *s, int keep_system) {
if (!s)
return -1;
cst_message_t sys = {0};
int have_sys = 0;
if (keep_system) {
for (int i = 0; i < s->nmsgs; i++) {
if (s->msgs[i].role == CST_ROLE_SYSTEM) {
sys.role = CST_ROLE_SYSTEM;
sys.content = xstrdup(s->msgs[i].content ? s->msgs[i].content : "");
have_sys = 1;
break;
}
}
}
for (int i = 0; i < s->nmsgs; i++) {
free(s->msgs[i].content);
free(s->msgs[i].tool_calls_json);
}
s->nmsgs = 0;
if (s->fp) {
fclose(s->fp);
s->fp = NULL;
}
s->fp = fopen(s->path, "w+");
if (!s->fp) {
if (have_sys)
free(sys.content);
return -1;
}
if (have_sys) {
cst_session_append(s, &sys);
free(sys.content);
}
return 0;
}
int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
if (!s || n < 0)
return -1;
for (int i = 0; i < s->nmsgs; i++) {
free(s->msgs[i].content);
free(s->msgs[i].tool_calls_json);
}
s->nmsgs = 0;
if (s->fp) {
fclose(s->fp);
s->fp = NULL;
}
s->fp = fopen(s->path, "w+");
if (!s->fp)
return -1;
for (int i = 0; i < n; i++) {
if (cst_session_append(s, &msgs[i]) != 0)
return -1;
}
return 0;
}
+784
View File
@@ -0,0 +1,784 @@
#define _DEFAULT_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cst_agent.h"
#include "os_compat.h"
#ifndef _WIN32
#include <unistd.h>
#else
#include <io.h>
#include <fcntl.h>
#endif
void cst_tool_result_free(cst_tool_result_t *r) {
if (!r)
return;
free(r->out);
r->out = NULL;
r->out_len = 0;
}
static const char *arg_get(const ctl_tool_call_t *call, const char *key) {
for (int i = 0; i < call->nargs; i++) {
if (strcmp(call->args[i].key, key) == 0)
return call->args[i].value ? call->args[i].value : "";
}
return NULL;
}
static int set_result(cst_tool_result_t *r, const char *s, int code) {
cst_tool_result_free(r);
size_t n = s ? strlen(s) : 0;
r->out = malloc(n + 1);
if (!r->out)
return -1;
if (n)
memcpy(r->out, s, n);
r->out[n] = 0;
r->out_len = n;
r->exit_code = code;
return 0;
}
/* Read whole file into malloc'd buffer; *out_len set. Returns NULL if missing. */
static char *slurp_file(const char *full, size_t *out_len) {
*out_len = 0;
FILE *fp = fopen(full, "rb");
if (!fp)
return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long sz = ftell(fp);
if (sz < 0) {
fclose(fp);
return NULL;
}
rewind(fp);
char *buf = malloc((size_t)sz + 1);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
buf[n] = 0;
*out_len = n;
return buf;
}
#define DIFF_OUT_CAP (48 * 1024)
/* OpenCode-style: always surface a unified diff after write/patch. */
static char *make_unified_diff(const char *relpath, const char *oldc, size_t olen,
const char *newc, size_t nlen) {
char t0[OS_PATH_MAX], t1[OS_PATH_MAX];
int fd0 = os_mkstemp(t0, sizeof(t0), "cstda");
int fd1 = os_mkstemp(t1, sizeof(t1), "cstdb");
if (fd0 < 0 || fd1 < 0) {
if (fd0 >= 0) {
#ifdef _WIN32
_close(fd0);
_unlink(t0);
#else
close(fd0);
unlink(t0);
#endif
}
if (fd1 >= 0) {
#ifdef _WIN32
_close(fd1);
_unlink(t1);
#else
close(fd1);
unlink(t1);
#endif
}
return NULL;
}
if (olen) {
#ifdef _WIN32
(void)_write(fd0, oldc, (unsigned)olen);
#else
(void)write(fd0, oldc, olen);
#endif
}
if (nlen) {
#ifdef _WIN32
(void)_write(fd1, newc, (unsigned)nlen);
#else
(void)write(fd1, newc, nlen);
#endif
}
#ifdef _WIN32
_close(fd0);
_close(fd1);
#else
close(fd0);
close(fd1);
#endif
const char *label = relpath && relpath[0] ? relpath : "file";
char cmd[OS_PATH_MAX * 2 + 256];
snprintf(cmd, sizeof(cmd),
"diff -u --label a/%s --label b/%s \"%s\" \"%s\"", label, label, t0,
t1);
os_run_result_t rr = {0};
int rc = os_run_shell_c(NULL, cmd, DIFF_OUT_CAP, &rr);
#ifdef _WIN32
_unlink(t0);
_unlink(t1);
#else
unlink(t0);
unlink(t1);
#endif
if (rc != 0) {
os_run_result_free(&rr);
return strdup("(no textual diff — diff unavailable)\n");
}
/* diff returns 1 when files differ — still success for our purposes */
if (!rr.out || rr.out_len == 0) {
os_run_result_free(&rr);
return strdup("(no textual diff — identical or binary)\n");
}
char *out = rr.out;
rr.out = NULL;
size_t len = rr.out_len;
os_run_result_free(&rr);
if (len >= DIFF_OUT_CAP - 64) {
snprintf(out + DIFF_OUT_CAP - 64, 64, "\n…(diff truncated)\n");
}
return out;
}
static int set_result_with_diff(cst_tool_result_t *r, const char *relpath,
const char *oldc, size_t olen, const char *newc,
size_t nlen, int code) {
char *diff = make_unified_diff(relpath, oldc ? oldc : "", olen, newc ? newc : "",
nlen);
char hdr[256];
snprintf(hdr, sizeof(hdr), "wrote %s (%zu bytes)\n",
relpath ? relpath : "?", nlen);
size_t hlen = strlen(hdr);
size_t dlen = diff ? strlen(diff) : 0;
char *buf = malloc(hlen + dlen + 1);
if (!buf) {
free(diff);
return set_result(r, "ok", code);
}
memcpy(buf, hdr, hlen);
if (diff && dlen)
memcpy(buf + hlen, diff, dlen);
buf[hlen + dlen] = 0;
free(diff);
cst_tool_result_free(r);
r->out = buf;
r->out_len = hlen + dlen;
r->exit_code = code;
return 0;
}
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *path = arg_get(call, "path");
if (!path)
return set_result(r, "error: missing path", 1);
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
FILE *fp = fopen(full, "rb");
if (!fp)
return set_result(r, "error: cannot open file", 1);
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return set_result(r, "error: seek failed", 1);
}
long sz = ftell(fp);
if (sz < 0 || sz > 2 * 1024 * 1024) {
fclose(fp);
return set_result(r, "error: file too large or empty seek", 1);
}
rewind(fp);
char *buf = malloc((size_t)sz + 1);
if (!buf) {
fclose(fp);
return set_result(r, "error: oom", 1);
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
buf[n] = 0;
cst_tool_result_free(r);
r->out = buf;
r->out_len = n;
r->exit_code = 0;
return 0;
}
int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *path = arg_get(call, "path");
const char *content = arg_get(call, "content");
if (!path || !content)
return set_result(r, "error: missing path or content", 1);
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
size_t olen = 0;
char *oldc = slurp_file(full, &olen);
FILE *fp = fopen(full, "wb");
if (!fp) {
free(oldc);
return set_result(r, "error: cannot write file", 1);
}
size_t n = strlen(content);
if (fwrite(content, 1, n, fp) != n) {
fclose(fp);
free(oldc);
return set_result(r, "error: write failed", 1);
}
fclose(fp);
int rc = set_result_with_diff(r, path, oldc, olen, content, n, 0);
free(oldc);
return rc;
}
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *cmd = arg_get(call, "cmd");
if (!cmd)
cmd = arg_get(call, "command");
if (!cmd)
return set_result(r, "error: missing cmd", 1);
char root_real[OS_PATH_MAX];
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
return set_result(r, "error: bad workspace", 1);
os_run_result_t rr = {0};
if (os_run_shell_c(root_real, cmd, 512 * 1024, &rr) != 0) {
os_run_result_free(&rr);
return set_result(r, "error: shell exec failed", 1);
}
cst_tool_result_free(r);
r->out = rr.out;
r->out_len = rr.out_len;
r->exit_code = rr.exit_code;
rr.out = NULL;
os_run_result_free(&rr);
return 0;
}
/* Minimal unified diff apply: only support *** Update File: / write via
* apply_patch tool with arg path + content for MVP simplicity is write_file.
* For apply_patch: expect arg "patch" with a simple format:
* *** Update File: <relpath>
* <full new file content>
* Or classic unified diff for single file with ---/+++ and @@ hunks.
*/
static int apply_full_replace(const char *workspace_root, const char *path,
const char *content, cst_tool_result_t *r) {
ctl_tool_call_t fake = {0};
snprintf(fake.name, sizeof(fake.name), "write_file");
fake.nargs = 2;
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
fake.args[0].value = (char *)path;
fake.args[0].value_len = strlen(path);
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
fake.args[1].value = (char *)content;
fake.args[1].value_len = strlen(content);
return cst_tool_write_file(workspace_root, &fake, r);
}
/* Apply one complete unified-diff hunk to a malloc'd file buffer.
* Returns 0 on success. Very small subset: context/add/remove lines. */
static int apply_hunk(char **file, size_t *flen, const char *hunk,
size_t hunk_len) {
/* Find @@ line */
const char *p = hunk;
const char *end = hunk + hunk_len;
while (p < end && !(*p == '@' && p + 1 < end && p[1] == '@')) {
while (p < end && *p != '\n')
p++;
if (p < end)
p++;
}
if (p >= end)
return -1;
while (p < end && *p != '\n')
p++;
if (p < end)
p++;
/* Parse old file into lines */
size_t nlines = 0;
for (size_t i = 0; i < *flen; i++)
if ((*file)[i] == '\n')
nlines++;
if (*flen > 0 && (*file)[*flen - 1] != '\n')
nlines++;
char **lines = calloc(nlines + 64, sizeof(char *));
size_t *llens = calloc(nlines + 64, sizeof(size_t));
if (!lines || !llens) {
free(lines);
free(llens);
return -1;
}
size_t li = 0, start = 0;
for (size_t i = 0; i <= *flen; i++) {
if (i == *flen || (*file)[i] == '\n') {
size_t L = i - start;
lines[li] = malloc(L + 1);
if (!lines[li]) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
memcpy(lines[li], *file + start, L);
lines[li][L] = 0;
llens[li] = L;
li++;
start = i + 1;
}
}
size_t cur = 0;
/* Walk hunk body */
while (p < end) {
char tag = *p;
const char *ls = p + 1;
const char *le = ls;
while (le < end && *le != '\n')
le++;
size_t L = (size_t)(le - ls);
if (tag == ' ' || tag == '-') {
if (cur >= li || llens[cur] != L || memcmp(lines[cur], ls, L) != 0) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
if (tag == '-') {
free(lines[cur]);
memmove(&lines[cur], &lines[cur + 1], (li - cur - 1) * sizeof(char *));
memmove(&llens[cur], &llens[cur + 1], (li - cur - 1) * sizeof(size_t));
li--;
} else {
cur++;
}
} else if (tag == '+') {
char *nl = malloc(L + 1);
if (!nl) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
memcpy(nl, ls, L);
nl[L] = 0;
lines = realloc(lines, (li + 2) * sizeof(char *));
llens = realloc(llens, (li + 2) * sizeof(size_t));
memmove(&lines[cur + 1], &lines[cur], (li - cur) * sizeof(char *));
memmove(&llens[cur + 1], &llens[cur], (li - cur) * sizeof(size_t));
lines[cur] = nl;
llens[cur] = L;
li++;
cur++;
} else if (tag == '\\') {
/* "\ No newline at end of file" — ignore */
} else {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
p = (le < end) ? le + 1 : end;
}
/* rebuild */
size_t total = 0;
for (size_t k = 0; k < li; k++)
total += llens[k] + 1;
char *out = malloc(total + 1);
if (!out) {
for (size_t k = 0; k < li; k++)
free(lines[k]);
free(lines);
free(llens);
return -1;
}
size_t o = 0;
for (size_t k = 0; k < li; k++) {
memcpy(out + o, lines[k], llens[k]);
o += llens[k];
out[o++] = '\n';
free(lines[k]);
}
free(lines);
free(llens);
free(*file);
*file = out;
*flen = o;
return 0;
}
int cst_tool_apply_patch(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *patch = arg_get(call, "patch");
if (!patch)
return set_result(r, "error: missing patch", 1);
/* Full-file replace shorthand */
if (strncmp(patch, "*** Update File: ", 17) == 0) {
const char *nl = strchr(patch, '\n');
if (!nl)
return set_result(r, "error: incomplete Update File header", 1);
char path[1024];
size_t plen = (size_t)(nl - (patch + 17));
if (plen >= sizeof(path))
return set_result(r, "error: path too long", 1);
memcpy(path, patch + 17, plen);
path[plen] = 0;
char full[4096];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return set_result(r, "error: path outside workspace", 1);
size_t olen = 0;
char *oldc = slurp_file(full, &olen);
const char *neu = nl + 1;
size_t nlen = strlen(neu);
ctl_tool_call_t fake = {0};
snprintf(fake.name, sizeof(fake.name), "write_file");
fake.nargs = 2;
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
fake.args[0].value = path;
fake.args[0].value_len = strlen(path);
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
fake.args[1].value = (char *)neu;
fake.args[1].value_len = nlen;
/* write_file already emits diff; free unused oldc */
free(oldc);
return cst_tool_write_file(workspace_root, &fake, r);
}
/* Unified diff: require complete hunks (each @@ ... until next @@ or EOF) */
const char *p = patch;
char *rel = NULL;
/* find +++ b/path */
const char *plus = strstr(p, "\n+++ ");
if (!plus)
plus = (strncmp(p, "+++ ", 4) == 0) ? p : NULL;
if (!plus)
return set_result(r, "error: missing +++ line (incomplete patch)", 1);
const char *ps = plus + (plus[0] == '\n' ? 5 : 4);
if (ps[0] == 'b' && ps[1] == '/')
ps += 2;
const char *pe = strchr(ps, '\n');
if (!pe)
return set_result(r, "error: incomplete +++ line", 1);
size_t rlen = (size_t)(pe - ps);
rel = malloc(rlen + 1);
if (!rel)
return set_result(r, "error: oom", 1);
memcpy(rel, ps, rlen);
rel[rlen] = 0;
char full[4096];
if (cst_sandbox_resolve(workspace_root, rel, full, sizeof(full)) != 0) {
free(rel);
return set_result(r, "error: path outside workspace", 1);
}
char *relpath_for_diff = rel; /* owned until return */
FILE *fp = fopen(full, "rb");
char *file = NULL;
size_t flen = 0;
if (fp) {
fseek(fp, 0, SEEK_END);
long sz = ftell(fp);
rewind(fp);
if (sz < 0) {
fclose(fp);
free(relpath_for_diff);
return set_result(r, "error: ftell", 1);
}
file = malloc((size_t)sz + 1);
if (!file) {
fclose(fp);
free(relpath_for_diff);
return set_result(r, "error: oom", 1);
}
flen = fread(file, 1, (size_t)sz, fp);
fclose(fp);
file[flen] = 0;
} else {
file = calloc(1, 1);
flen = 0;
}
/* Snapshot before mutating hunks. */
char *old_snapshot = NULL;
size_t old_len = flen;
if (flen) {
old_snapshot = malloc(flen + 1);
if (old_snapshot) {
memcpy(old_snapshot, file, flen);
old_snapshot[flen] = 0;
}
} else {
old_snapshot = calloc(1, 1);
old_len = 0;
}
/* Split into hunks starting at @@ */
const char *h = strstr(pe, "\n@@");
if (!h)
h = strstr(pe, "@@");
if (!h) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: no complete hunk", 1);
}
if (*h == '\n')
h++;
while (h && *h == '@') {
const char *next = strstr(h + 2, "\n@@");
size_t hlen;
if (next) {
hlen = (size_t)(next + 1 - h);
} else {
hlen = strlen(h);
}
const char *hnl = strchr(h, '\n');
if (!hnl || hnl >= h + hlen - 1) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: incomplete hunk (not confirmed)", 1);
}
if (apply_hunk(&file, &flen, h, hlen) != 0) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: hunk apply failed", 1);
}
h = next ? next + 1 : NULL;
}
fp = fopen(full, "wb");
if (!fp) {
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: cannot write", 1);
}
if (fwrite(file, 1, flen, fp) != flen) {
fclose(fp);
free(file);
free(old_snapshot);
free(relpath_for_diff);
return set_result(r, "error: write failed", 1);
}
fclose(fp);
/* Patch text is already a diff — show it when short. */
size_t plen = strlen(patch);
if (plen > 0 && plen < DIFF_OUT_CAP) {
char hdr[256];
snprintf(hdr, sizeof(hdr), "applied patch → %s\n", relpath_for_diff);
size_t hlen = strlen(hdr);
char *buf = malloc(hlen + plen + 1);
if (buf) {
memcpy(buf, hdr, hlen);
memcpy(buf + hlen, patch, plen);
buf[hlen + plen] = 0;
cst_tool_result_free(r);
r->out = buf;
r->out_len = hlen + plen;
r->exit_code = 0;
free(file);
free(old_snapshot);
free(relpath_for_diff);
return 0;
}
}
int rc = set_result_with_diff(r, relpath_for_diff, old_snapshot, old_len, file,
flen, 0);
free(file);
free(old_snapshot);
free(relpath_for_diff);
return rc;
}
int cst_tool_dispatch(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
if (!call || !r)
return -1;
memset(r, 0, sizeof(*r));
if (strcmp(call->name, "read_file") == 0)
return cst_tool_read_file(workspace_root, call, r);
if (strcmp(call->name, "run_shell") == 0)
return cst_tool_run_shell(workspace_root, call, r);
if (strcmp(call->name, "apply_patch") == 0)
return cst_tool_apply_patch(workspace_root, call, r);
if (strcmp(call->name, "write_file") == 0)
return cst_tool_write_file(workspace_root, call, r);
return set_result(r, "error: unknown tool", 1);
}
/* Parse JSON string at *pp (must point at '"'); advances *pp past closing quote.
* Returns malloc'd decoded value. */
static char *parse_json_string(const char **pp, size_t *out_len) {
const char *p = *pp;
if (!p || *p != '"')
return NULL;
p++;
size_t cap = 64, len = 0;
char *out = malloc(cap);
if (!out)
return NULL;
while (*p) {
char ch;
if (*p == '\\' && p[1]) {
p++;
if (*p == 'n')
ch = '\n';
else if (*p == 'r')
ch = '\r';
else if (*p == 't')
ch = '\t';
else
ch = *p;
p++;
} else if (*p == '"') {
p++;
out[len] = 0;
*pp = p;
if (out_len)
*out_len = len;
return out;
} else {
ch = *p++;
}
if (len + 2 > cap) {
cap *= 2;
char *nb = realloc(out, cap);
if (!nb) {
free(out);
return NULL;
}
out = nb;
}
out[len++] = ch;
}
free(out);
return NULL;
}
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
const char *name, const char *args_json) {
if (!out || !name || !name[0])
return -1;
memset(out, 0, sizeof(*out));
if (id)
snprintf(out->id, sizeof(out->id), "%s", id);
snprintf(out->name, sizeof(out->name), "%s", name);
if (!args_json)
return 0;
const char *p = args_json;
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
p++;
if (*p != '{')
return 0;
p++;
while (*p) {
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',')
p++;
if (*p == '}' || !*p)
break;
if (*p != '"')
break;
size_t klen = 0;
char *key = parse_json_string(&p, &klen);
if (!key)
break;
while (*p == ' ' || *p == '\t')
p++;
if (*p != ':') {
free(key);
break;
}
p++;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
if (*p == '"') {
size_t vlen = 0;
char *val = parse_json_string(&p, &vlen);
if (!val) {
free(key);
break;
}
if (out->nargs < CTL_MAX_ARGS && klen < CTL_MAX_KEY) {
snprintf(out->args[out->nargs].key, CTL_MAX_KEY, "%s", key);
out->args[out->nargs].value = val;
out->args[out->nargs].value_len = vlen;
out->nargs++;
} else {
free(val);
}
free(key);
} else if (*p == '{' || *p == '[') {
free(key);
char open = *p;
char close = (open == '{') ? '}' : ']';
int depth = 0;
int in_str = 0;
for (; *p; p++) {
if (in_str) {
if (*p == '\\' && p[1]) {
p++;
continue;
}
if (*p == '"')
in_str = 0;
continue;
}
if (*p == '"') {
in_str = 1;
continue;
}
if (*p == open)
depth++;
else if (*p == close) {
depth--;
if (depth == 0) {
p++;
break;
}
}
}
} else {
free(key);
while (*p && *p != ',' && *p != '}')
p++;
}
}
return 0;
}
+2761
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
#include "cst_agent.h"
#include <stdio.h>
#ifdef CST_HEADLESS_ONLY
int cst_tui_run(cst_agent_t *agent) {
(void)agent;
fprintf(stderr,
"TUI was not built into this binary (CST_BUILD_TUI=OFF).\n"
"Use --headless --prompt '...'\n");
return 1;
}
#endif