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.
318 lines
8.3 KiB
C
318 lines
8.3 KiB
C
#include "cst_agent.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
const char *p;
|
|
size_t n;
|
|
} line_t;
|
|
|
|
typedef struct {
|
|
line_t *v;
|
|
size_t n;
|
|
size_t cap;
|
|
} lines_t;
|
|
|
|
static int lines_push(lines_t *L, const char *p, size_t n) {
|
|
if (L->n == L->cap) {
|
|
size_t nc = L->cap ? L->cap * 2 : 64;
|
|
line_t *nv = realloc(L->v, nc * sizeof(*nv));
|
|
if (!nv)
|
|
return -1;
|
|
L->v = nv;
|
|
L->cap = nc;
|
|
}
|
|
L->v[L->n].p = p;
|
|
L->v[L->n].n = n;
|
|
L->n++;
|
|
return 0;
|
|
}
|
|
|
|
/* Split into lines without '\n'. A trailing newline does not add an empty
|
|
* final line. */
|
|
static int split_lines(const char *s, size_t n, lines_t *out) {
|
|
size_t start = 0;
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (s[i] == '\n') {
|
|
if (lines_push(out, s + start, i - start) != 0)
|
|
return -1;
|
|
start = i + 1;
|
|
}
|
|
}
|
|
if (start < n && lines_push(out, s + start, n - start) != 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
static size_t rtrim_len(const line_t *l) {
|
|
size_t n = l->n;
|
|
while (n > 0 && (l->p[n - 1] == ' ' || l->p[n - 1] == '\t' ||
|
|
l->p[n - 1] == '\r'))
|
|
n--;
|
|
return n;
|
|
}
|
|
|
|
static int line_eq(const line_t *a, const line_t *b, int loose) {
|
|
if (!loose)
|
|
return a->n == b->n && memcmp(a->p, b->p, a->n) == 0;
|
|
size_t na = rtrim_len(a), nb = rtrim_len(b);
|
|
return na == nb && memcmp(a->p, b->p, na) == 0;
|
|
}
|
|
|
|
static int seq_at(const lines_t *file, size_t pos, const lines_t *seq,
|
|
int loose) {
|
|
if (pos + seq->n > file->n)
|
|
return 0;
|
|
for (size_t i = 0; i < seq->n; i++) {
|
|
if (!line_eq(&file->v[pos + i], &seq->v[i], loose))
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* Find seq in file at or after min_pos. hint<0 means unknown. Returns
|
|
* position, -1 when absent, -2 when ambiguous without a hint. */
|
|
static long locate(const lines_t *file, size_t min_pos, const lines_t *seq,
|
|
long hint, int loose) {
|
|
if (seq->n > file->n)
|
|
return -1;
|
|
size_t last = file->n - seq->n;
|
|
if (hint >= 0) {
|
|
long best = -1;
|
|
long best_d = 0;
|
|
for (size_t p = min_pos; p <= last; p++) {
|
|
if (!seq_at(file, p, seq, loose))
|
|
continue;
|
|
long d = (long)p - hint;
|
|
if (d < 0)
|
|
d = -d;
|
|
if (best < 0 || d < best_d) {
|
|
best = (long)p;
|
|
best_d = d;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
long found = -1;
|
|
for (size_t p = min_pos; p <= last; p++) {
|
|
if (seq_at(file, p, seq, loose)) {
|
|
if (found >= 0)
|
|
return -2;
|
|
found = (long)p;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
static int parse_hunk_header(const char *p, size_t n, long *a, long *b,
|
|
long *c, long *d) {
|
|
/* "@@ -a[,b] +c[,d] @@..." */
|
|
char tmp[128];
|
|
if (n >= sizeof(tmp))
|
|
n = sizeof(tmp) - 1;
|
|
memcpy(tmp, p, n);
|
|
tmp[n] = 0;
|
|
*b = *d = 1;
|
|
if (sscanf(tmp, "@@ -%ld,%ld +%ld,%ld", a, b, c, d) == 4)
|
|
return 0;
|
|
*b = *d = 1;
|
|
if (sscanf(tmp, "@@ -%ld +%ld,%ld", a, c, d) == 3)
|
|
return 0;
|
|
*b = *d = 1;
|
|
if (sscanf(tmp, "@@ -%ld,%ld +%ld", a, b, c) == 3)
|
|
return 0;
|
|
*b = *d = 1;
|
|
if (sscanf(tmp, "@@ -%ld +%ld", a, c) == 2)
|
|
return 0;
|
|
return -1;
|
|
}
|
|
|
|
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
|
size_t hlen, char **out, size_t *out_len,
|
|
int *nhunks, char *err, size_t err_sz) {
|
|
lines_t file = {0}, hl = {0};
|
|
lines_t old_seq = {0}, new_seq = {0};
|
|
int rc = -1;
|
|
int count = 0;
|
|
long delta = 0;
|
|
size_t min_pos = 0;
|
|
int orig_final_nl = olen == 0 || orig[olen - 1] == '\n';
|
|
int final_nl = orig_final_nl;
|
|
if (err && err_sz)
|
|
err[0] = 0;
|
|
if (out)
|
|
*out = NULL;
|
|
|
|
if (split_lines(orig ? orig : "", olen, &file) != 0 ||
|
|
split_lines(hunks ? hunks : "", hlen, &hl) != 0) {
|
|
snprintf(err, err_sz, "oom");
|
|
goto done;
|
|
}
|
|
|
|
size_t i = 0;
|
|
while (i < hl.n) {
|
|
const line_t *h = &hl.v[i];
|
|
if (!(h->n >= 2 && h->p[0] == '@' && h->p[1] == '@')) {
|
|
i++;
|
|
continue;
|
|
}
|
|
long a = -1, b = -1, c = -1, d = -1;
|
|
int have_hint = parse_hunk_header(h->p, h->n, &a, &b, &c, &d) == 0;
|
|
i++;
|
|
old_seq.n = new_seq.n = 0;
|
|
int last_kind = 0; /* ' ', '-', '+' of previous body line */
|
|
int new_no_nl = 0, old_no_nl = 0;
|
|
for (; i < hl.n; i++) {
|
|
const line_t *l = &hl.v[i];
|
|
if (l->n >= 2 && l->p[0] == '@' && l->p[1] == '@')
|
|
break;
|
|
if (l->n == 0) {
|
|
/* models often drop the leading space of blank context lines; a
|
|
* blank line at the very end of the patch is just a terminator */
|
|
if (i + 1 == hl.n)
|
|
break;
|
|
lines_push(&old_seq, l->p, 0);
|
|
lines_push(&new_seq, l->p, 0);
|
|
last_kind = ' ';
|
|
continue;
|
|
}
|
|
char tag = l->p[0];
|
|
if (tag == ' ') {
|
|
lines_push(&old_seq, l->p + 1, l->n - 1);
|
|
lines_push(&new_seq, l->p + 1, l->n - 1);
|
|
} else if (tag == '-') {
|
|
if (l->n >= 4 && memcmp(l->p, "--- ", 4) == 0 && i + 1 < hl.n &&
|
|
hl.v[i + 1].n >= 4 && memcmp(hl.v[i + 1].p, "+++ ", 4) == 0)
|
|
break; /* next file header */
|
|
lines_push(&old_seq, l->p + 1, l->n - 1);
|
|
} else if (tag == '+') {
|
|
lines_push(&new_seq, l->p + 1, l->n - 1);
|
|
} else if (tag == '\\') {
|
|
if (last_kind == '+')
|
|
new_no_nl = 1;
|
|
else if (last_kind == '-')
|
|
old_no_nl = 1;
|
|
else if (last_kind == ' ')
|
|
new_no_nl = old_no_nl = 1;
|
|
continue;
|
|
} else {
|
|
break; /* diff --git, index ..., prose */
|
|
}
|
|
last_kind = tag;
|
|
}
|
|
|
|
long hint = -1;
|
|
if (have_hint) {
|
|
/* "-a,0" inserts after line a; otherwise a is 1-based start */
|
|
hint = (b == 0 ? a : a - 1) + delta;
|
|
if (hint < 0)
|
|
hint = 0;
|
|
}
|
|
|
|
long pos;
|
|
if (old_seq.n == 0) {
|
|
if (hint >= 0)
|
|
pos = hint > (long)file.n ? (long)file.n : hint;
|
|
else if (file.n == 0)
|
|
pos = 0;
|
|
else {
|
|
snprintf(err, err_sz,
|
|
"hunk %d: pure insertion needs @@ -a,0 +c,d @@ line numbers",
|
|
count + 1);
|
|
goto done;
|
|
}
|
|
} else {
|
|
pos = locate(&file, min_pos, &old_seq, hint, 0);
|
|
if (pos == -1)
|
|
pos = locate(&file, min_pos, &old_seq, hint, 1);
|
|
if (pos == -1 && min_pos > 0) {
|
|
pos = locate(&file, 0, &old_seq, hint, 0);
|
|
if (pos == -1)
|
|
pos = locate(&file, 0, &old_seq, hint, 1);
|
|
}
|
|
if (pos == -2) {
|
|
snprintf(err, err_sz,
|
|
"hunk %d: ambiguous, context matches several places; add "
|
|
"line numbers or more context",
|
|
count + 1);
|
|
goto done;
|
|
}
|
|
if (pos < 0) {
|
|
snprintf(err, err_sz,
|
|
"hunk %d: context/removed lines not found in file (first: "
|
|
"\"%.*s\")",
|
|
count + 1, (int)(old_seq.v[0].n > 80 ? 80 : old_seq.v[0].n),
|
|
old_seq.v[0].p);
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* splice: file[pos .. pos+old) -> new_seq */
|
|
size_t old_n = old_seq.n, new_n = new_seq.n;
|
|
size_t need = file.n - old_n + new_n;
|
|
if (need > file.cap) {
|
|
line_t *nv = realloc(file.v, (need + 16) * sizeof(line_t));
|
|
if (!nv) {
|
|
snprintf(err, err_sz, "oom");
|
|
goto done;
|
|
}
|
|
file.v = nv;
|
|
file.cap = need + 16;
|
|
}
|
|
memmove(&file.v[pos + new_n], &file.v[pos + old_n],
|
|
(file.n - (size_t)pos - old_n) * sizeof(line_t));
|
|
for (size_t k = 0; k < new_n; k++)
|
|
file.v[pos + k] = new_seq.v[k];
|
|
int touches_end = (size_t)pos + new_n == need;
|
|
file.n = need;
|
|
delta += (long)new_n - (long)old_n;
|
|
min_pos = (size_t)pos + new_n;
|
|
if (touches_end) {
|
|
if (new_no_nl)
|
|
final_nl = 0;
|
|
else if (old_no_nl)
|
|
final_nl = 1;
|
|
}
|
|
count++;
|
|
}
|
|
|
|
if (count == 0) {
|
|
snprintf(err, err_sz, "no @@ hunk found");
|
|
goto done;
|
|
}
|
|
|
|
{
|
|
size_t total = 1;
|
|
for (size_t k = 0; k < file.n; k++)
|
|
total += file.v[k].n + 1;
|
|
char *buf = malloc(total);
|
|
if (!buf) {
|
|
snprintf(err, err_sz, "oom");
|
|
goto done;
|
|
}
|
|
size_t o = 0;
|
|
for (size_t k = 0; k < file.n; k++) {
|
|
memcpy(buf + o, file.v[k].p, file.v[k].n);
|
|
o += file.v[k].n;
|
|
if (k + 1 < file.n || final_nl)
|
|
buf[o++] = '\n';
|
|
}
|
|
buf[o] = 0;
|
|
*out = buf;
|
|
if (out_len)
|
|
*out_len = o;
|
|
}
|
|
if (nhunks)
|
|
*nhunks = count;
|
|
rc = 0;
|
|
|
|
done:
|
|
free(file.v);
|
|
free(hl.v);
|
|
free(old_seq.v);
|
|
free(new_seq.v);
|
|
return rc;
|
|
}
|