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:
+1182
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user