Extensions: skills, plugins, MCP, sub-agents; configurable keys
- Skills (SKILL.md discovery, skill tool, /skills /skill) - plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins) - MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status - task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents - Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel - Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools - Spawn: close-on-exec pipes and a process-wide fork lock - Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
+577
@@ -0,0 +1,577 @@
|
||||
#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>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#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;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define LOCK() pthread_mutex_lock(&g_mu)
|
||||
#define UNLOCK() pthread_mutex_unlock(&g_mu)
|
||||
#else
|
||||
#define LOCK() ((void)0)
|
||||
#define UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
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, "");
|
||||
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) {
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
if (!d)
|
||||
return n;
|
||||
struct dirent *de;
|
||||
while (n < max && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
continue;
|
||||
char dir[OS_PATH_MAX], md[OS_PATH_MAX + 16];
|
||||
if (snprintf(dir, sizeof(dir), "%s/%s", root, de->d_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->d_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;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)root;
|
||||
(void)source;
|
||||
(void)out;
|
||||
(void)max;
|
||||
#endif
|
||||
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) {
|
||||
#ifndef _WIN32
|
||||
char full[OS_PATH_MAX];
|
||||
if (snprintf(full, sizeof(full), "%s%s%s", dir, rel[0] ? "/" : "", rel) >=
|
||||
(int)sizeof(full))
|
||||
return;
|
||||
DIR *d = opendir(full);
|
||||
if (!d)
|
||||
return;
|
||||
char *names[256];
|
||||
int nn = 0;
|
||||
struct dirent *de;
|
||||
while (nn < 256 && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
continue;
|
||||
names[nn] = strdup(de->d_name);
|
||||
if (names[nn])
|
||||
nn++;
|
||||
}
|
||||
closedir(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]);
|
||||
#else
|
||||
(void)dir;
|
||||
(void)rel;
|
||||
(void)depth;
|
||||
(void)b;
|
||||
(void)count;
|
||||
#endif
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user