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:
WHC
2026-10-05 14:55:31 +08:00
parent c81df42f0b
commit 7d3ec9fcb9
38 changed files with 8356 additions and 399 deletions
+96 -1
View File
@@ -28,7 +28,102 @@ static int schema_has(cJSON *arr, const char *name) {
return 0;
}
static int echo_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r, void *ud) {
(void)ctx;
const cst_tool_def_t *self = ud;
char *j = cst_tool_args_json(self, c);
r->out = j;
r->out_len = j ? strlen(j) : 0;
r->exit_code = 0;
return 0;
}
static void dynamic_tools(void) {
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
int base = cst_tool_count();
cst_tool_def_t d;
memset(&d, 0, sizeof(d));
d.name = "dyn_echo";
d.desc = "Echo the arguments.";
d.schema_json = "{\"type\":\"object\",\"properties\":{\"text\":{"
"\"type\":\"string\"},\"n\":{\"type\":\"integer\"},"
"\"opts\":{\"type\":\"object\"}},\"required\":[\"text\"]}";
d.perm = CST_TP_READ;
d.run_ex = echo_run;
d.owner = "test:echo";
EXPECT(cst_tool_register(&d) == 0, "register");
EXPECT(cst_tool_register(&d) != 0, "duplicate name refused");
EXPECT(cst_tool_count() == base + 1, "count grows");
const cst_tool_def_t *t = cst_tool_find("dyn_echo");
EXPECT(t && t != &d && strcmp(t->owner, "test:echo") == 0, "copied def");
/* the runner gets its own def as ud for typing */
((cst_tool_def_t *)t)->ud = (void *)t;
cJSON *sc = cst_tools_schema_cfg(&cfg);
char *ss = cJSON_PrintUnformatted(sc);
EXPECT(ss && strstr(ss, "\"name\":\"dyn_echo\"") &&
strstr(ss, "\"required\":[\"text\"]"),
"schema passes the JSON Schema through");
free(ss);
cJSON_Delete(sc);
char *pr = cst_tools_prompt(&cfg);
EXPECT(pr && strstr(pr, "- dyn_echo(text, n?, opts?): Echo the arguments."),
"text prompt lists schema params");
free(pr);
ctl_tool_call_t c;
memset(&c, 0, sizeof(c));
snprintf(c.name, sizeof(c.name), "dyn_echo");
cst_tool_ctx_t ctx;
memset(&ctx, 0, sizeof(ctx));
cst_tool_result_t r;
memset(&r, 0, sizeof(r));
cst_tool_run(&ctx, &c, &r);
EXPECT(r.exit_code == 1 && r.out && strstr(r.out, "needs argument \"text\""),
"schema required checked");
cst_tool_result_free(&r);
c.nargs = 3;
snprintf(c.args[0].key, sizeof(c.args[0].key), "text");
c.args[0].value = "42";
c.args[0].value_len = 2;
snprintf(c.args[1].key, sizeof(c.args[1].key), "n");
c.args[1].value = "7";
c.args[1].value_len = 1;
snprintf(c.args[2].key, sizeof(c.args[2].key), "opts");
c.args[2].value = "{\"a\":true}";
c.args[2].value_len = strlen(c.args[2].value);
memset(&r, 0, sizeof(r));
cst_tool_run(&ctx, &c, &r);
EXPECT(r.exit_code == 0 && r.out &&
strcmp(r.out, "{\"text\":\"42\",\"n\":7,\"opts\":{\"a\":true}}") == 0,
"args typed by schema");
cst_tool_result_free(&r);
cst_tool_set_disabled(&cfg, "dyn_echo", 1);
cst_tool_set_disabled(&cfg, "grep", 1);
EXPECT(!cst_tool_active(&cfg, t) &&
!cst_tool_active(&cfg, cst_tool_find("grep")),
"disable by name");
cst_tool_set_disabled(&cfg, "dyn_echo", 0);
EXPECT(cst_tool_active(&cfg, t) &&
!cst_tool_active(&cfg, cst_tool_find("grep")),
"re-enable one name");
cst_tool_set_disabled(&cfg, "grep", 0);
EXPECT(cfg.tools_off[0] == 0, "set empties");
char nm[64];
cst_tool_sanitize_name("mcp__my server__do.it", nm, sizeof(nm));
EXPECT(strcmp(nm, "mcp__my_server__do_it") == 0, "sanitize");
EXPECT(cst_tool_unregister_owner("test:echo") == 1 &&
!cst_tool_find("dyn_echo") && cst_tool_count() == base,
"unregister by owner");
}
int main(void) {
dynamic_tools();
cst_agent_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
@@ -85,7 +180,7 @@ int main(void) {
free(pdef);
/* /tools off: grep disappears everywhere */
cfg.tools_off = 1ULL << cst_tool_index("grep");
cst_tool_set_disabled(&cfg, "grep", 1);
EXPECT(!cst_tool_active(&cfg, cst_tool_find("grep")), "grep off");
cJSON *off = cst_tools_schema_cfg(&cfg);
EXPECT(!schema_has(off, "grep") && schema_has(off, "glob"), "schema off");