Tools - Table-driven tool registry with permission classes and default/minimal profiles; new tools: edit_file, list_dir, glob, grep, delete_file, move_file, todo_write, web_fetch, ask_user (plus read_file paging). - Built-in Myers diff; write tools return unified diffs. - Persistent permission rules (.agent/permissions.json), per-turn undo snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80% of the model context, cancellable run_shell with process-group kill. TUI - Table-driven slash commands: /provider /key /effort /status /tools /permissions /yolo /undo /retry /diff /export /copy /rename /delete /init /max-steps /todo, with / and @ completion. - Permission modal with diff and prefix rules, ask_user modal, todo panel, header with model/effort/context, bracketed paste, line editing, wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits. Startup and configuration - API keys are entered in the TUI (hidden) and saved per provider in <config>/credentials (0600); env variables are only a fallback. - Last provider, per-provider model and reasoning effort are remembered in <config>/settings.json; --effort/-e and /effort send reasoning_effort (low|medium|high|max) to models that accept it. - DeepSeek preset uses deepseek-flash / deepseek-v4-pro. - Session flags --new/--continue/--resume/--list-sessions, --yolo, --confirm. Compatibility and tests - ncurses detection, macOS/Homebrew support, make install, XDG config dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS). - Unit tests for diff, fs tools, registry, undo/perms and config; e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.
143 lines
4.1 KiB
C
143 lines
4.1 KiB
C
#include "cst_agent.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define MODEL_MAX_LEN 128
|
|
|
|
static const cst_provider_t PROVIDERS[] = {
|
|
{"minimax", "https://api.minimax.cn/v1", "MiniMax-M2.7", "MINIMAX_API_KEY"},
|
|
{"deepseek", "https://api.deepseek.com/v1", "deepseek-flash",
|
|
"DEEPSEEK_API_KEY"},
|
|
{"openai", "https://api.openai.com/v1", "gpt-4.1-mini", "OPENAI_API_KEY"},
|
|
};
|
|
|
|
const cst_provider_t *cst_provider_find(const char *name) {
|
|
if (!name || !name[0])
|
|
return NULL;
|
|
for (size_t i = 0; i < sizeof(PROVIDERS) / sizeof(PROVIDERS[0]); i++) {
|
|
if (strcmp(PROVIDERS[i].name, name) == 0)
|
|
return &PROVIDERS[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const cst_provider_t *cst_provider_default(void) {
|
|
const cst_provider_t *p = cst_provider_find(getenv("CST_PROVIDER"));
|
|
return p ? p : &PROVIDERS[0];
|
|
}
|
|
|
|
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;
|
|
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, "deepseek-flash");
|
|
push(out, &n, max_out, "deepseek-v4-pro");
|
|
push(out, &n, max_out, "MiniMax-M2.7");
|
|
push(out, &n, max_out, "MiniMax-M2.5");
|
|
push(out, &n, max_out, "gpt-4.1-mini");
|
|
push(out, &n, max_out, "gpt-4o");
|
|
}
|
|
|
|
if (current && current[0])
|
|
push(out, &n, max_out, current);
|
|
|
|
return n;
|
|
}
|
|
|
|
/* Context window in tokens (CST_CTX_TOKENS overrides). Prefix match. */
|
|
long cst_model_context_tokens(const char *model) {
|
|
const char *e = getenv("CST_CTX_TOKENS");
|
|
if (e && atol(e) > 0)
|
|
return atol(e);
|
|
static const struct {
|
|
const char *prefix;
|
|
long tokens;
|
|
} T[] = {{"deepseek", 128000}, {"MiniMax", 196608}, {"minimax", 196608},
|
|
{"gpt-4.1", 1047576}, {"gpt-4o", 128000}, {"gpt-5", 400000},
|
|
{"o3", 200000}, {"o4", 200000}, {"claude", 200000},
|
|
{"qwen", 131072}, {"glm", 128000}, {"kimi", 131072}};
|
|
if (model) {
|
|
for (size_t i = 0; i < sizeof(T) / sizeof(T[0]); i++) {
|
|
if (strncmp(model, T[i].prefix, strlen(T[i].prefix)) == 0)
|
|
return T[i].tokens;
|
|
}
|
|
}
|
|
return 128000;
|
|
}
|
|
|
|
int cst_effort_valid(const char *effort) {
|
|
static const char *const ok[] = {"low", "medium", "high", "max"};
|
|
for (size_t i = 0; effort && i < sizeof(ok) / sizeof(ok[0]); i++) {
|
|
if (strcmp(effort, ok[i]) == 0)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const char *cst_effort_wire(const char *provider, const char *model,
|
|
const char *effort) {
|
|
if (!cst_effort_valid(effort) || !provider || !model)
|
|
return NULL;
|
|
if (strcmp(provider, "deepseek") == 0)
|
|
return effort;
|
|
/* OpenAI reasoning models take low | medium | high. */
|
|
if (strcmp(provider, "openai") == 0 &&
|
|
(strncmp(model, "o1", 2) == 0 || strncmp(model, "o3", 2) == 0 ||
|
|
strncmp(model, "o4", 2) == 0 || strncmp(model, "gpt-5", 5) == 0))
|
|
return strcmp(effort, "max") == 0 ? "high" : effort;
|
|
return NULL;
|
|
}
|
|
|
|
int cst_provider_count(void) {
|
|
return (int)(sizeof(PROVIDERS) / sizeof(PROVIDERS[0]));
|
|
}
|
|
|
|
const cst_provider_t *cst_provider_at(int i) {
|
|
return i >= 0 && i < cst_provider_count() ? &PROVIDERS[i] : NULL;
|
|
}
|