#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #endif #include "cst_skills.h" #include #include #include #include #include #ifndef _WIN32 #include #include #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\n", name); cst_buf_puts(&b, body); cst_buf_puts(&b, "\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); }