Sessions: rename, delete and search; /api/retry regenerates the last answer, editing a prompt resends from there; /api/export downloads the transcript as Markdown (cst_session_export_md). Session messages carry note/tool_result markers, so a reload rebuilds the tool steps with their arguments, output and exit status; subagent progress is shown. Diff and settings: /api/diff shows the real diff; /api/settings covers protocol, max_steps, tool switches and confirm; /api/permissions edits the rules; sandbox status is reported. Session rename no longer reads past a short title. Commands: slash commands with completion in the composer (/model, /provider, /new, /retry, /undo, /compact, /export, /diff, /init, /skill and more), a Ctrl+P palette, Shift+Tab cycles the approval mode, F1 or ? shows the keys. "Thinking expanded" is saved in the settings (show_thinking), "expand all tool output" in the browser. Extensions: GET /api/extensions lists MCP servers, plugins, skills, agent roles and subagent runs; POST /api/extensions/reload and /api/extensions/trust; a workspace switch reloads them. /api/init and /api/skill run on the server (cst_init_prompt shared with the TUI). Errors, statuses and [cst] notes are shown in Chinese with a readable reason instead of rc=; a failed turn's error survives the reload. Tests: scripts/e2e_webui_sessions.py, e2e_webui_workspace.py and e2e_webui_ext.py, run from e2e_webui.sh.
566 lines
16 KiB
C
566 lines
16 KiB
C
#ifndef _DEFAULT_SOURCE
|
|
#define _DEFAULT_SOURCE
|
|
#endif
|
|
#ifndef _XOPEN_SOURCE
|
|
#define _XOPEN_SOURCE 700
|
|
#endif
|
|
|
|
#include "cst_skills.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <pthread.h>
|
|
|
|
#include "cst_tools.h"
|
|
#include "cst_util.h"
|
|
|
|
#define SKILL_MD_MAX (256 * 1024)
|
|
#define SKILL_FILE_MAX (256 * 1024)
|
|
#define SKILL_LIST_FILES 60
|
|
#define EXTRA_ROOTS 16
|
|
|
|
/* --- frontmatter --------------------------------------------------------- */
|
|
|
|
static const char *skip_bom(const char *s) {
|
|
if ((unsigned char)s[0] == 0xEF && (unsigned char)s[1] == 0xBB &&
|
|
(unsigned char)s[2] == 0xBF)
|
|
return s + 3;
|
|
return s;
|
|
}
|
|
|
|
/* Start of the frontmatter lines and of the closing fence, or NULL. */
|
|
static const char *fm_bounds(const char *text, const char **end) {
|
|
const char *s = skip_bom(text);
|
|
if (strncmp(s, "---", 3) != 0)
|
|
return NULL;
|
|
const char *p = s + 3;
|
|
while (*p == ' ' || *p == '\t' || *p == '\r')
|
|
p++;
|
|
if (*p != '\n')
|
|
return NULL;
|
|
const char *start = ++p;
|
|
while (*p) {
|
|
if (strncmp(p, "---", 3) == 0) {
|
|
const char *q = p + 3;
|
|
while (*q == ' ' || *q == '\t' || *q == '\r')
|
|
q++;
|
|
if (*q == '\n' || !*q) {
|
|
*end = p;
|
|
return start;
|
|
}
|
|
}
|
|
const char *nl = strchr(p, '\n');
|
|
if (!nl)
|
|
break;
|
|
p = nl + 1;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const char *cst_fm_body(const char *text) {
|
|
const char *end = NULL;
|
|
if (!text || !fm_bounds(text, &end))
|
|
return text ? skip_bom(text) : "";
|
|
const char *nl = strchr(end, '\n');
|
|
if (!nl)
|
|
return end + strlen(end);
|
|
nl++;
|
|
while (*nl == '\n' || *nl == '\r')
|
|
nl++;
|
|
return nl;
|
|
}
|
|
|
|
static void trim_into(const char *s, size_t n, char *out, size_t on) {
|
|
while (n && isspace((unsigned char)*s))
|
|
s++, n--;
|
|
while (n && isspace((unsigned char)s[n - 1]))
|
|
n--;
|
|
if (n >= 2 && ((s[0] == '"' && s[n - 1] == '"') ||
|
|
(s[0] == '\'' && s[n - 1] == '\''))) {
|
|
s++;
|
|
n -= 2;
|
|
}
|
|
if (n >= on)
|
|
n = on - 1;
|
|
memcpy(out, s, n);
|
|
out[n] = 0;
|
|
}
|
|
|
|
static void append_part(char *out, size_t n, const char *sep,
|
|
const char *part) {
|
|
size_t used = strlen(out);
|
|
if (used + 1 >= n || !part[0])
|
|
return;
|
|
snprintf(out + used, n - used, "%s%s", used ? sep : "", part);
|
|
}
|
|
|
|
int cst_fm_get(const char *text, const char *key, char *out, size_t n) {
|
|
if (!out || !n)
|
|
return 0;
|
|
out[0] = 0;
|
|
const char *end = NULL;
|
|
const char *p = text ? fm_bounds(text, &end) : NULL;
|
|
if (!p)
|
|
return 0;
|
|
size_t kl = strlen(key);
|
|
while (p < end) {
|
|
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
|
const char *le = nl ? nl : end;
|
|
if ((size_t)(le - p) > kl && strncmp(p, key, kl) == 0 && p[kl] == ':') {
|
|
char val[1024];
|
|
trim_into(p + kl + 1, (size_t)(le - p - kl - 1), val, sizeof(val));
|
|
const char *q = nl ? nl + 1 : end;
|
|
int folded = strcmp(val, ">") == 0 || strcmp(val, ">-") == 0;
|
|
int literal = strcmp(val, "|") == 0 || strcmp(val, "|-") == 0;
|
|
if (val[0] == '[') {
|
|
size_t vl = strlen(val);
|
|
if (vl && val[vl - 1] == ']')
|
|
val[--vl] = 0;
|
|
char *tok = strtok(val + 1, ",");
|
|
while (tok) {
|
|
char item[256];
|
|
trim_into(tok, strlen(tok), item, sizeof(item));
|
|
append_part(out, n, ",", item);
|
|
tok = strtok(NULL, ",");
|
|
}
|
|
return 1;
|
|
}
|
|
if (!folded && !literal && val[0]) {
|
|
snprintf(out, n, "%s", val);
|
|
return 1;
|
|
}
|
|
/* block value: following indented lines (or "- item" list) */
|
|
while (q < end && (*q == ' ' || *q == '\t')) {
|
|
const char *qn = memchr(q, '\n', (size_t)(end - q));
|
|
const char *qe = qn ? qn : end;
|
|
char line[1024];
|
|
trim_into(q, (size_t)(qe - q), line, sizeof(line));
|
|
if (!folded && !literal && line[0] == '-' &&
|
|
(line[1] == ' ' || !line[1])) {
|
|
char item[1024];
|
|
trim_into(line + 1, strlen(line + 1), item, sizeof(item));
|
|
append_part(out, n, ",", item);
|
|
} else {
|
|
append_part(out, n, literal ? "\n" : " ", line);
|
|
}
|
|
q = qn ? qn + 1 : end;
|
|
}
|
|
return 1;
|
|
}
|
|
p = nl ? nl + 1 : end;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* --- discovery ----------------------------------------------------------- */
|
|
|
|
static struct {
|
|
char dir[OS_PATH_MAX];
|
|
char source[48];
|
|
} g_roots[EXTRA_ROOTS];
|
|
static int g_nroots;
|
|
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
|
#define LOCK() pthread_mutex_lock(&g_mu)
|
|
#define UNLOCK() pthread_mutex_unlock(&g_mu)
|
|
|
|
void cst_skills_add_root(const char *dir, const char *source) {
|
|
if (!dir || !dir[0])
|
|
return;
|
|
LOCK();
|
|
int dup = 0;
|
|
for (int i = 0; i < g_nroots; i++)
|
|
dup |= strcmp(g_roots[i].dir, dir) == 0;
|
|
if (!dup && g_nroots < EXTRA_ROOTS) {
|
|
snprintf(g_roots[g_nroots].dir, sizeof(g_roots[0].dir), "%s", dir);
|
|
snprintf(g_roots[g_nroots].source, sizeof(g_roots[0].source), "%s",
|
|
source ? source : "plugin");
|
|
g_nroots++;
|
|
}
|
|
UNLOCK();
|
|
}
|
|
|
|
void cst_skills_clear_roots(const char *prefix) {
|
|
LOCK();
|
|
int k = 0;
|
|
size_t pl = prefix ? strlen(prefix) : 0;
|
|
for (int i = 0; i < g_nroots; i++) {
|
|
if (pl && strncmp(g_roots[i].source, prefix, pl) != 0)
|
|
g_roots[k++] = g_roots[i];
|
|
}
|
|
g_nroots = k;
|
|
UNLOCK();
|
|
}
|
|
|
|
static char *slurp(const char *path, size_t max, size_t *len) {
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp)
|
|
return NULL;
|
|
cst_buf_t b = {0};
|
|
char tmp[8192];
|
|
size_t n;
|
|
int big = 0;
|
|
while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0) {
|
|
if (b.len + n > max) {
|
|
big = 1;
|
|
break;
|
|
}
|
|
cst_buf_append(&b, tmp, n);
|
|
}
|
|
fclose(fp);
|
|
if (big) {
|
|
cst_buf_free(&b);
|
|
return NULL;
|
|
}
|
|
if (!b.buf)
|
|
cst_buf_puts(&b, "");
|
|
cst_strip_bom(b.buf, &b.len);
|
|
if (len)
|
|
*len = b.len;
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
static int skill_cmp(const void *a, const void *b) {
|
|
return strcmp(((const cst_skill_t *)a)->name, ((const cst_skill_t *)b)->name);
|
|
}
|
|
|
|
static int have(const cst_skill_t *v, int n, const char *name) {
|
|
for (int i = 0; i < n; i++)
|
|
if (strcmp(v[i].name, name) == 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int scan_root(const char *root, const char *source, cst_skill_t *out,
|
|
int n, int max) {
|
|
os_dir_t *d = os_dir_open(root);
|
|
if (!d)
|
|
return n;
|
|
const os_dirent_t *de;
|
|
while (n < max && (de = os_dir_next(d)) != NULL) {
|
|
if (de->name[0] == '.')
|
|
continue;
|
|
char dir[OS_PATH_MAX], md[OS_PATH_MAX + 16];
|
|
if (snprintf(dir, sizeof(dir), "%s/%s", root, de->name) >=
|
|
(int)sizeof(dir))
|
|
continue;
|
|
snprintf(md, sizeof(md), "%s/SKILL.md", dir);
|
|
char *text = slurp(md, SKILL_MD_MAX, NULL);
|
|
if (!text)
|
|
continue;
|
|
cst_skill_t s;
|
|
memset(&s, 0, sizeof(s));
|
|
if (!cst_fm_get(text, "name", s.name, sizeof(s.name)) || !s.name[0])
|
|
snprintf(s.name, sizeof(s.name), "%.63s", de->name);
|
|
cst_fm_get(text, "description", s.desc, sizeof(s.desc));
|
|
if (!s.desc[0]) {
|
|
/* first prose line of the body */
|
|
const char *b = cst_fm_body(text);
|
|
for (;;) {
|
|
while (*b == ' ' || *b == '\t' || *b == '\n' || *b == '\r')
|
|
b++;
|
|
if (*b != '#')
|
|
break;
|
|
b += strcspn(b, "\n");
|
|
}
|
|
trim_into(b, strcspn(b, "\r\n"), s.desc, sizeof(s.desc));
|
|
}
|
|
free(text);
|
|
snprintf(s.dir, sizeof(s.dir), "%s", dir);
|
|
snprintf(s.source, sizeof(s.source), "%s", source);
|
|
if (!have(out, n, s.name))
|
|
out[n++] = s;
|
|
}
|
|
os_dir_close(d);
|
|
return n;
|
|
}
|
|
|
|
int cst_skills_list(const char *ws, cst_skill_t *out, int max) {
|
|
if (!out || max <= 0)
|
|
return 0;
|
|
int n = 0;
|
|
char p[OS_PATH_MAX];
|
|
if (ws && ws[0]) {
|
|
snprintf(p, sizeof(p), "%s/.agent/skills", ws);
|
|
n = scan_root(p, "workspace", out, n, max);
|
|
snprintf(p, sizeof(p), "%s/.claude/skills", ws);
|
|
n = scan_root(p, "claude", out, n, max);
|
|
}
|
|
char cfg[OS_PATH_MAX];
|
|
if (os_config_dir(cfg, sizeof(cfg)) == 0) {
|
|
snprintf(p, sizeof(p), "%.4000s/skills", cfg);
|
|
n = scan_root(p, "user", out, n, max);
|
|
}
|
|
LOCK();
|
|
int nr = g_nroots;
|
|
char roots[EXTRA_ROOTS][OS_PATH_MAX], srcs[EXTRA_ROOTS][48];
|
|
for (int i = 0; i < nr; i++) {
|
|
memcpy(roots[i], g_roots[i].dir, sizeof(roots[i]));
|
|
memcpy(srcs[i], g_roots[i].source, sizeof(srcs[i]));
|
|
}
|
|
UNLOCK();
|
|
for (int i = 0; i < nr; i++)
|
|
n = scan_root(roots[i], srcs[i], out, n, max);
|
|
qsort(out, (size_t)n, sizeof(*out), skill_cmp);
|
|
return n;
|
|
}
|
|
|
|
char *cst_skills_index(const char *ws) {
|
|
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
|
if (!v)
|
|
return NULL;
|
|
int n = cst_skills_list(ws, v, CST_SKILLS_MAX);
|
|
if (n == 0) {
|
|
free(v);
|
|
return NULL;
|
|
}
|
|
cst_buf_t b = {0};
|
|
cst_buf_puts(&b, "\nSkills (call skill(name) to load one before working on a "
|
|
"task that matches its description):\n");
|
|
int shown = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
char line[400];
|
|
size_t dl = strlen(v[i].desc);
|
|
size_t keep = cst_utf8_floor(v[i].desc, dl > 240 ? 240 : dl);
|
|
snprintf(line, sizeof(line), "- %s: %.*s%s\n", v[i].name, (int)keep,
|
|
v[i].desc, keep < dl ? "…" : "");
|
|
if (b.len + strlen(line) > CST_SKILL_INDEX_MAX - 64)
|
|
break;
|
|
cst_buf_puts(&b, line);
|
|
shown++;
|
|
}
|
|
if (shown < n)
|
|
cst_buf_printf(&b, "- … %d more (see /skills)\n", n - shown);
|
|
free(v);
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
/* --- loading ------------------------------------------------------------- */
|
|
|
|
static int find_skill(const char *ws, const char *name, cst_skill_t *out) {
|
|
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
|
if (!v)
|
|
return 0;
|
|
int n = cst_skills_list(ws, v, CST_SKILLS_MAX), hit = 0;
|
|
for (int i = 0; i < n && !hit; i++) {
|
|
if (strcmp(v[i].name, name) == 0) {
|
|
*out = v[i];
|
|
hit = 1;
|
|
}
|
|
}
|
|
free(v);
|
|
return hit;
|
|
}
|
|
|
|
static void list_files(const char *dir, const char *rel, int depth,
|
|
cst_buf_t *b, int *count) {
|
|
char full[OS_PATH_MAX];
|
|
if (snprintf(full, sizeof(full), "%s%s%s", dir, rel[0] ? "/" : "", rel) >=
|
|
(int)sizeof(full))
|
|
return;
|
|
os_dir_t *d = os_dir_open(full);
|
|
if (!d)
|
|
return;
|
|
char *names[256];
|
|
int nn = 0;
|
|
const os_dirent_t *de;
|
|
while (nn < 256 && (de = os_dir_next(d)) != NULL) {
|
|
if (de->name[0] == '.')
|
|
continue;
|
|
names[nn] = strdup(de->name);
|
|
if (names[nn])
|
|
nn++;
|
|
}
|
|
os_dir_close(d);
|
|
for (int i = 1; i < nn; i++) {
|
|
for (int j = i; j > 0 && strcmp(names[j - 1], names[j]) > 0; j--) {
|
|
char *tmp = names[j];
|
|
names[j] = names[j - 1];
|
|
names[j - 1] = tmp;
|
|
}
|
|
}
|
|
for (int i = 0; i < nn; i++) {
|
|
char crel[OS_PATH_MAX], cfull[OS_PATH_MAX + 2];
|
|
snprintf(crel, sizeof(crel), "%s%s%.255s", rel, rel[0] ? "/" : "",
|
|
names[i]);
|
|
snprintf(cfull, sizeof(cfull), "%s/%s", dir, crel);
|
|
struct stat st;
|
|
if (stat(cfull, &st) != 0)
|
|
continue;
|
|
if (S_ISDIR(st.st_mode)) {
|
|
if (depth < 3)
|
|
list_files(dir, crel, depth + 1, b, count);
|
|
} else if (strcmp(crel, "SKILL.md") != 0) {
|
|
if (*count < SKILL_LIST_FILES)
|
|
cst_buf_printf(b, "- %s\n", crel);
|
|
(*count)++;
|
|
}
|
|
}
|
|
for (int i = 0; i < nn; i++)
|
|
free(names[i]);
|
|
}
|
|
|
|
static char *errf(int *code, const char *fmt, const char *a, const char *b) {
|
|
char msg[1200];
|
|
snprintf(msg, sizeof(msg), fmt, a, b);
|
|
*code = 1;
|
|
return strdup(msg);
|
|
}
|
|
|
|
static char *names_list(const char *ws) {
|
|
cst_skill_t *v = calloc(CST_SKILLS_MAX, sizeof(*v));
|
|
cst_buf_t b = {0};
|
|
int n = v ? cst_skills_list(ws, v, CST_SKILLS_MAX) : 0;
|
|
for (int i = 0; i < n; i++)
|
|
cst_buf_printf(&b, "%s%s", i ? ", " : "", v[i].name);
|
|
free(v);
|
|
if (!b.buf)
|
|
cst_buf_puts(&b, "(none)");
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
char *cst_skill_read(const char *ws, const char *name, const char *file,
|
|
int *exit_code) {
|
|
int dummy;
|
|
if (!exit_code)
|
|
exit_code = &dummy;
|
|
*exit_code = 0;
|
|
cst_skill_t s;
|
|
if (!name || !name[0] || !find_skill(ws, name, &s)) {
|
|
char *names = names_list(ws);
|
|
char *r = errf(exit_code, "error: no skill named \"%.100s\" (available: %.900s)",
|
|
name ? name : "", names ? names : "");
|
|
free(names);
|
|
return r;
|
|
}
|
|
if (file && file[0]) {
|
|
char want[OS_PATH_MAX * 2], real[OS_PATH_MAX], root[OS_PATH_MAX];
|
|
snprintf(want, sizeof(want), "%s/%s", s.dir, file);
|
|
if (os_realpath(s.dir, root, sizeof(root)) != 0 ||
|
|
os_realpath(want, real, sizeof(real)) != 0)
|
|
return errf(exit_code, "error: skill %s has no file %.300s", s.name,
|
|
file);
|
|
if (!os_path_under(root, real))
|
|
return errf(exit_code, "error: %.300s is outside the skill %s directory",
|
|
file, s.name);
|
|
size_t n = 0;
|
|
char *body = slurp(real, SKILL_FILE_MAX, &n);
|
|
if (!body)
|
|
return errf(exit_code, "error: cannot read %.300s of skill %s (missing, "
|
|
"a directory, or over 256 KiB)", file, s.name);
|
|
if (memchr(body, 0, n)) {
|
|
free(body);
|
|
return errf(exit_code, "error: %.300s of skill %s is binary", file,
|
|
s.name);
|
|
}
|
|
cst_buf_t b = {0};
|
|
cst_buf_printf(&b, "skill %s · %s\n", s.name, file);
|
|
cst_buf_append(&b, body, n);
|
|
free(body);
|
|
return cst_buf_steal(&b);
|
|
}
|
|
char md[OS_PATH_MAX + 16];
|
|
snprintf(md, sizeof(md), "%s/SKILL.md", s.dir);
|
|
char *text = slurp(md, SKILL_MD_MAX, NULL);
|
|
if (!text)
|
|
return errf(exit_code, "error: cannot read %.300s%.1s", md, "");
|
|
cst_buf_t b = {0};
|
|
cst_buf_printf(&b, "skill %s (%s) · %s\n\n", s.name, s.source, s.dir);
|
|
cst_buf_puts(&b, cst_fm_body(text));
|
|
free(text);
|
|
if (b.len && b.buf[b.len - 1] != '\n')
|
|
cst_buf_puts(&b, "\n");
|
|
cst_buf_t files = {0};
|
|
int count = 0;
|
|
list_files(s.dir, "", 0, &files, &count);
|
|
if (count) {
|
|
cst_buf_printf(&b, "\nFiles in this skill (read with skill(name=\"%s\", "
|
|
"file=...); paths are relative to %s):\n",
|
|
s.name, s.dir);
|
|
cst_buf_append(&b, files.buf, files.len);
|
|
if (count > SKILL_LIST_FILES)
|
|
cst_buf_printf(&b, "- … %d more\n", count - SKILL_LIST_FILES);
|
|
}
|
|
cst_buf_free(&files);
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
const char *cst_init_prompt(void) {
|
|
return "Analyze this repository and write an AGENTS.md file at the "
|
|
"workspace root for future coding agents. Cover: what the project "
|
|
"is, how to build, test and run it (exact commands), the directory "
|
|
"layout, coding conventions you can infer from the code, and "
|
|
"anything surprising. Explore with list_dir/glob/grep/read_file "
|
|
"before writing. Keep it under 150 lines. If AGENTS.md already "
|
|
"exists, improve it rather than starting over.";
|
|
}
|
|
|
|
char *cst_skill_prompt(const char *ws, const char *name, const char *task) {
|
|
int code = 0;
|
|
char *body = cst_skill_read(ws, name, NULL, &code);
|
|
if (!body || code != 0) {
|
|
free(body);
|
|
return NULL;
|
|
}
|
|
cst_buf_t b = {0};
|
|
cst_buf_printf(&b, "Use the \"%s\" skill for this task. Its instructions "
|
|
"(already loaded):\n\n<skill>\n",
|
|
name);
|
|
cst_buf_puts(&b, body);
|
|
cst_buf_puts(&b, "</skill>\n\n");
|
|
cst_buf_puts(&b, task && task[0] ? task : "Apply this skill now.");
|
|
free(body);
|
|
return cst_buf_steal(&b);
|
|
}
|
|
|
|
/* --- tool ---------------------------------------------------------------- */
|
|
|
|
static const char *arg_of(const ctl_tool_call_t *c, const char *key) {
|
|
for (int i = 0; i < c->nargs; i++)
|
|
if (strcmp(c->args[i].key, key) == 0)
|
|
return c->args[i].value ? c->args[i].value : "";
|
|
return NULL;
|
|
}
|
|
|
|
static int skill_tool(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
|
cst_tool_result_t *r, void *ud) {
|
|
(void)ud;
|
|
int code = 0;
|
|
char *out = cst_skill_read(ctx->workspace_root, arg_of(c, "name"),
|
|
arg_of(c, "file"), &code);
|
|
r->out = out ? out : strdup("error: out of memory");
|
|
r->out_len = r->out ? strlen(r->out) : 0;
|
|
r->exit_code = out ? code : 1;
|
|
return 0;
|
|
}
|
|
|
|
static int always(const ctl_tool_call_t *c, void *ud) {
|
|
(void)c;
|
|
(void)ud;
|
|
return 1;
|
|
}
|
|
|
|
int cst_skills_register_tool(void) {
|
|
if (cst_tool_find("skill"))
|
|
return 0;
|
|
cst_tool_def_t d;
|
|
memset(&d, 0, sizeof(d));
|
|
d.name = "skill";
|
|
d.desc = "Load a skill from the Skills list: its instructions (SKILL.md) "
|
|
"and the list of its files. Load the matching skill before "
|
|
"starting such a task; pass file to read one of its files.";
|
|
d.schema_json =
|
|
"{\"type\":\"object\",\"properties\":{"
|
|
"\"name\":{\"type\":\"string\",\"description\":\"skill name\"},"
|
|
"\"file\":{\"type\":\"string\",\"description\":\"a file inside the "
|
|
"skill directory (optional)\"}},\"required\":[\"name\"]}";
|
|
d.perm = CST_TP_META;
|
|
d.run_ex = skill_tool;
|
|
d.parallel = always;
|
|
d.owner = "builtin:skills";
|
|
return cst_tool_register(&d);
|
|
}
|