From 7d3ec9fcb9eb7fa14f73b2b5b96e3d8ca2d3060d Mon Sep 17 00:00:00 2001 From: WHC Date: Mon, 5 Oct 2026 14:55:31 +0800 Subject: [PATCH] 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 --- CMakeLists.txt | 10 +- Makefile | 11 +- README.md | 61 ++- docs/plugins.md | 186 +++++++ include/cst_agent.h | 21 +- include/cst_ext.h | 31 ++ include/cst_keymap.h | 134 +++++ include/cst_mcp.h | 66 +++ include/cst_plugins.h | 51 ++ include/cst_skills.h | 62 +++ include/cst_subagent.h | 65 +++ include/cst_tools.h | 40 ++ include/os_compat.h | 32 +- src/agent_loop.c | 694 ++++++++++++++++++++++---- src/agent_store.c | 15 + src/extensions.c | 89 ++++ src/keymap.c | 502 +++++++++++++++++++ src/llm_openai.c | 7 +- src/main.c | 10 + src/mcp.c | 936 +++++++++++++++++++++++++++++++++++ src/os_compat.c | 381 +++++++++++++- src/plugins.c | 444 +++++++++++++++++ src/settings.c | 70 ++- src/skills.c | 577 ++++++++++++++++++++++ src/subagent.c | 703 ++++++++++++++++++++++++++ src/tools/fs_tools.c | 422 ++++++++++++---- src/tools/registry.c | 387 ++++++++++++--- src/tools/tools.c | 164 ++++++ src/tui.c | 986 ++++++++++++++++++++++++++++++++----- tests/test_keymap.c | 126 +++++ tests/test_mcp.c | 297 +++++++++++ tests/test_plugins.c | 219 ++++++++ tests/test_registry.c | 97 +++- tests/test_shell_compact.c | 116 +++++ tests/test_skills.c | 193 ++++++++ tests/test_subagent.c | 484 ++++++++++++++++++ tests/test_tools_fs.c | 59 ++- vendor/cjson/cJSON.c | 7 + 38 files changed, 8356 insertions(+), 399 deletions(-) create mode 100644 docs/plugins.md create mode 100644 include/cst_ext.h create mode 100644 include/cst_keymap.h create mode 100644 include/cst_mcp.h create mode 100644 include/cst_plugins.h create mode 100644 include/cst_skills.h create mode 100644 include/cst_subagent.h create mode 100644 src/extensions.c create mode 100644 src/keymap.c create mode 100644 src/mcp.c create mode 100644 src/plugins.c create mode 100644 src/skills.c create mode 100644 src/subagent.c create mode 100644 tests/test_keymap.c create mode 100644 tests/test_mcp.c create mode 100644 tests/test_plugins.c create mode 100644 tests/test_shell_compact.c create mode 100644 tests/test_skills.c create mode 100644 tests/test_subagent.c diff --git a/CMakeLists.txt b/CMakeLists.txt index e219d51..c192efd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,12 @@ set(CST_CORE_SOURCES src/os_compat.c src/sandbox.c src/sandbox_exec.c + src/keymap.c + src/skills.c + src/extensions.c + src/plugins.c + src/mcp.c + src/subagent.c src/session.c src/tools/tools.c src/tools/patch.c @@ -167,7 +173,9 @@ endforeach() # Tests that link the whole core (no TUI). foreach(t test_llm_body test_sse_decode test_apply_patch test_session_roundtrip test_shell test_diff test_tools_fs test_registry - test_undo test_config test_models test_sandbox) + test_undo test_config test_models test_sandbox + test_keymap test_shell_compact test_skills + test_plugins test_mcp test_subagent) add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES}) target_include_directories(${t} PRIVATE include "${CJSON_DIR}") target_compile_definitions(${t} PRIVATE _GNU_SOURCE) diff --git a/Makefile b/Makefile index d340c53..b572109 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,9 @@ CORE_SRC = \ src/os_compat.c \ src/sandbox.c \ src/sandbox_exec.c \ + src/keymap.c \ + src/skills.c \ + src/extensions.c src/plugins.c src/mcp.c src/subagent.c \ src/session.c \ src/tools/tools.c \ src/tools/patch.c \ @@ -99,7 +102,13 @@ TESTS = \ tests/test_undo \ tests/test_config \ tests/test_models \ - tests/test_sandbox + tests/test_sandbox \ + tests/test_keymap \ + tests/test_shell_compact \ + tests/test_skills \ + tests/test_plugins \ + tests/test_mcp \ + tests/test_subagent TEST_LIBS = -lm $(CURL_LIBS) $(PTHREAD_LIBS) diff --git a/README.md b/README.md index 37514e7..40a5461 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,11 @@ cst-agent --headless --prompt 'List files' | `web_fetch` | 网络 | 抓取 URL,HTML 转纯文本 | | `todo_write` | 元 | 维护任务清单(TUI 右侧面板,Ctrl+T 开关) | | `ask_user` | 询问 | 向用户提问(可给选项);headless 下返回错误让模型自行决定 | +| `skill` | 元 | 加载一个 skill 的说明(`SKILL.md`)或其中的文件;有 skill 时才出现 | +| `task` | 子 agent | 把独立任务交给子 agent(`general` / `explore` / `plan` / 自定义角色),只返回最终报告 | + +插件工具和 MCP 工具(`mcp____`)在运行时注册,和内置工具走同一套审批与 `/tools` 开关。 +同一步里的多个只读调用(读文件、搜索、只读子 agent)会并行执行(最多 4 个)。 `--tools minimal` 只保留 `read_file` / `run_shell` / `write_file` / `apply_patch`(论文实验口径)。 TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 @@ -281,8 +286,29 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 - **AGENTS.md**:工作区根目录的 `AGENTS.md` 会注入 system prompt;`/init` 让 agent 自己生成一份。 - **自动压缩**:上一轮 prompt 用到模型上下文 80% 时,下一轮开始前自动总结历史(`CST_AUTO_COMPACT=0` 关闭)。 +### 扩展:Skills、插件、MCP、子 agent + +`--tools minimal` 下这些都不加载(论文口径保持不变)。配置写法与示例见 [docs/plugins.md](docs/plugins.md)。 + +- **Skills**:`/SKILL.md`(frontmatter `name` + `description`),放在工作区 `.agent/skills/`、`.claude/skills/` + 或配置目录 `skills/`。system prompt 只放名字和一句描述,模型需要时调用 `skill(name)` 读全文; + `/skills` 浏览,`/skill NAME 任务` 直接带着某个 skill 开始一轮。 +- **插件**:`plugins//plugin.json` 声明外部命令工具(参数以 JSON 写到 stdin,stdout 即结果,`perm` 决定审批, + 默认 60 s 超时),也可以附带 skills 目录和 MCP 服务器。 +- **MCP**:`mcp.json`(`{"mcpServers": {...}}`,与 Claude/Cursor 格式相同)里的 stdio 服务器在后台启动, + 工具注册为 `mcp____`,默认按网络工具审批(`autoApprove` 可放行)。`/mcp` 看状态,侧栏显示连接数。 +- **工作区信任**:工作区里的 `.agent/plugins/` 和 `.agent/mcp.json` 会执行命令,默认不加载; + `/plugins trust` 后才生效(记在 `settings.json`)。配置目录(`~/.config/cst-agent/`)下的始终加载。 +- **子 agent**:`task(description, prompt, agent?)` 新开一个独立上下文跑完一轮,把最终报告交回主 agent。 + 内置 `general`(除 task 外全部工具)、`explore`(只读,可并行)、`plan`(只读,输出计划); + `.agent/agents/*.md` 或配置目录 `agents/*.md` 可自定义角色(frontmatter `name / description / model / tools / + max_steps / readonly`,正文是角色说明)。子 agent 不能再开子 agent;Esc 会一并取消;它的审批弹窗标 `[角色名]`; + 文件改动并入当前轮,`/undo` 一起撤销。转录在 `.agent/sessions/<会话>/sub-N.jsonl`,`/agents` 查看。 + ### TUI 快捷键 +默认 leader 是 **Ctrl+X**:先按 Ctrl+X,再按后面的字母。`/keys`(或 F1、Ctrl+X h)列出当前全部绑定。 + | 按键 | 说明 | |---|---| | Enter | 发送;busy 时入队 | @@ -293,12 +319,37 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 | Tab | 补全 `/命令` 和 `@文件`;没有可补全时切换 Build / Plan(Plan 只开放只读工具) | | Shift+Tab | 循环切换审批模式 ask → auto → yolo | | Ctrl+P | 命令面板(输入过滤,↑↓ / 鼠标选择) | -| Ctrl+O / Ctrl+T | 展开最近 tool 输出 / 开关任务面板 | -| PgUp / PgDn / 鼠标滚轮 | 滚动对话 | +| Ctrl+O / Ctrl+R | 展开/折叠最近的 tool 输出 / 最近的 Thought | +| Ctrl+T / Ctrl+L | 开关任务面板 / 重绘 | +| PgUp / PgDn、Alt+↑ / Alt+↓、Ctrl+Home / Ctrl+End | 半页滚动、单行滚动、跳到开头 / 最新 | +| Ctrl+X n / l | 新会话 / 会话列表 | +| Ctrl+X m / p / e | 模型 / provider / 推理强度 | +| Ctrl+X u / r / c | 撤销上一轮改动 / 重问 / 压缩上下文 | +| Ctrl+X y / x | 复制上一条回复 / 导出记录 | +| Ctrl+X t / d / s | 全部 Thought 默认展开或折叠 / 全部 tool 输出 / 侧栏 | +| Ctrl+X a / k / g | 子 agent 面板 / skills / 插件 | +| Ctrl+X h、F1 / Ctrl+X q | 快捷键列表 / 退出 | | 鼠标点击 | 展开/折叠 tool 卡片和 Thought、在输入框里定位光标、点模式行切换 Build/Plan、在选择框里选中(`/mouse` 关闭后可用终端原生选择复制) | | Esc | 取消当前 turn(不会退出) | | Ctrl+C 两次 / 空输入 Ctrl+D | 退出 | +Thought(推理过程)默认折叠成一行“Thought · 3.2s”,生成中显示实时计时;`/thinking on` 改为默认展开。 + +**自定义按键**:`~/.config/cst-agent/keybinds.json`,键是动作名(`/keys` 里能看到),值是一个或多个按键, +`"none"` 解除绑定: + +```json +{ + "leader": "ctrl+x", + "new_session": [" n", "f2"], + "toggle_thought": "ctrl+g", + "todo": "none" +} +``` + +按键写法:`ctrl+x`、`alt+b`、`shift+tab`、`f1`…`f12`、`pgup`、`home`、` n`。 +写错的条目会被跳过并在 `/keys` 里提示,其余照常生效。 + ### 斜杠命令 输入 `/` 弹出补全;唯一前缀即可(如 `/st` → `/status`)。标 * 的命令在模型运行中也能用。 @@ -320,7 +371,11 @@ TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。 | `/compact` | 手动压缩上下文 | | `/export [file]` *、`/copy` * | 导出 Markdown 记录;复制上一条回复(OSC 52,需终端支持) | | `/init` | 让 agent 写 `AGENTS.md` | -| `/details` *、`/quit` * | 展开全部 tool 输出;退出 | +| `/details` *、`/thinking [on\|off]`、`/keys` * | 展开全部 tool 输出;Thought 默认展开/折叠;快捷键列表 | +| `/skills [reload\|list]`、`/skill [任务]` | 浏览 skills;带着某个 skill 开始一轮 | +| `/plugins [trust\|untrust\|reload]`、`/mcp [reload]` | 插件与工作区信任;MCP 服务器状态 | +| `/agents` | 子 agent 角色与本会话的子 agent 记录(可查看转录) | +| `/quit` * | 退出 | ### 协议 diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..d356143 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,186 @@ +# 扩展:Skills、插件、MCP 与自定义子 agent + +所有扩展只在默认工具档(`--tools default`)下加载;`--tools minimal` 完全不受影响。 +下文的“配置目录”指 `~/.config/cst-agent/`(`$XDG_CONFIG_HOME/cst-agent/`;Windows 为 `%APPDATA%\cst-agent\`)。 + +| 放在哪 | 什么 | 何时加载 | +|---|---|---| +| `/.agent/skills/*/SKILL.md`、`/.claude/skills/*/SKILL.md` | skill | 总是 | +| `<配置目录>/skills/*/SKILL.md` | skill | 总是 | +| `/.agent/plugins/*/plugin.json` | 插件 | 工作区被信任后(`/plugins trust`) | +| `<配置目录>/plugins/*/plugin.json` | 插件 | 总是 | +| `/.agent/mcp.json` | MCP 服务器 | 工作区被信任后 | +| `<配置目录>/mcp.json` | MCP 服务器 | 总是 | +| `/.agent/agents/*.md`、`<配置目录>/agents/*.md` | 子 agent 角色 | 总是 | + +同名时先出现的生效:工作区 > 配置目录 > 插件。改完后 `/skills reload`、`/plugins reload` 或 `/mcp reload` +重新加载(会重启 MCP 服务器)。启动时首页和 headless 的 stderr 会打印一行汇总,例如 +`2 skills · 1 plugin (3 tools) · 1 MCP server`。 + +## Skills + +一个 skill 是一个目录,里面有 `SKILL.md`,可以附带脚本、参考资料等文件: + +``` +.agent/skills/pdf-forms/ +├── SKILL.md +├── reference.md +└── scripts/fill.py +``` + +```markdown +--- +name: pdf-forms +description: Fill PDF forms with the bundled script; use when the user asks to fill or inspect a PDF form. +--- +# PDF forms + +1. List the fields: `python3 scripts/fill.py --list form.pdf` +2. ... +``` + +- system prompt 里只有一张索引(`- pdf-forms: Fill PDF forms…`,总长 ≤ 4 KB),模型认为相关时调用 + `skill(name)` 读正文和文件列表,再用 `skill(name, file="reference.md")` 读附带文件(只能读 skill 目录内的文件)。 +- 没有 frontmatter 时,名字取目录名,描述取正文第一行非标题文字。 +- `/skill pdf-forms 把 a.pdf 填好` 会把 skill 正文直接放进这条消息再发送。 + +## 插件(plugin.json) + +``` +~/.config/cst-agent/plugins/jira/ +├── plugin.json +├── issue.py +└── skills/ # 可选:插件自带的 skills +``` + +```json +{ + "name": "jira", + "description": "Read Jira issues.", + "tools": [ + { + "name": "jira_issue", + "description": "Fetch one Jira issue (summary, status, comments).", + "command": ["python3", "issue.py"], + "parameters": { + "type": "object", + "properties": { + "key": {"type": "string", "description": "issue key, e.g. CST-42"}, + "comments": {"type": "integer", "description": "how many comments"} + }, + "required": ["key"] + }, + "perm": "net", + "timeout": 30 + } + ], + "skills": "skills", + "mcpServers": { + "jira-search": {"command": "npx", "args": ["-y", "some-jira-mcp"]} + } +} +``` + +工具字段: + +| 字段 | 说明 | +|---|---| +| `name` | 工具名(非 `[A-Za-z0-9_-]` 的字符换成 `_`,≤ 63 字节),不能和已有工具重名 | +| `description` | 给模型看的说明 | +| `command` | 字符串数组:直接执行,数组里能在插件目录找到的相对路径会换成绝对路径;或一个字符串:交给 `sh` 执行(可以用管道) | +| `parameters`(或 `inputSchema`) | JSON Schema;`required` 会在执行前校验 | +| `perm` | `read`(不询问,可并行)/ `write` / `net` / `shell`(默认),按对应类别审批 | +| `timeout` | 秒,默认 60,最多 600;超时整组进程被杀,退出码 124 | + +执行约定: + +- 参数按 schema 转成类型正确的 JSON 对象(`{"key":"CST-42","comments":3}`),**从 stdin 读**。 +- 工作目录是工作区根目录;环境变量 `CST_PLUGIN_DIR`(插件目录)、`CST_WORKSPACE`、`CST_TOOL`。 +- stdout + stderr 就是工具结果(上限 512 KB),退出码非 0 视为失败。 +- `perm` 为 `shell` 且审批模式是 `auto`、系统有沙箱时,命令在沙箱里运行(只能写工作区和临时目录)。 +- 目前插件工具只支持 POSIX 系统(Linux / macOS / Termux)。 + +一个最小的 Python 工具: + +```python +import json, sys +args = json.load(sys.stdin) +print(f"hello {args['name']}") +``` + +## MCP 服务器(mcp.json) + +格式与 Claude Desktop / Cursor 相同: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] + }, + "github": { + "command": "github-mcp-server", + "args": ["stdio"], + "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"}, + "autoApprove": ["get_issue", "search_code"] + }, + "old": {"command": "foo", "disabled": true} + } +} +``` + +| 字段 | 说明 | +|---|---| +| `command`、`args` | 启动命令(按 `PATH` 查找);只支持 stdio 传输,带 `url` 的服务器会显示为 failed | +| `env` | 追加的环境变量;`${NAME}` 取当前环境里的值,密钥不必写进文件 | +| `cwd` | 工作目录(相对路径相对于工作区;插件里的服务器默认在插件目录) | +| `disabled` | `true` 时不启动 | +| `autoApprove` | `true` 或工具名数组:这些工具按只读处理(不询问、可并行) | + +- 服务器在后台启动(握手、分页 `tools/list`),TUI 不等待;工具在握手完成后出现。headless 运行会先等最多 30 s。 +- 工具注册为 `mcp____`,参数 schema 原样交给模型;默认按网络工具审批(`auto` 模式下会询问,`a` 可记住)。 +- 结果里的 text 内容拼接返回;图片/音频只给出类型和大小;`isError` 视为失败。单次调用 60 s 超时,Esc 会发送 + `notifications/cancelled`。 +- 服务器退出后状态变为 exited(`/mcp` 显示 stderr 最后一行),之后的调用直接报错;`/mcp reload` 重启。 +- 侧栏显示 `MCP 就绪数/总数` 和每个服务器的工具数。 + +## 工作区信任 + +工作区里的 `.agent/plugins/` 和 `.agent/mcp.json` 会在你的机器上执行命令,clone 下来的仓库可能带着它们, +所以默认不加载,首页会提示 “untrusted workspace: /plugins trust”。 + +- `/plugins trust`:信任当前工作区(按真实路径记在 `settings.json` 的 `trusted_workspaces`),立即加载。 +- `/plugins untrust`:取消信任。 +- skills 和子 agent 角色只是文本,不受信任设置影响。 + +## 自定义子 agent 角色 + +`.agent/agents/reviewer.md`: + +```markdown +--- +name: reviewer +description: Reviews a diff for bugs and missing tests; read-only. +tools: read_file, grep, glob, run_shell +readonly: true +model: deepseek-v4-pro +max_steps: 12 +--- +You review changes. Run `git diff` first, read the touched files around each +hunk, and report concrete problems with file:line and a suggested fix. +Do not restate the diff. +``` + +| 字段 | 说明 | +|---|---| +| `name` | 角色名(默认取文件名);与内置 `general` / `explore` / `plan` 同名时覆盖内置角色 | +| `description` | 写进主 agent 的角色索引,决定它什么时候选这个角色 | +| `tools` | 允许的工具白名单(默认全部,`task` 和 `ask_user` 始终不给子 agent) | +| `readonly` | `true` 时去掉写文件和 shell 工具,并允许多个同类 task 并行 | +| `model` | 用别的模型(同一 provider) | +| `max_steps` | 子 agent 每轮的模型调用上限 | + +正文是角色说明,追加在子 agent 的 system prompt 末尾。主 agent 调用 +`task(description="review auth", prompt="…", agent="reviewer")`;子 agent 的转录保存在 +`.agent/sessions/<会话 id>/sub-N.jsonl`,`/agents` 里选中一条记录可以查看它的提示、工具调用和报告。 diff --git a/include/cst_agent.h b/include/cst_agent.h index 2aa5f6b..09893df 100644 --- a/include/cst_agent.h +++ b/include/cst_agent.h @@ -76,7 +76,14 @@ typedef struct { int approval; /* cst_approval_t (interactive sessions) */ int plan_mode; /* read-only tools + planning note */ int tool_profile; /* cst_tool_profile_t (CST_TOOLS) */ - unsigned long long tools_off; /* bit i = registry tool i disabled */ + char tools_off[1024]; /* ",name,name," tools switched off (/tools off) */ + /* Sub-agents (task tool). */ + int depth; /* 0 = top-level; sub-agents run at 1 */ + int read_only; /* no write / shell tools */ + char tools_only[1024]; /* ",a,b," allowlist; empty = every tool */ + const char *role_prompt; /* role instructions (not owned) */ + char undo_sid[128]; /* snapshot into this session's undo... */ + char undo_tag[64]; /* ...under this turn tag (the parent's) */ } cst_agent_config_t; /* ask: confirm per tool; auto: sandbox/rules decide, ask on doubt; yolo: never @@ -299,6 +306,9 @@ int cst_agent_undo_available(const cst_agent_t *a); struct cst_perms *cst_agent_perms(cst_agent_t *a); /* /retry: remove the last user prompt and its replies; caller frees. */ char *cst_agent_rewind_last_turn(cst_agent_t *a); +/* Session id and turn tag that /undo snapshots of the running turn use. */ +void cst_agent_undo_key(const cst_agent_t *a, char *sid, size_t sid_sz, + char *tag, size_t tag_sz); /* Last prompt_tokens reported by the model (0 if unknown). */ long cst_agent_last_prompt_tokens(const cst_agent_t *a); @@ -323,10 +333,15 @@ int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz); /* Last used provider, model per provider and effort, in * /settings.json. */ #define CST_SETTINGS_MAX_MODELS 16 +#define CST_SETTINGS_MAX_TRUSTED 32 typedef struct { char provider[32]; char effort[16]; char approval[8]; /* "" = not chosen yet */ + int show_thinking; /* TUI: Thought blocks start expanded */ + /* workspaces whose .agent/plugins and .agent/mcp.json may run */ + int ntrusted; + char trusted[CST_SETTINGS_MAX_TRUSTED][512]; int nmodels; struct { char provider[32]; @@ -340,6 +355,10 @@ const char *cst_settings_model(const cst_settings_t *s, const char *provider); int cst_settings_remember(const cst_agent_config_t *cfg); /* Record an approval mode the user picked (CST_APPROVAL_*). */ int cst_settings_remember_approval(int approval); +int cst_settings_remember_thinking(int show_thinking); +/* Workspace trust for project plugins / MCP servers (by real path). */ +int cst_settings_trusted(const char *workspace); +int cst_settings_trust(const char *workspace, int on); /* JSON baseline parser for experiments (buffer until valid object). */ typedef struct { diff --git a/include/cst_ext.h b/include/cst_ext.h new file mode 100644 index 0000000..4f89735 --- /dev/null +++ b/include/cst_ext.h @@ -0,0 +1,31 @@ +#ifndef CST_EXT_H +#define CST_EXT_H + +/* Startup / reload of everything that adds tools at runtime: the skill + * tool, plugin exec tools and MCP servers. */ + +#include "cst_agent.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Load for cfg's workspace (no-op for `--tools minimal`). One-line summary + * in report, e.g. "2 skills · 1 plugin (3 tools) · mcp 1/2". */ +void cst_extensions_load(const cst_agent_config_t *cfg, char *report, + size_t report_sz); +/* Summary of the last load (empty when nothing was found). */ +const char *cst_extensions_report(void); +/* Headless: wait for MCP handshakes, then note servers that failed. */ +void cst_extensions_wait(FILE *log); +/* Drop plugin/MCP tools and stop MCP servers. */ +void cst_extensions_unload(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_keymap.h b/include/cst_keymap.h new file mode 100644 index 0000000..495b1d9 --- /dev/null +++ b/include/cst_keymap.h @@ -0,0 +1,134 @@ +#ifndef CST_KEYMAP_H +#define CST_KEYMAP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Key codes: a Unicode code point (control characters included: ctrl+a = 1), + * a CST_K_* special key, optionally OR'ed with CST_KEY_ALT. */ +#define CST_KEY_SPECIAL 0x110000 +#define CST_KEY_ALT 0x400000 +enum { + CST_K_UP = CST_KEY_SPECIAL, + CST_K_DOWN, + CST_K_LEFT, + CST_K_RIGHT, + CST_K_HOME, + CST_K_END, + CST_K_PGUP, + CST_K_PGDN, + CST_K_BTAB, /* shift+tab */ + CST_K_DEL, + CST_K_INS, + CST_K_CTRL_HOME, + CST_K_CTRL_END, + CST_K_CTRL_UP, + CST_K_CTRL_DOWN, + CST_K_ALT_UP, + CST_K_ALT_DOWN, + CST_K_F1, /* CST_K_F1 + n - 1 for f1..f12 */ + CST_K_LAST = CST_K_F1 + 11 +}; + +typedef enum { + CST_KA_NONE = 0, + /* general */ + CST_KA_PALETTE, + CST_KA_KEYS, + CST_KA_MODE, + CST_KA_APPROVAL, + CST_KA_NEWLINE, + CST_KA_REDRAW, + CST_KA_QUIT, + /* view */ + CST_KA_TOGGLE_TOOL, + CST_KA_TOGGLE_THOUGHT, + CST_KA_THINKING_ALL, + CST_KA_DETAILS, + CST_KA_TODO, + CST_KA_SIDEBAR, + CST_KA_SCROLL_UP, + CST_KA_SCROLL_DOWN, + CST_KA_LINE_UP, + CST_KA_LINE_DOWN, + CST_KA_TOP, + CST_KA_BOTTOM, + /* session */ + CST_KA_NEW_SESSION, + CST_KA_SESSIONS, + CST_KA_UNDO, + CST_KA_RETRY, + CST_KA_COMPACT, + CST_KA_COPY, + CST_KA_EXPORT, + /* model */ + CST_KA_MODEL, + CST_KA_PROVIDER, + CST_KA_EFFORT, + /* extensions */ + CST_KA_AGENTS, + CST_KA_SKILLS, + CST_KA_PLUGINS, + CST_KA_COUNT +} cst_key_action_t; + +typedef struct { + const char *name; /* keybinds.json key, e.g. "new_session" */ + const char *group; /* "General", "View", ... */ + const char *desc; + const char *slash; /* equivalent slash command, or NULL */ +} cst_key_action_info_t; + +const cst_key_action_info_t *cst_key_action_info(int action); +int cst_key_action_find(const char *name); /* CST_KA_NONE when unknown */ + +/* "ctrl+p", "shift+tab", "alt+enter", "f1", "pgup", "x" → code; -1 if bad. */ +int cst_key_parse(const char *s); +/* code → canonical name ("ctrl+x", "tab", "alt+up", ...). */ +const char *cst_key_name(int code, char *buf, size_t n); + +#define CST_KEYMAP_MAX 128 +typedef struct { + int action; + int keys[2]; + int nkeys; + int user; /* from keybinds.json */ +} cst_binding_t; + +typedef struct { + int leader; + cst_binding_t b[CST_KEYMAP_MAX]; + int n; +} cst_keymap_t; + +void cst_keymap_defaults(cst_keymap_t *km); +/* Apply keybinds.json text over the defaults: + * {"leader":"ctrl+x", "palette":"ctrl+p", "new_session":[" n","f2"], + * "todo":"none"} + * Returns 0, or -1 with a message (bad entries are skipped, the rest apply). */ +int cst_keymap_apply_json(cst_keymap_t *km, const char *json, char *err, + size_t err_sz); +/* Defaults + /keybinds.json when present. */ +int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz); + +#define CST_KM_NONE 0 +#define CST_KM_MATCH 1 +#define CST_KM_PREFIX 2 +/* seq of 1 or 2 keys → CST_KM_*; *action set on a match. */ +int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n, + int *action); +/* "ctrl+x n, f2" (or "" when unbound). */ +void cst_keymap_describe(const cst_keymap_t *km, int action, char *buf, + size_t n); +/* One line per conflict ("ctrl+o: toggle_tool, toggle_thought"); returns the + * number of conflicts. */ +int cst_keymap_conflicts(const cst_keymap_t *km, char *buf, size_t n); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_mcp.h b/include/cst_mcp.h new file mode 100644 index 0000000..e1a6c93 --- /dev/null +++ b/include/cst_mcp.h @@ -0,0 +1,66 @@ +#ifndef CST_MCP_H +#define CST_MCP_H + +/* + * MCP (Model Context Protocol) client over stdio: newline-delimited + * JSON-RPC 2.0. Each server is spawned with its command/args/env, gets + * initialize + notifications/initialized + tools/list, and every tool is + * registered as mcp____ (approved like network tools). + * + * Config, merged in this order (first name wins): + * /.agent/mcp.json (trusted workspaces only), /mcp.json, + * plugin "mcpServers". Format: {"mcpServers": {"name": {"command": "npx", + * "args": ["-y", "pkg"], "env": {"K": "V"}, "cwd": "...", + * "disabled": false}}} + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CST_MCP_MAX 16 + +typedef enum { + CST_MCP_OFF = 0, + CST_MCP_STARTING, + CST_MCP_READY, + CST_MCP_FAILED, + CST_MCP_EXITED +} cst_mcp_state_t; + +typedef struct { + char name[64]; + char source[48]; /* "workspace" | "user" | "plugin:" */ + char command[256]; + int state; /* cst_mcp_state_t */ + int ntools; + char error[200]; +} cst_mcp_info_t; + +/* One server from a JSON object text ({"command":..., "args":[...], ...}). + * cwd: default working directory (NULL = workspace). Returns 0, or -1 when + * the name exists / the config is invalid. */ +int cst_mcp_add_config_json(const char *name, const char *server_json, + const char *cwd, const char *source); +/* Read the mcp.json files for workspace (the workspace one only when + * trusted). Returns servers added; *untrusted counts skipped ones. */ +int cst_mcp_load_configs(const char *workspace, int ws_trusted, + int *untrusted); +/* Start every configured server in the background; tools appear as each + * one finishes its handshake. */ +void cst_mcp_start_all(const char *workspace); +/* Wait up to ms for servers still starting (headless runs). */ +void cst_mcp_wait_ready(int ms); +/* Stop servers, unregister their tools and forget the configs. */ +void cst_mcp_stop_all(void); +int cst_mcp_info(cst_mcp_info_t *out, int max); +/* Ready / configured counts (for the sidebar). */ +void cst_mcp_counts(int *ready, int *total); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_plugins.h b/include/cst_plugins.h new file mode 100644 index 0000000..c62cc4e --- /dev/null +++ b/include/cst_plugins.h @@ -0,0 +1,51 @@ +#ifndef CST_PLUGINS_H +#define CST_PLUGINS_H + +/* + * Plugins: /plugins//plugin.json (always loaded) and + * /.agent/plugins//plugin.json (only for trusted workspaces). + * + * {"name": "jira", "description": "...", + * "tools": [{"name": "jira_search", "description": "...", + * "parameters": {JSON Schema}, + * "command": ["python3", "search.py"], (or a shell string) + * "perm": "read|write|shell|net", "timeout": 60}], + * "skills": "skills", + * "mcpServers": {"name": {"command": "...", "args": [], "env": {}}}} + * + * A tool call runs the command in the workspace with the arguments as JSON + * on stdin (CST_PLUGIN_DIR / CST_WORKSPACE in the environment); stdout + + * stderr is the result and the exit status its exit code. Relative argv + * entries that exist in the plugin directory are made absolute. + */ + +#include "os_compat.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define CST_PLUGINS_MAX 32 + +typedef struct { + char name[64]; + char dir[OS_PATH_MAX]; + char source[16]; /* "user" | "workspace" */ + char desc[200]; + int ntools; + int nmcp; + int nskills_root; /* 1 when it adds a skills directory */ + char error[200]; /* load problem, "" when fine */ +} cst_plugin_info_t; + +/* Load plugins; workspace ones only when ws_trusted. Returns the number of + * plugins loaded; *untrusted (optional) counts skipped workspace plugins. */ +int cst_plugins_load(const char *workspace, int ws_trusted, int *untrusted); +void cst_plugins_unload(void); +int cst_plugins_info(cst_plugin_info_t *out, int max); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_skills.h b/include/cst_skills.h new file mode 100644 index 0000000..a49d3f8 --- /dev/null +++ b/include/cst_skills.h @@ -0,0 +1,62 @@ +#ifndef CST_SKILLS_H +#define CST_SKILLS_H + +/* + * Skills: directories with a SKILL.md (YAML frontmatter `name`, + * `description`, then instructions) plus any helper files. Found in + * /.agent/skills// /.claude/skills// + * /skills// plugin skill roots (cst_skills_add_root) + * The first skill of a name wins. The system prompt lists name + + * description; the `skill` tool loads the rest on demand. + */ + +#include "os_compat.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CST_SKILLS_MAX 64 +#define CST_SKILL_INDEX_MAX 4096 + +typedef struct { + char name[64]; + char desc[600]; + char dir[OS_PATH_MAX]; + char source[48]; /* "workspace", "claude", "user", "plugin:" */ +} cst_skill_t; + +/* Skills visible from workspace, sorted by name. Returns the count. */ +int cst_skills_list(const char *workspace, cst_skill_t *out, int max); +/* Extra root (a directory of skill directories), e.g. from a plugin. */ +void cst_skills_add_root(const char *dir, const char *source); +/* Forget the extra roots whose source starts with prefix ("" = all). */ +void cst_skills_clear_roots(const char *source_prefix); +/* "Skills (...):\n- name: description\n" capped at CST_SKILL_INDEX_MAX, or + * NULL when there are none. malloc'd. */ +char *cst_skills_index(const char *workspace); +/* SKILL.md body (or one file of the skill) as tool output; *exit_code is 0 + * or 1. malloc'd. */ +char *cst_skill_read(const char *workspace, const char *name, + const char *file, int *exit_code); +/* User message for `/skill name task`: the instructions inlined before the + * task. NULL when the skill does not exist. malloc'd. */ +char *cst_skill_prompt(const char *workspace, const char *name, + const char *task); +/* Register the `skill` tool (idempotent). */ +int cst_skills_register_tool(void); + +/* --- YAML-ish frontmatter (also used for agent role files) --- */ +/* Value of key: plain, quoted, folded (`>`/`|`), `[a, b]` or `- a` lists + * (lists come back comma-separated). Returns 1 when found. */ +int cst_fm_get(const char *text, const char *key, char *out, size_t n); +/* Text after the closing `---` (the whole text when there is none). */ +const char *cst_fm_body(const char *text); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_subagent.h b/include/cst_subagent.h new file mode 100644 index 0000000..63e5e2e --- /dev/null +++ b/include/cst_subagent.h @@ -0,0 +1,65 @@ +#ifndef CST_SUBAGENT_H +#define CST_SUBAGENT_H + +/* + * Sub-agents: the task(description, prompt, agent?) tool runs one turn of a + * child agent with its own context and returns its final report. + * + * Roles: built-in general / explore / plan, plus Markdown files in + * /.agent/agents/ and /agents/ (the workspace wins, both win + * over built-ins). Frontmatter: name, description, model, tools (allowlist), + * max_steps, readonly; the body is the role's instructions. + * + * Transcripts: /.agent/sessions//sub-.jsonl, with a + * sub-.json summary next to each (not listed as sessions). + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CST_ROLES_MAX 32 + +typedef struct { + char name[64]; + char desc[256]; + char source[16]; /* "builtin" | "workspace" | "user" */ + char model[128]; /* empty = the parent's */ + char tools[1024]; /* ",a,b," allowlist; empty = all but task */ + int max_steps; /* 0 = default */ + int read_only; + char *prompt; /* role instructions (malloc'd) */ +} cst_agent_role_t; + +/* Roles sorted built-ins first, then by name. Free with cst_roles_free. */ +int cst_roles_list(const char *workspace, cst_agent_role_t *out, int max); +void cst_roles_free(cst_agent_role_t *v, int n); +/* System prompt section listing the roles (malloc'd), or NULL. */ +char *cst_subagent_index(const char *workspace); +/* Register the task tool (owner "builtin:task") for workspace. */ +int cst_subagent_register_tool(const char *workspace); + +typedef struct { + char file[64]; /* "sub-3" */ + char role[64]; + char desc[160]; + char status[32]; /* "done" | "error" | "cancelled" | "running" */ + int steps, tools; + double ms; + long long mtime; +} cst_subagent_rec_t; + +/* This parent session's sub-agent runs, newest first. */ +int cst_subagent_list(const char *workspace, const char *parent_id, + cst_subagent_rec_t *out, int max); +/* Full path of a transcript (sub-.jsonl). */ +int cst_subagent_path(const char *workspace, const char *parent_id, + const char *file, char *out, size_t out_sz); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/cst_tools.h b/include/cst_tools.h index 3b47122..f815c6a 100644 --- a/include/cst_tools.h +++ b/include/cst_tools.h @@ -41,6 +41,13 @@ typedef struct cst_tool_ctx { void (*set_todo)(const char *text, void *ud); void *todo_ud; int sandbox; /* run_shell: confine to the workspace (cst_sandbox.h) */ + /* Information-dense output (read_file headers and line numbers, cleaned + * run_shell output). Off for `--tools minimal` so paper runs see the + * original bytes. */ + int compact; + /* The calling agent and its turn hooks (task runs sub-agents). */ + cst_agent_t *agent; + const cst_agent_hooks_t *hooks; } cst_tool_ctx_t; typedef int (*cst_tool_run_fn)(const cst_tool_ctx_t *ctx, @@ -51,6 +58,10 @@ typedef int (*cst_tool_run_fn)(const cst_tool_ctx_t *ctx, typedef int (*cst_tool_preview_fn)(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call, char **out); +typedef int (*cst_tool_run_ex_fn)(const cst_tool_ctx_t *ctx, + const ctl_tool_call_t *call, + cst_tool_result_t *r, void *ud); + typedef struct { const char *name; const char *desc; @@ -62,8 +73,32 @@ typedef struct { int minimal; /* member of `--tools minimal` (the original four) */ cst_tool_run_fn run; cst_tool_preview_fn preview; + /* Dynamic tools (skills, plugins, MCP, sub-agents). */ + const char *schema_json; /* JSON Schema object for the arguments; when set + params[] is unused */ + cst_tool_run_ex_fn run_ex; + void *ud; + const char *owner; /* "plugin:", "mcp:", ... */ + /* Non-READ tools that may still join a parallel batch for this call. */ + int (*parallel)(const ctl_tool_call_t *call, void *ud); } cst_tool_def_t; +/* Add a tool at runtime (strings are copied, ud is not). Safe while turns + * run; returns 0, or -1 when the name is taken or the table is full. */ +int cst_tool_register(const cst_tool_def_t *def); +/* Drop every dynamic tool of owner (call only while no turn runs). */ +int cst_tool_unregister_owner(const char *owner); +/* Arguments of call as a JSON object text, typed by the tool's schema + * (numbers/booleans/objects stay JSON). malloc'd. */ +char *cst_tool_args_json(const cst_tool_def_t *t, const ctl_tool_call_t *call); +/* Turn a name into a valid tool name ([A-Za-z0-9_-], at most 63 bytes). */ +void cst_tool_sanitize_name(const char *in, char *out, size_t out_sz); + +/* Per-config switches by name (/tools on|off). */ +int cst_tool_disabled(const cst_agent_config_t *cfg, const char *name); +void cst_tool_set_disabled(cst_agent_config_t *cfg, const char *name, + int off); + typedef enum { CST_TOOLS_DEFAULT = 0, CST_TOOLS_MINIMAL } cst_tool_profile_t; int cst_tool_profile_parse(const char *s, int *out); @@ -95,6 +130,11 @@ int cst_tool_preview(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *call, * Returns the count written into paths. */ int cst_tool_targets(const ctl_tool_call_t *call, char paths[][1024], int max); +/* Shell output cleaned for a model: ANSI escapes removed, `\r` progress + * redraws folded to their final state, runs of identical lines collapsed, + * very long output cut to head + tail. Returns malloc'd text. */ +char *cst_shell_compact(const char *s, size_t n, size_t *out_len); + /* Unified diff (3 lines of context, GNU `diff -u` layout) with * `--- a/