#include "cst_agent.h" #include "cst_util.h" #include "os_compat.h" #include "cJSON.h" #include #include #include static int settings_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, "settings.json"); } static void copy_str(char *dst, size_t n, const cJSON *v) { if (cJSON_IsString(v) && v->valuestring) snprintf(dst, n, "%s", v->valuestring); } int cst_settings_load(cst_settings_t *s) { memset(s, 0, sizeof(*s)); char path[OS_PATH_MAX]; if (settings_path(path, sizeof(path)) != 0) return -1; FILE *fp = fopen(path, "rb"); if (!fp) return -1; enum { SETTINGS_MAX = 65536 }; char *buf = malloc(SETTINGS_MAX); if (!buf) { fclose(fp); return -1; } size_t n = fread(buf, 1, SETTINGS_MAX - 1, fp); fclose(fp); buf[n] = 0; cJSON *root = cJSON_Parse(buf + cst_bom_len(buf, n)); free(buf); if (!cJSON_IsObject(root)) { cJSON_Delete(root); return -1; } copy_str(s->provider, sizeof(s->provider), cJSON_GetObjectItemCaseSensitive(root, "provider")); copy_str(s->effort, sizeof(s->effort), cJSON_GetObjectItemCaseSensitive(root, "effort")); copy_str(s->mode, sizeof(s->mode), cJSON_GetObjectItemCaseSensitive(root, "mode")); copy_str(s->approval, sizeof(s->approval), cJSON_GetObjectItemCaseSensitive(root, "approval")); copy_str(s->collaboration, sizeof(s->collaboration), cJSON_GetObjectItemCaseSensitive(root, "collaboration")); s->show_thinking = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root, "show_thinking")); const cJSON *tr = cJSON_GetObjectItemCaseSensitive(root, "trusted_workspaces"); const cJSON *w; cJSON_ArrayForEach(w, tr) { if (s->ntrusted < CST_SETTINGS_MAX_TRUSTED && cJSON_IsString(w)) copy_str(s->trusted[s->ntrusted++], sizeof(s->trusted[0]), w); } const cJSON *models = cJSON_GetObjectItemCaseSensitive(root, "models"); const cJSON *m; cJSON_ArrayForEach(m, models) { if (s->nmodels >= CST_SETTINGS_MAX_MODELS) break; if (!m->string || !cJSON_IsString(m)) continue; snprintf(s->models[s->nmodels].provider, sizeof(s->models[s->nmodels].provider), "%s", m->string); copy_str(s->models[s->nmodels].model, sizeof(s->models[s->nmodels].model), m); s->nmodels++; } const cJSON *params = cJSON_GetObjectItemCaseSensitive(root, "params"); const cJSON *p; cJSON_ArrayForEach(p, params) { if (s->nparams >= CST_SETTINGS_MAX_PARAMS || !p->string || !cJSON_IsObject(p)) continue; cst_gen_params_t *g = &s->params[s->nparams]; memset(g, 0, sizeof(*g)); snprintf(g->provider, sizeof(g->provider), "%s", p->string); const cJSON *v = cJSON_GetObjectItemCaseSensitive(p, "temperature"); if (cJSON_IsNumber(v)) g->temperature = v->valuedouble; v = cJSON_GetObjectItemCaseSensitive(p, "top_p"); if (cJSON_IsNumber(v)) g->top_p = v->valuedouble; v = cJSON_GetObjectItemCaseSensitive(p, "max_tokens"); if (cJSON_IsNumber(v)) g->max_tokens = (int)v->valuedouble; s->nparams++; } cJSON_Delete(root); return 0; } int cst_settings_save(const cst_settings_t *s) { char dir[OS_PATH_MAX], path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8]; if (os_config_dir(dir, sizeof(dir)) != 0 || os_mkdir_p(dir, 0700) != 0 || settings_path(path, sizeof(path)) != 0) return -1; if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp)) return -1; cJSON *root = cJSON_CreateObject(); if (s->provider[0]) cJSON_AddStringToObject(root, "provider", s->provider); if (s->effort[0]) cJSON_AddStringToObject(root, "effort", s->effort); if (s->mode[0]) cJSON_AddStringToObject(root, "mode", s->mode); if (s->approval[0]) cJSON_AddStringToObject(root, "approval", s->approval); if (s->collaboration[0]) cJSON_AddStringToObject(root, "collaboration", s->collaboration); if (s->show_thinking) cJSON_AddTrueToObject(root, "show_thinking"); if (s->ntrusted) { cJSON *tr = cJSON_AddArrayToObject(root, "trusted_workspaces"); for (int i = 0; i < s->ntrusted; i++) cJSON_AddItemToArray(tr, cJSON_CreateString(s->trusted[i])); } cJSON *models = cJSON_AddObjectToObject(root, "models"); for (int i = 0; i < s->nmodels; i++) cJSON_AddStringToObject(models, s->models[i].provider, s->models[i].model); if (s->nparams) { cJSON *params = cJSON_AddObjectToObject(root, "params"); for (int i = 0; i < s->nparams; i++) { const cst_gen_params_t *g = &s->params[i]; if (!g->provider[0]) continue; cJSON *o = cJSON_AddObjectToObject(params, g->provider); if (g->temperature > 0) cJSON_AddNumberToObject(o, "temperature", g->temperature); if (g->top_p > 0) cJSON_AddNumberToObject(o, "top_p", g->top_p); if (g->max_tokens > 0) cJSON_AddNumberToObject(o, "max_tokens", g->max_tokens); } } char *text = cJSON_Print(root); cJSON_Delete(root); if (!text) return -1; FILE *fp = fopen(tmp, "wb"); int rc = -1; if (fp) { int ok = fputs(text, fp) >= 0 && fputc('\n', fp) != EOF; if (fclose(fp) == 0 && ok) { #ifdef _WIN32 remove(path); #endif rc = rename(tmp, path) == 0 ? 0 : -1; } if (rc != 0) remove(tmp); } cJSON_free(text); return rc; } static const char *const APPROVAL_NAMES[] = {"ask", "auto", "yolo"}; const char *cst_approval_name(int a) { return a >= 0 && a <= CST_APPROVAL_YOLO ? APPROVAL_NAMES[a] : "auto"; } int cst_approval_parse(const char *s, int *out) { for (int i = 0; s && i <= CST_APPROVAL_YOLO; i++) { if (strcmp(s, APPROVAL_NAMES[i]) == 0) { *out = i; return 0; } } return -1; } static const char *const MODE_NAMES[] = {"coding", "general"}; int cst_mode_valid(const char *m) { for (size_t i = 0; m && m[0] && i < sizeof(MODE_NAMES) / sizeof(*MODE_NAMES); i++) { if (strcmp(m, MODE_NAMES[i]) == 0) return 1; } return 0; } const char *cst_mode_name(const char *m) { return cst_mode_valid(m) ? m : CST_DEFAULT_MODE; } const char *cst_settings_model(const cst_settings_t *s, const char *provider) { for (int i = 0; provider && i < s->nmodels; i++) { if (strcmp(s->models[i].provider, provider) == 0 && s->models[i].model[0]) return s->models[i].model; } return NULL; } int cst_settings_remember(const cst_agent_config_t *cfg) { cst_settings_t s; cst_settings_load(&s); snprintf(s.provider, sizeof(s.provider), "%s", cfg->provider); snprintf(s.effort, sizeof(s.effort), "%s", cfg->effort); if (cfg->mode[0]) snprintf(s.mode, sizeof(s.mode), "%s", cfg->mode); int i = 0; while (i < s.nmodels && strcmp(s.models[i].provider, cfg->provider) != 0) i++; if (i == s.nmodels) { if (s.nmodels >= CST_SETTINGS_MAX_MODELS) return -1; s.nmodels++; } snprintf(s.models[i].provider, sizeof(s.models[i].provider), "%s", cfg->provider); snprintf(s.models[i].model, sizeof(s.models[i].model), "%s", cfg->model); /* The generation parameters ride along with the provider choice. An * all-zero config (the CLI, the TUI) leaves what the GUI saved alone. */ if (cfg->temperature > 0 || cfg->top_p > 0 || cfg->max_tokens > 0) { int pi = 0; while (pi < s.nparams && strcmp(s.params[pi].provider, cfg->provider) != 0) pi++; if (pi == s.nparams) { if (s.nparams >= CST_SETTINGS_MAX_PARAMS) return -1; memset(&s.params[pi], 0, sizeof(s.params[pi])); snprintf(s.params[pi].provider, sizeof(s.params[pi].provider), "%s", cfg->provider); s.nparams++; } s.params[pi].temperature = cfg->temperature; s.params[pi].top_p = cfg->top_p; s.params[pi].max_tokens = cfg->max_tokens; } return cst_settings_save(&s); } const cst_gen_params_t *cst_settings_params(const cst_settings_t *s, const char *provider) { for (int i = 0; s && provider && provider[0] && i < s->nparams; i++) if (strcmp(s->params[i].provider, provider) == 0) return &s->params[i]; return NULL; } int cst_settings_remember_params(const char *provider, const cst_gen_params_t *p) { if (!provider || !provider[0]) return -1; cst_settings_t s; cst_settings_load(&s); int i = 0; while (i < s.nparams && strcmp(s.params[i].provider, provider) != 0) i++; const int forget = !p || (p->temperature <= 0 && p->top_p <= 0 && p->max_tokens <= 0); if (i == s.nparams) { if (forget) return 0; /* nothing remembered, nothing to forget */ if (s.nparams >= CST_SETTINGS_MAX_PARAMS) return -1; memset(&s.params[i], 0, sizeof(s.params[i])); snprintf(s.params[i].provider, sizeof(s.params[i].provider), "%s", provider); s.nparams++; } if (forget) { /* keep the array compact when an entry goes away */ memmove(&s.params[i], &s.params[i + 1], sizeof(s.params[i]) * (s.nparams - i - 1)); s.nparams--; } else { s.params[i].temperature = p->temperature; s.params[i].top_p = p->top_p; s.params[i].max_tokens = p->max_tokens; } return cst_settings_save(&s); } int cst_settings_remember_approval(int approval) { cst_settings_t s; cst_settings_load(&s); snprintf(s.approval, sizeof(s.approval), "%s", cst_approval_name(approval)); return cst_settings_save(&s); } int cst_settings_remember_thinking(int show_thinking) { cst_settings_t s; cst_settings_load(&s); s.show_thinking = show_thinking != 0; return cst_settings_save(&s); } int cst_settings_remember_mode(const char *mode) { cst_settings_t s; cst_settings_load(&s); snprintf(s.mode, sizeof(s.mode), "%s", cst_mode_name(mode)); return cst_settings_save(&s); } static void real_ws(const char *ws, char *out, size_t n) { if (!ws || os_realpath(ws, out, n) != 0) snprintf(out, n, "%s", ws ? ws : ""); } int cst_settings_trusted(const char *workspace) { char real[OS_PATH_MAX]; real_ws(workspace, real, sizeof(real)); cst_settings_t s; cst_settings_load(&s); for (int i = 0; i < s.ntrusted; i++) if (strcmp(s.trusted[i], real) == 0) return 1; return 0; } int cst_settings_trust(const char *workspace, int on) { char real[OS_PATH_MAX]; real_ws(workspace, real, sizeof(real)); if (!real[0] || strlen(real) >= sizeof(((cst_settings_t *)0)->trusted[0])) return -1; cst_settings_t s; cst_settings_load(&s); int k = 0, found = 0; for (int i = 0; i < s.ntrusted; i++) { if (strcmp(s.trusted[i], real) == 0) { found = 1; if (!on) continue; } if (k != i) memcpy(s.trusted[k], s.trusted[i], sizeof(s.trusted[0])); k++; } s.ntrusted = k; if (on && !found) { if (s.ntrusted >= CST_SETTINGS_MAX_TRUSTED) { memmove(s.trusted[0], s.trusted[1], sizeof(s.trusted[0]) * (CST_SETTINGS_MAX_TRUSTED - 1)); s.ntrusted--; } snprintf(s.trusted[s.ntrusted++], sizeof(s.trusted[0]), "%.511s", real); } return cst_settings_save(&s); }