wezard 1.4.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CLAUDE.md +17 -13
- package/README.md +130 -77
- package/cli/wezard.sh +0 -62
- package/config.example.jsonc +3 -9
- package/dist/cli/init.js +12 -21
- package/dist/cli/init.js.map +1 -1
- package/dist/daemon/approval.js +11 -7
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/cc-session.js +136 -0
- package/dist/daemon/cc-session.js.map +1 -0
- package/dist/daemon/chat-name.js +8 -18
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/detail.js +54 -13
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/graph.js +5 -7
- package/dist/daemon/graph.js.map +1 -1
- package/dist/daemon/inbound.js +188 -254
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +459 -258
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +2 -2
- package/dist/daemon/memory-steward.js +82 -0
- package/dist/daemon/memory-steward.js.map +1 -0
- package/dist/daemon/mirror-bridge.js +370 -170
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js.map +1 -1
- package/dist/daemon/model-select.js +74 -0
- package/dist/daemon/model-select.js.map +1 -0
- package/dist/daemon/peers.js +10 -16
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/session-name.js +4 -27
- package/dist/daemon/session-name.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +61 -16
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/task-gate.js +97 -0
- package/dist/daemon/task-gate.js.map +1 -0
- package/dist/daemon/task-registry.js +132 -0
- package/dist/daemon/task-registry.js.map +1 -0
- package/dist/daemon/tasks.js +126 -54
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/wizard-memory.js +52 -0
- package/dist/daemon/wizard-memory.js.map +1 -0
- package/dist/daemon/wizard.js +107 -41
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +181 -209
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +187 -108
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +25 -34
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +14 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/cli-backends.js +2 -7
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/config.js +26 -9
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +12 -6
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +1 -1
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-render.js +55 -0
- package/dist/shared/role-render.js.map +1 -0
- package/dist/shared/role-view.js +228 -0
- package/dist/shared/role-view.js.map +1 -0
- package/dist/shared/schedule-spec.js +92 -116
- package/dist/shared/schedule-spec.js.map +1 -1
- package/dist/shared/session-label.js +103 -72
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/std.js +3 -0
- package/dist/shared/std.js.map +1 -1
- package/dist/shared/task-file.js +120 -0
- package/dist/shared/task-file.js.map +1 -0
- package/dist/shared/trigger.js +232 -0
- package/dist/shared/trigger.js.map +1 -0
- package/dist/shared/world.js +17 -5
- package/dist/shared/world.js.map +1 -1
- package/dist/svr/index.js +67 -2
- package/dist/svr/index.js.map +1 -1
- package/hooks/pre-tool-use.sh +4 -4
- package/package.json +1 -2
- package/web/chat.css +424 -212
- package/web/chat.js +953 -323
- package/commands/wrc.md +0 -7
- package/dist/daemon/cc-bridge.js +0 -183
- package/dist/daemon/cc-bridge.js.map +0 -1
- package/dist/daemon/sessions.js +0 -4
- package/dist/daemon/sessions.js.map +0 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
// MCP server `wezard`. Stdio transport
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// every child process), so multiple windows can each /wrc without trampling.
|
|
1
|
+
// MCP server `wezard`. Stdio transport, stateless: every tool POSTs to the
|
|
2
|
+
// resident daemon over loopback. Sessions are opened from the chat side (the
|
|
3
|
+
// daemon spawns the pane), so nothing here attaches a session.
|
|
5
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
9
|
-
import { join, basename } from "node:path";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
6
|
import { z } from "zod";
|
|
12
7
|
const DAEMON_BASE = process.env.WEZARD_DAEMON_BASE ?? "http://127.0.0.1:17890";
|
|
13
8
|
const ok = (data) => ({
|
|
@@ -17,74 +12,34 @@ const fail = (msg) => ({
|
|
|
17
12
|
isError: true,
|
|
18
13
|
content: [{ type: "text", text: msg }],
|
|
19
14
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
{ dir: join(homedir(), ".codebuddy", "projects"), encode: encodeCodebuddy },
|
|
29
|
-
];
|
|
30
|
-
const findProjectDir = (cwd) => {
|
|
31
|
-
for (const root of PROJECT_ROOTS) {
|
|
32
|
-
const p = join(root.dir, root.encode(cwd));
|
|
33
|
-
if (existsSync(p))
|
|
34
|
-
return p;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
};
|
|
38
|
-
const latestJsonlByMtime = (projectDir) => {
|
|
39
|
-
const files = readdirSync(projectDir).filter((n) => n.endsWith(".jsonl"));
|
|
40
|
-
if (files.length === 0)
|
|
41
|
-
return null;
|
|
42
|
-
return files
|
|
43
|
-
.map((n) => ({ p: join(projectDir, n), m: statSync(join(projectDir, n)).mtimeMs }))
|
|
44
|
-
.reduce((a, b) => (b.m > a.m ? b : a)).p;
|
|
15
|
+
const server = new McpServer({ name: "wezard", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
16
|
+
const withLegacyAddress = (shape) => {
|
|
17
|
+
const extra = {
|
|
18
|
+
...("name" in shape ? { tag: z.string().optional().describe("旧名, 同 `name`。") } : {}),
|
|
19
|
+
...("names" in shape ? { tags: z.array(z.string()).optional().describe("旧名, 同 `names`。") } : {}),
|
|
20
|
+
};
|
|
21
|
+
const name = shape.name;
|
|
22
|
+
return { ...shape, ...(name && !name.isOptional() ? { name: name.optional() } : {}), ...extra };
|
|
45
23
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Primary: env tells us exactly which session invoked us. Trust it
|
|
57
|
-
// unconditionally — claude only writes the jsonl after the first user
|
|
58
|
-
// message lands, so a fresh session (e.g. /clear-then-/wrc, or a brand-new
|
|
59
|
-
// CLI window) will have envSid set but no file yet. The daemon's tail
|
|
60
|
-
// tolerates a missing path (see mirror-bridge.ts attach()), and any
|
|
61
|
-
// existsSync gate here would mis-route to a stale jsonl picked by mtime.
|
|
62
|
-
const envSid = process.env.CODEBUDDY_SESSION_ID
|
|
63
|
-
?? process.env.CLAUDE_CODE_SESSION_ID
|
|
64
|
-
?? process.env.CLAUDE_SESSION_ID;
|
|
65
|
-
if (envSid)
|
|
66
|
-
return { sessionId: envSid, jsonlPath: join(projectDir, `${envSid}.jsonl`) };
|
|
67
|
-
// Fallback: most-recently-written jsonl. Only reached when env is absent
|
|
68
|
-
// (older claude versions, exotic launchers).
|
|
69
|
-
const jsonlPath = latestJsonlByMtime(projectDir);
|
|
70
|
-
if (!jsonlPath)
|
|
71
|
-
return { error: `no .jsonl under ${projectDir}` };
|
|
72
|
-
return { sessionId: basename(jsonlPath, ".jsonl"), jsonlPath };
|
|
24
|
+
const foldLegacyAddress = (required) => (args) => {
|
|
25
|
+
const { tag, tags, ...rest } = args;
|
|
26
|
+
const next = {
|
|
27
|
+
...rest,
|
|
28
|
+
...(rest.name === undefined && tag !== undefined ? { name: tag } : {}),
|
|
29
|
+
...(rest.names === undefined && tags !== undefined ? { names: tags } : {}),
|
|
30
|
+
};
|
|
31
|
+
if (required && next.name === undefined)
|
|
32
|
+
throw new Error("missing `name` (the wizard's global name, e.g. 'fix')");
|
|
33
|
+
return next;
|
|
73
34
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const p = spawn("tmux", ["display-message", "-p", "#{session_name}"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
82
|
-
let out = "";
|
|
83
|
-
p.stdout?.on("data", (c) => (out += c.toString("utf8")));
|
|
84
|
-
p.on("error", () => resolve(undefined));
|
|
85
|
-
p.on("close", (code) => resolve(code === 0 ? out.trim() || undefined : undefined));
|
|
35
|
+
const registerTool = server.registerTool.bind(server);
|
|
36
|
+
server.registerTool = ((toolName, config, cb) => {
|
|
37
|
+
const shape = config.inputSchema;
|
|
38
|
+
if (!shape || !("name" in shape || "names" in shape))
|
|
39
|
+
return registerTool(toolName, config, cb);
|
|
40
|
+
const fold = foldLegacyAddress(!!shape.name && !shape.name.isOptional());
|
|
41
|
+
return registerTool(toolName, { ...config, inputSchema: withLegacyAddress(shape) }, ((args, extra) => cb(fold(args), extra)));
|
|
86
42
|
});
|
|
87
|
-
const server = new McpServer({ name: "wezard", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
88
43
|
// Accept user-friendly prefixes from the LLM: vid:<id> → user:<id>, chatid:<id> → chat:<id>.
|
|
89
44
|
// Pass anything else (already user:/chat:/group:, or empty) through unchanged.
|
|
90
45
|
const normalizeTarget = (raw) => {
|
|
@@ -96,39 +51,6 @@ const normalizeTarget = (raw) => {
|
|
|
96
51
|
return `chat:${raw.slice(7)}`;
|
|
97
52
|
return raw;
|
|
98
53
|
};
|
|
99
|
-
server.registerTool("wrc", {
|
|
100
|
-
title: "WeCom remote control",
|
|
101
|
-
description: "wecom remote control — attach the current agent session to a WeCom chat for live mirror push",
|
|
102
|
-
inputSchema: {
|
|
103
|
-
target: z
|
|
104
|
-
.string()
|
|
105
|
-
.optional()
|
|
106
|
-
.describe('Optional push target. Accepts "vid:<userid>" (DM), "chatid:<chatid>" (group), or raw "user:<id>"/"chat:<id>". Empty → use config defaultChat / mirror.pushChat.'),
|
|
107
|
-
},
|
|
108
|
-
}, async ({ target }) => {
|
|
109
|
-
const r = resolveCallerSession();
|
|
110
|
-
if ("error" in r)
|
|
111
|
-
return fail(r.error);
|
|
112
|
-
const normalizedTarget = normalizeTarget(target);
|
|
113
|
-
// tmux sets $TMUX_PANE for every process inside a pane (e.g. `%5`); we
|
|
114
|
-
// inherit it through claude → MCP child, so each /wrc auto-picks its own
|
|
115
|
-
// pane without the user touching config. Pane ids are not stable across
|
|
116
|
-
// tmux server restarts, so we also capture the session name — the daemon
|
|
117
|
-
// uses it to re-derive a fresh paneId after reload, and as the "user wants
|
|
118
|
-
// a tmux pane" signal that drives respawn when their pane dies.
|
|
119
|
-
const tmuxPane = process.env.TMUX_PANE?.trim();
|
|
120
|
-
const tmuxSession = tmuxPane ? await detectTmuxSession() : undefined;
|
|
121
|
-
const { j } = await daemonPost("/mirror/attach", {
|
|
122
|
-
sessionId: r.sessionId,
|
|
123
|
-
jsonlPath: r.jsonlPath,
|
|
124
|
-
...(normalizedTarget ? { target: normalizedTarget } : {}),
|
|
125
|
-
...(tmuxPane ? { tmuxPane } : {}),
|
|
126
|
-
...(tmuxSession ? { tmuxSession } : {}),
|
|
127
|
-
});
|
|
128
|
-
return j.ok
|
|
129
|
-
? ok({ ok: true, sessionId: r.sessionId, target: j.target })
|
|
130
|
-
: fail(`attach failed: ${j.reason ?? "unknown"}`);
|
|
131
|
-
});
|
|
132
54
|
// set_workspace — one-shot project switch: the daemon applies the switch
|
|
133
55
|
// itself by walking the exact /new path — setPendingCwd → kill pane →
|
|
134
56
|
// respawn in the new cwd → attach → "📂 当前项目" push. NOTE: when the
|
|
@@ -139,19 +61,27 @@ server.registerTool("wrc", {
|
|
|
139
61
|
// the switch.
|
|
140
62
|
server.registerTool("set_workspace", {
|
|
141
63
|
title: "Switch workspace directory",
|
|
142
|
-
description: "一步换掉这个 wizard 的**工作区**: 杀掉当前 pane, 在给定目录下重开一个全新的会话 —— 等价于往那个目录 `/new`。群里收到新会话的 📂 项目信息气泡当回执; 对话上下文**不会**带过去 (和 /new 一样是全新会话, 但身份的系统提示还在)。调用方就是被替换的那一个时, 它在调用当口就被终结 —— 这是预期行为, 群里那条气泡就是回执。用绝对路径 (或 `~` 开头)。想换目录又想保住手上的上下文: 先 wizard_handoff_self 把工作压成简报, 或者
|
|
64
|
+
description: "一步换掉这个 wizard 的**工作区**: 杀掉当前 pane, 在给定目录下重开一个全新的会话 —— 等价于往那个目录 `/new`。群里收到新会话的 📂 项目信息气泡当回执; 对话上下文**不会**带过去 (和 /new 一样是全新会话, 但身份的系统提示还在)。调用方就是被替换的那一个时, 它在调用当口就被终结 —— 这是预期行为, 群里那条气泡就是回执。用绝对路径 (或 `~` 开头)。想换目录又想保住手上的上下文: 先 wizard_handoff_self 把工作压成简报, 或者 spawn_wizard({cwd}) 生一个子 wizard 去那边干。\n"
|
|
65
|
+
+ "**`keep:true` 是另一支**: 人回答「不用换, 就用现在这个目录」时调它 —— 不重开会话, 只把「这个工作区是人认过的」记下来, 此后新会话的开局不再问这件事。一个新聊天的第一个 wizard 落在默认兜底目录里, 开局会被要求先问一句要去哪个项目; 人给路径就走 `cwd`, 人说不用换就走 `keep`。",
|
|
143
66
|
inputSchema: {
|
|
144
|
-
cwd: z.string().describe("Absolute project path, e.g. /Users/foo/projects/bar. ~ is expanded."),
|
|
67
|
+
cwd: z.string().optional().describe("Absolute project path, e.g. /Users/foo/projects/bar. ~ is expanded. 与 `keep` 二选一。"),
|
|
68
|
+
keep: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("人回答了「不用换, 就用现在这个目录」时传 `true` (不要传 cwd)。只记一笔确认 —— 不重开会话、不动上下文, 此后再没人问这件事。"),
|
|
145
72
|
target: z
|
|
146
73
|
.string()
|
|
147
74
|
.optional()
|
|
148
75
|
.describe('Optional target override. "vid:<userid>" / "chatid:<chatid>" / raw "user:<id>"/"chat:<id>". Empty → derive from the calling session.'),
|
|
149
76
|
},
|
|
150
|
-
}, async ({ cwd, target }) => {
|
|
77
|
+
}, async ({ cwd, keep, target }) => {
|
|
78
|
+
if (!cwd && !keep)
|
|
79
|
+
return fail("set_workspace 要么给 cwd (换到那个目录), 要么给 keep:true (确认沿用当前目录)");
|
|
151
80
|
const { sessionId, tmuxPane } = selfRef();
|
|
152
81
|
const normalizedTarget = normalizeTarget(target);
|
|
153
82
|
const { j } = await daemonPost("/mirror/workspace", {
|
|
154
|
-
cwd,
|
|
83
|
+
...(cwd ? { cwd } : {}),
|
|
84
|
+
...(keep ? { keep: true } : {}),
|
|
155
85
|
...(normalizedTarget ? { target: normalizedTarget } : {}),
|
|
156
86
|
...(sessionId ? { sessionId } : {}),
|
|
157
87
|
...(tmuxPane ? { tmuxPane } : {}),
|
|
@@ -160,7 +90,7 @@ server.registerTool("set_workspace", {
|
|
|
160
90
|
const queued = j.pendingCwd ? ` (switch queued for ${j.pendingCwd} — send /new from WeCom to apply)` : "";
|
|
161
91
|
return fail(`set_workspace failed: ${j.reason ?? "unknown"}${queued}`);
|
|
162
92
|
}
|
|
163
|
-
return ok({ ok: true, target: j.target, sessionId: j.sessionId, cwd: j.cwd });
|
|
93
|
+
return ok({ ok: true, target: j.target, sessionId: j.sessionId, cwd: j.cwd, ...(j.confirmed ? { confirmed: true } : {}) });
|
|
164
94
|
});
|
|
165
95
|
// 企业微信 doc / smartsheet / contact MCP 桥接。把 daemon 远端的 MCP 转发
|
|
166
96
|
// 给本地 Claude: list_tools 列工具, call_tool 调用 (创建文档 / 写表格 / 读
|
|
@@ -225,8 +155,8 @@ server.registerTool("switch_claude_session", {
|
|
|
225
155
|
return unwrap("switch_claude_session", await daemonPost("/sessions/switch", { sessionId }));
|
|
226
156
|
});
|
|
227
157
|
// ── wizard ↔ wizard (一个聊天里的同伴) ────────────────────────────────────
|
|
228
|
-
//
|
|
229
|
-
// 各自可以跑不同的 CLI / 模型 / 项目。它们互为同伴 (peer): 看得见彼此, 也驱动得动
|
|
158
|
+
// 每个 wizard 有一个全局唯一的名字, 人在群里写 `.fix`、wizard 之间传 `fix` 就叫到它,
|
|
159
|
+
// 与它住在哪个群无关; 各自可以跑不同的 CLI / 模型 / 项目。它们互为同伴 (peer): 看得见彼此, 也驱动得动
|
|
230
160
|
// 彼此 —— 下面这组工具就是那条通路。本进程只看得见**自己**, 所以任何关于同伴的
|
|
231
161
|
// 问题都要问守护进程, 它才是持有全部 attachment 的那个。
|
|
232
162
|
//
|
|
@@ -257,19 +187,18 @@ const daemonGet = async (path) => {
|
|
|
257
187
|
const unwrap = (name, { j, status }, pick = (x) => x) => j.ok ? ok(pick(j)) : fail(`${name} failed: ${(j.reason ?? j.error ?? `http ${status}`)}`);
|
|
258
188
|
// 地址语法只有一套, 每个吃地址的工具都把它原样重述一遍: 模型单看一个 schema
|
|
259
189
|
// 时没有别的地方能学到它, 而猜出来的地址会安静地指向另一个 wizard 的终端。
|
|
260
|
-
const ADDRESS_DOC = "wizard
|
|
261
|
-
// 造一个 wizard
|
|
262
|
-
//
|
|
263
|
-
// 聊天里, 这正是给聊天起名换来的东西。
|
|
190
|
+
const ADDRESS_DOC = "wizard 的名字 —— 全机唯一, 它就是地址: `'fix'` 或 `'.fix'` 都行, 不分它住在哪个群。`''` = 你这个聊天的默认 wizard。永远别自己拼 key: wizard_roster / list_peers / spawn_wizard / clone_wizard 返回的 `name` (或 409 里的 `address`) 原样传回来。老的 `聊天名#tag` 写法仍然认, 但别再写。";
|
|
191
|
+
// 造一个 wizard: 它的 home 默认是调用方自己的聊天 (所以走 `selfRef`); 给了 `chat`
|
|
192
|
+
// 就落在另一个**起过名字**的聊天里。名字全局唯一, 与 home 无关。
|
|
264
193
|
server.registerTool("new_claude_session", {
|
|
265
194
|
title: "Spawn a blank new wizard",
|
|
266
|
-
description: "在指定项目目录下长出一个**全新的 wizard** —— 自己的 tmux pane
|
|
195
|
+
description: "在指定项目目录下长出一个**全新的 wizard** —— 自己的 tmux pane、自己的全局名字 (`.name`), home 默认是你这个聊天, 等价于人在群里敲 `/new .name`。它**不继承任何上下文**(白纸一张): 要一个开局就带着你读过的材料的分身, 用 clone_wizard; 要一个归你管、干完可回收的白板子 wizard, 用 spawn_wizard。这个工具生的 wizard 独立长住, 不挂在谁名下。新 wizard 在群里说话时气泡头是 `emoji .name`, 之后用 wizard_roster / peek_peer / send_peer / wait_peer 驱动它。用户说「在 /path 下新建一个会话」「帮我在 xxx 目录起个 agent」时调它。给 `chat` 就把它的 home 设在**另一个**聊天里 —— 那个聊天必须起过名字(list_chats 能看到)。目录不存在会自动创建。绝不会顶掉一个聊天的默认 wizard。名字全局唯一: 撞上一个还在的 wizard 返回 409, 返回里的 `name` 是最终落定的名字。",
|
|
267
196
|
inputSchema: {
|
|
268
197
|
cwd: z.string().describe("Absolute project path to start the new session in, e.g. /Users/foo/projects/bar. Created if missing."),
|
|
269
|
-
|
|
198
|
+
name: z
|
|
270
199
|
.string()
|
|
271
200
|
.optional()
|
|
272
|
-
.describe("新 wizard
|
|
201
|
+
.describe("新 wizard 的名字 (如 'fix'、'docs', 带不带 '.' 都行) —— 全机唯一, 它就是地址, 挑一个说明它干什么的短词, 之后用它 send_peer / peek_peer。省略则按目录名生成。"),
|
|
273
202
|
chat: z
|
|
274
203
|
.string()
|
|
275
204
|
.optional()
|
|
@@ -281,23 +210,27 @@ server.registerTool("new_claude_session", {
|
|
|
281
210
|
model: z
|
|
282
211
|
.string()
|
|
283
212
|
.optional()
|
|
284
|
-
.describe("这个 wizard
|
|
213
|
+
.describe("这个 wizard 跑在哪个模型上, 口语化随便写 ('opus' / 'sonnet' / '最新的 opus' / 'claude-sonnet-5' 都行) —— 不是直接塞给启动参数, 而是等 pane 起来后真的敲 `/model` 校验, 认不出就翻一遍这台 CLI 此刻的目录再重试, 所以返回里的 `model` 才是真正落地的那个, 可能跟你传的字符串不完全一样; 万一没匹配上会带 `modelWarning`, 那时 wizard 还在跑但停在了原来的模型上, 不是失败。省略用该 CLI 的默认。同一个聊天里的 wizard 可以各跑各的模型 —— 又长又要判断的活给 opus, 跑腿的 lint/grep 给 haiku。"),
|
|
214
|
+
keepalive: z
|
|
215
|
+
.boolean()
|
|
216
|
+
.optional()
|
|
217
|
+
.describe("这个 wizard 要不要被 keepalive 心跳保温 (空闲时定期 ping 一下防 prompt cache 过期)。false = 永远不保温, 省下那份 ping 的钱 —— 适合跑腿一次就收工的临时会话; true = 明确要保温。省略则按 daemon 配置的默认值。"),
|
|
285
218
|
},
|
|
286
|
-
}, async ({ cwd,
|
|
219
|
+
}, async ({ cwd, name, chat, cli, model, keepalive }) => unwrap("new_claude_session", await daemonPost("/sessions/new", {
|
|
287
220
|
cwd,
|
|
288
|
-
...(
|
|
221
|
+
...(name ? { name } : {}),
|
|
289
222
|
...(chat ? { chat } : {}),
|
|
290
223
|
...(cli ? { cli } : {}),
|
|
291
224
|
...(model ? { model } : {}),
|
|
225
|
+
...(keepalive !== undefined ? { keepalive } : {}),
|
|
292
226
|
})));
|
|
293
|
-
// ── Chat naming
|
|
294
|
-
// A WeCom chat's identity is an unreadable `chat:wrkS…` id
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// pass, which is what makes `daily#fix` — and spawning into `daily` — possible.
|
|
227
|
+
// ── Chat naming ────────────────────────────────────────────────────────────
|
|
228
|
+
// A WeCom chat's identity is an unreadable `chat:wrkS…` id. Wizards are addressed
|
|
229
|
+
// by their own global names; a chat name is what notify / new_claude_session
|
|
230
|
+
// point at, and what a chat's default wizard is born named after.
|
|
298
231
|
server.registerTool("name_chat", {
|
|
299
232
|
title: "Name this WeCom chat",
|
|
300
|
-
description: "
|
|
233
|
+
description: "给**这个聊天**起个短名字: notify 的收件人、new_claude_session / spawn_wizard / clone_wizard 的 `chat` 从此能指到这里。没起过名字的聊天不会一直没名字 —— 第一次有人用到时守护进程按它的工作区自动补一个 (`~/develop/foo` → `foo`, 撞名加序号), 所以这个工具的用途是**起一个更好的名字**。这里的默认 wizard 出生时取聊天名; 它若还叫旧聊天名, 会跟着改 (wizard 的名字全局唯一, 撞名加后缀, 返回里的 `wizardRenamed` 就是它的新名字)。wizard 自己的名字用 wizard_identity 改, 与这里无关。用户说「给这个群起名叫 daily」「这个群叫什么」(不传 `name` 就是读) 「取消命名」(传 '-') 时调它。名字全机唯一、大小写不敏感。",
|
|
301
234
|
inputSchema: {
|
|
302
235
|
name: z
|
|
303
236
|
.string()
|
|
@@ -309,80 +242,84 @@ server.registerTool("name_chat", {
|
|
|
309
242
|
: unwrap("name_chat", await daemonPost("/chats/name", { name })));
|
|
310
243
|
server.registerTool("list_chats", {
|
|
311
244
|
title: "List every chat and its sessions",
|
|
312
|
-
description: "跨聊天目录: 守护进程知道的每一个企微聊天、它的名字 (空 = 没起名)、是不是你住的那个 (`self`),
|
|
245
|
+
description: "跨聊天目录: 守护进程知道的每一个企微聊天、它的名字 (空 = 没起名)、是不是你住的那个 (`self`), 以及以每个聊天为 home 的 wizard 及其名字。用户指向这个聊天之外的活时调它 —— 「别的群有谁在跑」「把这个结论发到 daily 群」「在 sanitizer 群里开个会话」。wizard 本身不必经过聊天就能叫到 (名字全局唯一); 聊天名用于 notify 与在那边生新 wizard。",
|
|
313
246
|
inputSchema: {},
|
|
314
247
|
}, async () => unwrap("list_chats", await daemonPost("/chats/list", {})));
|
|
315
248
|
server.registerTool("list_peers", {
|
|
316
249
|
title: "List the wizards sharing this chat",
|
|
317
|
-
description: "
|
|
250
|
+
description: "和你同一个 home 聊天的其他 wizard (默认 wizard 加任意多个分身), 各有各的 pane、CLI、模型和工作区。每一个返回: 名字 (`address`, 即传给其他工具的那个串)、emoji、工作区、CLI、pane 是否还活着、此刻是否在生成 (`busy`)、最后动过是什么时候、最近在聊什么的一行摘要; `self: true` 是你自己。另外返回 `foreignPeers`: 别的聊天里的 wizard —— 名字全局唯一, 一样叫得到。用户提到另一个 agent 时先调它 —— 「.fix 进展如何」「还有谁在跑」「让 .docs 也看看」—— 再用 peek_peer / send_peer / wait_peer 真正协作。想连**职责、家谱**一起看, 用 wizard_roster。",
|
|
318
251
|
inputSchema: {},
|
|
319
252
|
}, async () => unwrap("list_peers", await daemonPost("/peers/list", {})));
|
|
320
253
|
server.registerTool("peek_peer", {
|
|
321
254
|
title: "Read what another wizard has been saying",
|
|
322
|
-
description: "**不打扰**地观察另一个 wizard: 返回 `dialog` —— 它最近 N 轮真实对话 (从它的 transcript 读的, `▸` 是别人说的, `◂` 是它答的), 外加它此刻是否在生成 (`busy`) 与它最后一条完整回复 (`lastText`)。这是「它和驱动它的人到底说了什么」的可读记录:
|
|
255
|
+
description: "**不打扰**地观察另一个 wizard: 返回 `dialog` —— 它最近 N 轮真实对话 (从它的 transcript 读的, `▸` 是别人说的, `◂` 是它答的), 外加它此刻是否在生成 (`busy`) 与它最后一条完整回复 (`lastText`)。这是「它和驱动它的人到底说了什么」的可读记录: 回答「.fix 进展如何」「它们聊到哪了」, 或者判断要不要推它一把, 都读这里。用户消息里写的 `.name` 指的就是那个 wizard —— 守护进程会在消息尾部挂一条 system-reminder 点名每一个解析得出的名字, 所以 prompt 里的 `.b` 是 wizard `b`: 去 peek 它, 别猜它在干嘛, 更别替它回答。它还没有可读 transcript 时, `pane` 兜底给它终端的原始尾巴。`foreign: true` 表示它的 home 在别的聊天。只读, 随便轮询。",
|
|
323
256
|
inputSchema: {
|
|
324
|
-
|
|
257
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
325
258
|
turns: z.number().optional().describe("How many recent conversation turns to return (1-40, default 6)."),
|
|
326
259
|
},
|
|
327
|
-
}, async ({
|
|
260
|
+
}, async ({ name, turns }) => unwrap("peek_peer", await daemonPost("/peers/peek", { name, ...(turns ? { turns } : {}) })));
|
|
328
261
|
server.registerTool("send_peer", {
|
|
329
262
|
title: "Say something to another wizard",
|
|
330
|
-
description: "跟另一个 wizard 说话 —— 文本原样落进它的输入框, 它当成新的一轮接手。这是你**驱动**同伴的唯一方式: 派活、解它的阻塞、回答它的提问、叫它继续。「推动
|
|
331
|
-
"
|
|
263
|
+
description: "跟另一个 wizard 说话 —— 文本原样落进它的输入框, 它当成新的一轮接手。这是你**驱动**同伴的唯一方式: 派活、解它的阻塞、回答它的提问、叫它继续。「推动 .fix 干到底」的典型循环: peek_peer 看它在哪 → send_peer 说该说的 → wait_peer 等它停下 → 再 peek。名字全局唯一, 目标住在哪个群都一样叫。对方还不存在就自己造: 要它继承你的上下文用 clone_wizard, 要一个白纸一张的子 wizard 用 spawn_wizard。\n" +
|
|
264
|
+
"**默认是私聊**: 不出任何群气泡, 只记在你们双方的 rolepage 里; 它那一轮的回复也不进群, 用 wait_peer 取。**`public:true` 则在公开频道说** —— 你这一轮所在的群里出一条 `.你 → .它` 的气泡, 它那一轮的回复也发进这个群。公开与否由你判断: 需要人知道的、或本该当着人讨论的 (关键决策、给人的结论、要人拍板的分歧) 用 public; 过程性的派活、催进度、对齐细节用私聊。无论哪种都直说: 要什么、给什么、结论是什么, 不用寒暄、不用引用原文。拒绝对自己发送。",
|
|
332
265
|
inputSchema: {
|
|
333
|
-
|
|
266
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
334
267
|
text: z.string().describe("Message to inject. Plain prompt text; slash commands like '/clear' also work."),
|
|
335
268
|
when: z
|
|
336
269
|
.enum(["now", "idle"])
|
|
337
270
|
.optional()
|
|
338
271
|
.describe("什么时候投。`now` (默认) 立刻投 —— 对方正在生成时这句话会排在它这一轮后面, 回答它的提问、打断它、催它都该用这个。`idle` 先等它闲下来再投: **派一件新活给一个正在忙的同伴时用它**, 否则你和别人的两段文本会挤进同一个输入框被当成一轮读掉。返回里的 `wasBusy` 告诉你投的时候它忙不忙。"),
|
|
339
272
|
waitSec: z.number().optional().describe("`when:'idle'` 最多等多少秒 (10-3600, 默认 600)。等不到就返回失败, 不会强行投。"),
|
|
340
|
-
|
|
273
|
+
public: z
|
|
274
|
+
.boolean()
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("true = 在公开频道 (你这一轮所在的群) 里说: 群里出 `.你 → .它` 气泡, 它的回复也进群。默认 false = 私聊, 只记在 rolepage。需要人知道 / 该当着人讨论才用 true。"),
|
|
277
|
+
job: z.string().optional().describe("这次派活归到某个工单名下 (open_job 给的 id) —— 收工那一条会把各自那段活列出来。"),
|
|
341
278
|
},
|
|
342
|
-
}, async ({
|
|
279
|
+
}, async ({ name, text, when, waitSec, job, public: pub }) => unwrap("send_peer", await daemonPost("/peers/send", { name, text, ...(when ? { when } : {}), ...(waitSec ? { waitSec } : {}), ...(job ? { job } : {}), ...(pub ? { public: true } : {}) })));
|
|
343
280
|
server.registerTool("notify", {
|
|
344
281
|
title: "Post a message into a chat for people to read",
|
|
345
282
|
description: "把一段 markdown 贴进一个企微聊天**给人看**。和 send_peer 分工明确: send_peer 是把话塞进另一个 agent 的输入框 (驱动它干活), notify 是说给人听 —— 不会触发任何一轮对话。\n" +
|
|
346
|
-
"`to` 省略 =
|
|
283
|
+
"`to` 省略 = 你这一轮所在的群 (人从哪个群叫的你就是哪个; 私聊轮则是你的 home 群); 要发到别的群就写聊天名 (`list_chats` 里那个), 一次可以写多个。气泡头自动写成 `emoji .你的名字` 并挂上你的 rolepage 链接, 那边的人一眼知道是谁。\n" +
|
|
347
284
|
"什么时候用: 长活跑完了要通知另一个群的人; 一批分身收工后把汇总播给发起那个群; 定时任务 (schedule_task) 到点跑完把结论送到该看的人那里。别拿它跟同群的人说话 —— 那是你的正常回复。",
|
|
348
285
|
inputSchema: {
|
|
349
286
|
to: z
|
|
350
287
|
.array(z.string())
|
|
351
288
|
.optional()
|
|
352
|
-
.describe("
|
|
289
|
+
.describe("收件聊天: 聊天名 (`daily`) 或裸 principal (`chat:wr…` / `user:…`)。省略 = 你这一轮所在的群。认不出的整条拒绝并列出来, 不会部分送达。"),
|
|
353
290
|
markdown: z.string().describe("正文, markdown。头 (是谁发的) 由守护进程自动加, 别自己写。"),
|
|
354
291
|
},
|
|
355
292
|
}, async ({ to, markdown }) => unwrap("notify", await daemonPost("/notify", { ...(to ? { to } : {}), markdown })));
|
|
356
293
|
server.registerTool("wait_peer", {
|
|
357
294
|
title: "Wait until another wizard stops working",
|
|
358
|
-
description: "挂起, 直到点名的 wizard 停下来 (它的终端不再显示中断提示), 然后返回它最新的回复。send_peer 之后就该用它 —— 这样你拿到的是写完的答案, 而不是写了一半的。超时先到则返回 `idle: false` 与原因: 它只是还在干, 你可以 peek 一眼再等。很便宜: 守护进程轮询的是 pane, 不烧 token
|
|
359
|
-
"**派了一批活就用 `
|
|
295
|
+
description: "挂起, 直到点名的 wizard 停下来 (它的终端不再显示中断提示), 然后返回它最新的回复。send_peer 之后就该用它 —— 这样你拿到的是写完的答案, 而不是写了一半的。超时先到则返回 `idle: false` 与原因: 它只是还在干, 你可以 peek 一眼再等。很便宜: 守护进程轮询的是 pane, 不烧 token。私聊轮的回复不进群 —— 人需要知道结论时, 用你自己的话收口给人, 别把原话再念一遍; 公开轮的回复已经在群里了, 更不必复述。\n" +
|
|
296
|
+
"**派了一批活就用 `names` 一次等一组**, 别一个一个等: 它们本来在同时干活, 串行等的墙钟是所有人之和, 并行等只等最慢的那一个。`results` 按你给的顺序逐个回 `idle` / `lastText`。`need` 决定满几个就返回 (默认全部; `need:1` = 谁先完事就先处理谁, 剩下的还在跑, 再调一次接着等)。",
|
|
360
297
|
inputSchema: {
|
|
361
|
-
|
|
362
|
-
|
|
298
|
+
name: z.string().optional().describe(`${ADDRESS_DOC} 等一组时改用 \`names\`。`),
|
|
299
|
+
names: z
|
|
363
300
|
.array(z.string())
|
|
364
301
|
.optional()
|
|
365
|
-
.describe("一次等多个 wizard
|
|
302
|
+
.describe("一次等多个 wizard 的名字 (最多 16 个), 每个的写法同 `name`。fan-out 之后的 join 用它 —— 五个分身并行等只花最慢那一个的时间。"),
|
|
366
303
|
need: z
|
|
367
304
|
.number()
|
|
368
305
|
.optional()
|
|
369
306
|
.describe("满几个就返回 (1 到地址个数, 默认全部)。`1` = 任意一个先完事就返回; 中间值 = 法定人数。满足后剩下的等待会被撤掉, 它们照常继续干活, 结果里 `idle:false`。"),
|
|
370
307
|
timeoutSec: z.number().optional().describe("Max seconds to wait (10-7200, default 900)."),
|
|
371
308
|
},
|
|
372
|
-
}, async ({
|
|
373
|
-
...(
|
|
309
|
+
}, async ({ name, names, need, timeoutSec }) => unwrap("wait_peer", await daemonPost("/peers/wait", {
|
|
310
|
+
...(names?.length ? { names } : { name: name ?? "" }),
|
|
374
311
|
...(need ? { need } : {}),
|
|
375
312
|
...(timeoutSec ? { timeoutSec } : {}),
|
|
376
313
|
})));
|
|
377
314
|
server.registerTool("run_agent_graph", {
|
|
378
315
|
title: "Run a loop graph over several tagged agents",
|
|
379
|
-
description: "把这个聊天里的几个 wizard 串成一条**会循环的流水线**, 交给守护进程去驱动。`nodes` 是参与的 `#tag` wizard (每个可以自选 cli / 模型 / 工作区; 不存在的当场造出来, 已经在跑的原样复用、上下文不动)。`steps` 是有序管线 —— 每一步向一个 wizard 发一段提示、等它干完、抓住它的回复、喂给下一步。整张 step 表会被走 `rounds` 遍, 这才叫**循环**: `fix → review → fix → review …` 直到某个回复里出现 `until` 或轮次用完。提示模板可以引用前面的产出: `{{last}}` = 上一步的回复, `{{<tag>}}` = 那个 wizard 最新的回复, `{{round}}` = 第几轮。立刻返回 runId 并把进度播报进群; 用 graph_status 查、stop_graph 停。用户要「几个 agent 互相评审/迭代到收敛」时用它。只是推一个 wizard 一把, 用 send_peer + wait_peer。要它们开局就共享同一批材料, 先
|
|
316
|
+
description: "把这个聊天里的几个 wizard 串成一条**会循环的流水线**, 交给守护进程去驱动。`nodes` 是参与的 `#tag` wizard (每个可以自选 cli / 模型 / 工作区; 不存在的当场造出来, 已经在跑的原样复用、上下文不动)。`steps` 是有序管线 —— 每一步向一个 wizard 发一段提示、等它干完、抓住它的回复、喂给下一步。整张 step 表会被走 `rounds` 遍, 这才叫**循环**: `fix → review → fix → review …` 直到某个回复里出现 `until` 或轮次用完。提示模板可以引用前面的产出: `{{last}}` = 上一步的回复, `{{<tag>}}` = 那个 wizard 最新的回复, `{{round}}` = 第几轮。立刻返回 runId 并把进度播报进群; 用 graph_status 查、stop_graph 停。用户要「几个 agent 互相评审/迭代到收敛」时用它。只是推一个 wizard 一把, 用 send_peer + wait_peer。要它们开局就共享同一批材料, 先 clone_wizard 出这些节点再跑图。",
|
|
380
317
|
inputSchema: {
|
|
381
318
|
nodes: z
|
|
382
319
|
.array(z.object({
|
|
383
320
|
tag: z.string().describe("wizard 的 tag, 不带 '#', 如 'fix'。"),
|
|
384
321
|
cli: z.enum(["claude", "claude-internal", "codebuddy"]).optional().describe("这个 wizard 用哪个 CLI。省略则继承本聊天的。"),
|
|
385
|
-
model: z.string().optional().describe("要现造这个 wizard
|
|
322
|
+
model: z.string().optional().describe("要现造这个 wizard 时跑在哪个模型上, 口语化随便写 ('opus' / 'haiku' / '最新的 opus' 都行) —— 起来后会用 `/model` 实测校验。已经在跑的不受影响。"),
|
|
386
323
|
cwd: z.string().optional().describe("这个 wizard 的工作区绝对路径。省略则继承本聊天的。"),
|
|
387
324
|
}))
|
|
388
325
|
.describe("参与的 wizard。每个 step 的 `to` 都必须点到这里面的某个 tag。"),
|
|
@@ -420,16 +357,16 @@ server.registerTool("stop_graph", {
|
|
|
420
357
|
}, async ({ runId }) => unwrap("stop_graph", await daemonPost("/graph/stop", { runId })));
|
|
421
358
|
server.registerTool("handoff", {
|
|
422
359
|
title: "Hand another wizard's work over to a fresh context",
|
|
423
|
-
description: "给**另一个** wizard 做交接, 原地完成: 守护进程让它把当前工作压成一份自洽的交接简报, 等它写完抓取, 再往**同一个 pane** 注入 `/clear` (上下文清零、新 sessionId、cwd 不变、身份的系统提示还在), 然后把简报作为新会话的第一条消息贴回去。它的上下文撑不住了、或者用户说「让
|
|
360
|
+
description: "给**另一个** wizard 做交接, 原地完成: 守护进程让它把当前工作压成一份自洽的交接简报, 等它写完抓取, 再往**同一个 pane** 注入 `/clear` (上下文清零、新 sessionId、cwd 不变、身份的系统提示还在), 然后把简报作为新会话的第一条消息贴回去。它的上下文撑不住了、或者用户说「让 .fix 交接一下」「叫它压缩上下文重开」时用。按 tmux `pane` id (`%5`, 来自 wizard_roster / list_claude_sessions) 或按 `name` 寻址。**要交接的是你自己就用 wizard_handoff_self** —— 这里拒绝对自身操作 (会死锁: 你没法在自己生成的当口再被问一次)。返回被带过去的那份简报。",
|
|
424
361
|
inputSchema: {
|
|
425
|
-
pane: z.string().optional().describe("目标 tmux pane id, 如 '%5'。优先于
|
|
426
|
-
|
|
362
|
+
pane: z.string().optional().describe("目标 tmux pane id, 如 '%5'。优先于 name。从 wizard_roster / list_claude_sessions 拿。"),
|
|
363
|
+
name: z.string().optional().describe(`${ADDRESS_DOC} 给了 pane 就忽略它。`),
|
|
427
364
|
focus: z.string().optional().describe("交接简报里要特别交代的点, 如 '重点交代还没跑通的测试'。可选。"),
|
|
428
365
|
timeoutSec: z.number().optional().describe("Max seconds to wait for the summary before aborting (30-7200, default 600)."),
|
|
429
366
|
},
|
|
430
|
-
}, async ({ pane,
|
|
367
|
+
}, async ({ pane, name, focus, timeoutSec }) => unwrap("handoff", await daemonPost("/handoff", {
|
|
431
368
|
...(pane ? { pane } : {}),
|
|
432
|
-
...(
|
|
369
|
+
...(name !== undefined ? { name } : {}),
|
|
433
370
|
...(focus ? { focus } : {}),
|
|
434
371
|
...(timeoutSec ? { timeoutSec } : {}),
|
|
435
372
|
})));
|
|
@@ -438,24 +375,37 @@ server.registerTool("handoff", {
|
|
|
438
375
|
// 定时器给不了的那一半: 它们的循环活在会话里, 会话一死就没了; 这个活在 daemon 里。
|
|
439
376
|
server.registerTool("schedule_task", {
|
|
440
377
|
title: "Schedule a prompt to run in a wizard session, on a recurring or one-off schedule",
|
|
441
|
-
description: "
|
|
378
|
+
description: "排一个**到点自动执行**的活, **归在你名下** (你的 rolepage「日程」里看得到): 到时间了, daemon 起一个**全新的白板 wizard** (名字 `<你>-task-xxxx`, 挂在你的家谱下), 把 `prompt` 原样说给它听, 它干完活自动收掉 —— 产出照常落在群里。守护进程级, 跨 CLI 重启/会话结束仍在。用户说「每个工作日晚上 9:30 自动跑一下 xxx」「每天早上帮我看看 yyy」「每 2 小时同步一次 zzz」「明早 9 点提醒并整理 www」时调它。\n" +
|
|
379
|
+
"**每条任务落成一份你能直接改的代码文件** `~/.wezard/tasks/<id>.task.mjs` (返回值里的 `file`)。这个工具只管最常见的那条路 (什么时候 + 说什么); 要更细的东西就**用 Read/Edit 改那个文件**, 存盘即生效, 不用 reload:\n" +
|
|
380
|
+
"· 触发条件可组合 —— `when: ({every, daily, at, between, onDays, and, not}) => and(every(\"1h\"), between(\"08:00\",\"20:00\"), onDays(\"工作日\"))`。`every/daily/at` 产生时刻, `between/onDays/not` 只做筛选。\n" +
|
|
381
|
+
"· **放枪前先探一眼**用 `gate` —— 一段在 daemon 侧跑的异步函数, 返回 `false` 这一轮就不放枪 (不起 wizard、群里不出声), 返回 `{vars}` 则填进 prompt 里的 `{{名字}}`, 返回 `{state}` 下一轮还拿得到。`gate: async ({sh, state}) => { const out = await sh(\"git fetch -q && git log --oneline HEAD..@{u}\"); return out.trim() ? {vars:{commits: out}} : false; }`。「有新东西才处理」这类需求必须用它, 别写成「到点起个 wizard 让它自己看一眼没有就退出」—— 那是每次空转一个 pane 加一份上下文。\n" +
|
|
382
|
+
"· `enabled: false` 暂停而不删。\n" +
|
|
383
|
+
"**默认新建, 不在任何已有会话里续**: 定时的活是一件独立的事, 塞进一个常驻 wizard 会把两件不相关的事挤进同一个 transcript, 那个 wizard 正忙时还会连触发时刻一起被拖走。只有用户明确说了「在 .foo 里继续 / 让 .foo 每天…」才传 `name` 点名它 —— 这时到点直接投进它那一轮 (它正忙则仍然另起白板执行)。`prompt` 本身就在说「建两个 wizard 去干 xxx」时, 就算给了 `name` 也照新建办 —— 守护进程自己认这句话, 那个名字只决定在谁的聊天/目录下办。要强行在它那一轮里续, 显式传 `fresh:false`。\n" +
|
|
384
|
+
"`when` 用人话原样写, 别自己翻译成 cron: 「每个工作日晚上9:30」「每天 8:00」「每周三下午3点」「每隔两个小时」「每小时」「每 30 分钟」「20 分钟后」「明早 9 点」都认, 还能叠时间窗口 —— 「白天每隔一个小时」「工作时间每半小时」「8点到20点每小时」。解析不出会报错并列出能认的说法 —— 这时把原话回给用户让他重说, 别自己猜一个时间存进去。\n" +
|
|
385
|
+
"存成功后**必须把回显的 `when` 和 `next` 念给用户**确认 (例: 「每个工作日 21:30, 下次 2026-09-21 21:30」)。`prompt` 要写成一句完整的、零上下文也能执行的指令 —— 到点接活的多半是个刚出生的白板 wizard, 它只看得见这句话。",
|
|
442
386
|
inputSchema: {
|
|
443
|
-
when: z.string().describe("什么时候跑, 人话原样传: 「每个工作日晚上9:30」「每天早上9
|
|
444
|
-
prompt: z.string().describe("到点要说给那个 wizard 听的话。写成自洽的完整指令 (要做什么、在哪个目录/文件上、做完怎么汇报),
|
|
445
|
-
|
|
446
|
-
|
|
387
|
+
when: z.string().describe("什么时候跑, 人话原样传: 「每个工作日晚上9:30」「每天早上9点」「白天每隔一个小时」「工作时间每半小时」「每30分钟」「20分钟后」「明早9点」。时间窗口 (白天 / 工作时间 / 8点到20点) 会被解析成筛子, 窗外的那些枪直接吞掉。"),
|
|
388
|
+
prompt: z.string().describe("到点要说给那个 wizard 听的话。写成自洽的完整指令 (要做什么、在哪个目录/文件上、做完怎么汇报), 别依赖当前对话的上下文。里面可以留 `{{名字}}` 占位, 由任务文件里的 gate 填。"),
|
|
389
|
+
name: z.string().optional().describe(`点名在**哪个已有 wizard**里跑 —— 只有用户要求「在它那儿继续」时才传。省略 = 到点新建一个白板 wizard 干完就收 (默认, 也是「定时新建 wizard 干 xxx」要的那个)。传了它还想要新建, 再加 \`fresh:true\`: 那时它只当模板, 新 wizard 继承它的聊天/目录/模型。${ADDRESS_DOC}`),
|
|
390
|
+
fresh: z.boolean().optional().describe("覆盖默认: true = 每次到点新建白板 wizard 执行 (给了 name 时用来表达「在它的目录下新开一个干」), false = 注入 name 指向的已有会话。默认由 name 推断 (给了 name = false, 没给 = true)。"),
|
|
391
|
+
id: z.string().optional().describe("任务 id, 同时也是文件名 (`~/.wezard/tasks/<id>.task.mjs`)。省略则从 note/prompt 生成。取个好认的短名 —— 之后你要改这条任务, 改的就是那个文件。"),
|
|
392
|
+
note: z.string().optional().describe("给人看的一句话备注, 在 list_tasks 里回显。"),
|
|
447
393
|
},
|
|
448
|
-
}, async ({ when, prompt,
|
|
394
|
+
}, async ({ when, prompt, name, note, fresh, id }) => unwrap("schedule_task", await daemonPost("/tasks/schedule", {
|
|
395
|
+
when, prompt, name: name ?? "", note: note ?? "", id: id ?? "",
|
|
396
|
+
...(fresh === undefined ? {} : { fresh }),
|
|
397
|
+
})));
|
|
449
398
|
server.registerTool("list_tasks", {
|
|
450
399
|
title: "List scheduled tasks on this host",
|
|
451
|
-
description: "列出本机所有定时任务: `id` (取消要用)、`when` (人话回显)、`next` (下次触发时刻)、`lastFired
|
|
400
|
+
description: "列出本机所有定时任务: `id` (取消要用)、`when` (人话回显)、`next` (下次触发时刻)、`lastFired`、`runIn` (到点是新建白板 wizard 还是注入已有会话)、目标 wizard 的 `address` 与 `prompt`。用户问「有哪些定时任务」「我设了什么定时」「下次什么时候跑」时调它。只读。`mine:true` 只看排给你自己的。\n" +
|
|
401
|
+
"`file` 是这条任务的源码 (`~/.wezard/tasks/<id>.task.mjs`) —— 要改触发条件、加 gate、暂停 (`enabled:false`) 就 Read/Edit 它, 存盘即生效。`gate` 说明这条任务有没有放枪前的探查以及上一轮的去向 (go 放了 / skip 没活 / error 炸了), `lastError` 与 `loadError` 是它上次为什么没跑成 —— 改坏了的文件也会出现在列表里, 带着理由。",
|
|
452
402
|
inputSchema: {
|
|
453
403
|
mine: z.boolean().optional().describe("true = 只列排给调用方自己的任务。默认列全机。"),
|
|
454
404
|
},
|
|
455
405
|
}, async ({ mine }) => unwrap("list_tasks", await daemonPost("/tasks/list", { mine: mine === true })));
|
|
456
406
|
server.registerTool("cancel_task", {
|
|
457
407
|
title: "Cancel one scheduled task by id",
|
|
458
|
-
description: "按 id 删掉一条定时任务 (id 从 list_tasks 拿)
|
|
408
|
+
description: "按 id 删掉一条定时任务 (id 从 list_tasks 拿), 连同它那份任务文件一起。用户说「取消那个定时」「别再每天跑了」时调它: 先 list_tasks 把候选念给用户确认是哪一条, 再删。删的是日程本身, 不影响任何正在跑的活。**一条定时的 prompt 要求「没活就停掉这个定时」时, 到点接活的那个 wizard 就该自己调它**: list_tasks 按 note/prompt 认出是哪一条, 再 cancel_task。只想暂时停就改任务文件的 `enabled: false`, 留档。",
|
|
459
409
|
inputSchema: {
|
|
460
410
|
id: z.string().describe("Schedule id from list_tasks."),
|
|
461
411
|
},
|
|
@@ -490,14 +440,14 @@ server.registerTool("config_set", {
|
|
|
490
440
|
// 分身还剩几个、别人是谁。
|
|
491
441
|
server.registerTool("wizard_whoami", {
|
|
492
442
|
title: "Who am I",
|
|
493
|
-
description: "你自己是谁:
|
|
443
|
+
description: "你自己是谁: 全局唯一的名字 (即地址, 别人用它找你)、home 聊天、工作区、职责、记忆、家谱 (谁生的你、你生了谁), 以及此刻的 contextTokens 与 handoffSuggested。用户问「你是谁」「你叫什么」「你在哪个目录」「你有几个分身」时先调它; 要做任何编排之前也先调它 —— 你得知道自己的工作区在哪、手里已经有哪些分身。handoffSuggested=true 表示上下文该交接了 (见 wizard_handoff_self)。",
|
|
494
444
|
inputSchema: {},
|
|
495
445
|
}, async () => unwrap("wizard_whoami", await daemonPost("/wizard/whoami", {})));
|
|
496
446
|
server.registerTool("wizard_identity", {
|
|
497
447
|
title: "Name yourself / declare your job",
|
|
498
|
-
description: "
|
|
448
|
+
description: "改名字、写职责。名字就是别人喊你的那个词 (`.name`), **全机唯一**, 与聊天名无关 (给聊天起名用 name_chat): 撞名时自动加 `-N` 后缀, 返回里的 `name` 是落定的那个, `renamed` 会告诉你发生了改写。职责是一句话的「我是干什么的」—— 别的 wizard 在名册里读到它, 据此决定该不该找你。用户说「你以后叫 X」「你负责 X」时调它; 你自己发现 whoami 里职责是空的, 也应当主动补上。只传要改的那个字段。",
|
|
499
449
|
inputSchema: {
|
|
500
|
-
name: z.string().optional().describe("新名字, 1-32 位字母/数字/`_`/`-`,
|
|
450
|
+
name: z.string().optional().describe("新名字, 1-32 位字母/数字/`_`/`-`, 全机唯一。不改就别传。"),
|
|
501
451
|
description: z.string().optional().describe("一句话职责, 例如 '盯 wezard 主仓的重构与发版'。不改就别传。"),
|
|
502
452
|
},
|
|
503
453
|
}, async ({ name, description }) => unwrap("wizard_identity", await daemonPost("/wizard/identity", {
|
|
@@ -506,10 +456,10 @@ server.registerTool("wizard_identity", {
|
|
|
506
456
|
})));
|
|
507
457
|
server.registerTool("wizard_roster", {
|
|
508
458
|
title: "Every wizard and clone",
|
|
509
|
-
description: "这个世界上所有的 wizard 与 clone:
|
|
459
|
+
description: "这个世界上所有的 wizard 与 clone: 每一个的名字 (全局唯一, 即地址)、**home 聊天**、**工作区**、职责、忙闲 (busy)、是否还活着 (alive)、最近在干嘛 (summary), 以及家谱 (parent / clones / ancestors)。跨聊天的也在里面。这是你感知同伴的唯一入口 —— 用户说「还有谁在跑」「谁在弄那个项目」「让懂 X 的那个来看看」时先调它, 拿到目标的名字再 send_peer / peek_peer / wait_peer; 要往某个群里对人说话则把那个 `chat` 交给 notify。\n" +
|
|
510
460
|
"**这是一张索引, 不是一份名单**: 整台机器上可能有几百个会话, 所以默认只回最相关的一页 (自己 → 活着的 → 最近动过的), 并告诉你 `total` / `matched` 有多少。找人就带上条件: `query` 匹配名字/职责/地址, `cwd` 匹配工作区路径 (「谁在这个目录里干活」), `chat` 限定某个聊天, `alive:true` 只看还活着的。别不带条件硬拉全表。",
|
|
511
461
|
inputSchema: {
|
|
512
|
-
query: z.string().optional().describe("在名字 / 职责 /
|
|
462
|
+
query: z.string().optional().describe("在名字 / 职责 / target 里做子串匹配 (不分大小写)。「让懂 X 的那个来看看」就把 X 写在这里。"),
|
|
513
463
|
chat: z.string().optional().describe("只看某个聊天里的 wizard: 聊天名, 或者聊天 principal 的一段 (无名聊天用它)。"),
|
|
514
464
|
cwd: z.string().optional().describe("只看工作区路径包含这一段的 wizard —— 「谁在 /path 下干活」的反查。"),
|
|
515
465
|
alive: z.boolean().optional().describe("true = 只看 pane 还活着的。默认全给 (冷会话发消息就会被唤醒)。"),
|
|
@@ -522,50 +472,70 @@ server.registerTool("wizard_roster", {
|
|
|
522
472
|
...(alive ? { alive } : {}),
|
|
523
473
|
...(limit ? { limit } : {}),
|
|
524
474
|
})));
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
475
|
+
// 生孩子与分身是两个操作, 不是一个开关的两档: 一个白纸起步、可以去别的目录;
|
|
476
|
+
// 一个 fork 调用方此刻的上下文、必须留在原地。拆成两个工具, 模型选的是动词,
|
|
477
|
+
// 而不是在一个布尔上猜。两者落到同一条路由 (`inherit` 由工具定死)。
|
|
478
|
+
const offspringShape = {
|
|
479
|
+
description: z.string().describe("它负责什么, 一句话。它会写进它的系统提示, 也会出现在名册里让别人看到。"),
|
|
480
|
+
name: z.string().optional().describe("它的名字 (如 'docs'、'fix', 带不带 '.' 都行)。全机唯一。省略则按 description 首词生成。"),
|
|
481
|
+
task: z.string().optional().describe("就位后立刻派下去的第一件活 (私聊)。省略则它就位待命。"),
|
|
482
|
+
chat: z.string().optional().describe("把它生在另一个聊天里 (list_chats 里的名字)。省略 = 你自己的聊天, 这是绝大多数情况。"),
|
|
483
|
+
cli: z.enum(["claude", "claude-internal", "codebuddy"]).optional().describe("用哪个 CLI。省略则继承。"),
|
|
484
|
+
model: z.string().optional().describe("跑在哪个模型上, 口语化随便写 ('opus' / 'sonnet' / '最新的 opus' / 'claude-sonnet-5' 都行) —— 不是直接塞给启动参数, 而是等 pane 起来后真的敲 `/model` 校验, 认不出就翻一遍这台 CLI 此刻的目录再重试, 所以返回里的 `model` 才是真正落地的那个, 可能跟你传的字符串不完全一样; 万一没匹配上会带 `modelWarning`, 那时它还在跑但停在了原来的模型上, 不是失败。省略用该 CLI 的默认。要判断力的给 opus, 跑腿的 (grep、跑测试、照着清单改) 给 haiku —— 一批不必齐步走。"),
|
|
485
|
+
job: z
|
|
486
|
+
.string()
|
|
487
|
+
.optional()
|
|
488
|
+
.describe("归到某个工单名下 (open_job 给的 id)。它们攒到 close_job 那一条里一起交代, 过程在各自的 rolepage; close_job 还会把它们整批回收掉。"),
|
|
489
|
+
keepalive: z
|
|
490
|
+
.boolean()
|
|
491
|
+
.optional()
|
|
492
|
+
.describe("要不要被 keepalive 心跳保温 (空闲时定期 ping 一下防 prompt cache 过期)。false = 永远不保温, 省下那份 ping 的钱 —— 适合跑腿一次就收工的; true = 明确要保温 —— 适合会长期挂着、随时可能被叫醒接手的。省略则按 daemon 配置的默认值。"),
|
|
493
|
+
};
|
|
494
|
+
const bear = (tool, inherit) => async (a) => unwrap(tool, await daemonPost("/wizard/clone", {
|
|
495
|
+
inherit,
|
|
496
|
+
description: a.description,
|
|
497
|
+
...(a.name ? { name: a.name } : {}),
|
|
498
|
+
...(a.task ? { task: a.task } : {}),
|
|
499
|
+
...(a.from ? { from: a.from } : {}),
|
|
500
|
+
...(a.job ? { job: a.job } : {}),
|
|
501
|
+
...(a.cwd ? { cwd: a.cwd } : {}),
|
|
502
|
+
...(a.chat ? { chat: a.chat } : {}),
|
|
503
|
+
...(a.cli ? { cli: a.cli } : {}),
|
|
504
|
+
...(a.model ? { model: a.model } : {}),
|
|
505
|
+
...(a.keepalive !== undefined ? { keepalive: a.keepalive } : {}),
|
|
506
|
+
}));
|
|
507
|
+
const OFFSPRING_TAIL = "名字全局唯一: 撞上一个还在的 wizard 返回 409 (附它的死活), 返回里的 `name` 是落定的名字。之后用 send_peer 继续派活、wait_peer 等它做完、stop_wizard 收掉它。它自己也能再 spawn_wizard / clone_wizard, 层级不限。每一个都有成本 (一个 pane + 一份上下文), 名下同时活着的有上限; 任务少于两三件时你自己做完更快。";
|
|
508
|
+
server.registerTool("spawn_wizard", {
|
|
509
|
+
title: "Spawn a blank child wizard",
|
|
510
|
+
description: "**从白板生**一个子 wizard —— 新的 tmux pane、自己的全局名字 (`.name`)、自己的职责, 归你管 (家谱里挂在你名下, 工单收工时可整批回收)。它**不继承你的任何上下文**, 只拿到身份。适合干一件与你手头无关的事, 或者要去别的目录 (`cwd`) / 别的聊天里干活。要一个开局就带着你读过的材料的, 用 clone_wizard。\n" +
|
|
511
|
+
"带上 `task` 可以在它就位的同时把第一件活私聊派下去, 省掉一次 send_peer; 要它的回复进群, 就别带 task, 就位后用 `send_peer({public:true})` 转给它。" +
|
|
512
|
+
OFFSPRING_TAIL,
|
|
532
513
|
inputSchema: {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
.describe("true = fork 你此刻的上下文 (它开局就有你读过的材料, 必须留在同一工作区); false = 空白分身, 只继承身份。必填, 没有默认值。"),
|
|
536
|
-
description: z.string().describe("这个分身负责什么, 一句话。它会写进分身的系统提示, 也会出现在名册里让别人看到。"),
|
|
537
|
-
tag: z.string().optional().describe("分身的地址 tag, 不带 '#' (如 'docs'、'fix')。同一聊天内不能重名。省略则按 description 首词生成。"),
|
|
538
|
-
task: z.string().optional().describe("就位后立刻派下去的第一件活。省略则它就位待命。"),
|
|
539
|
-
cwd: z.string().optional().describe("分身的工作区绝对路径。只在 inherit=false 时有意义 —— 换目录与继承上下文互斥。"),
|
|
540
|
-
chat: z.string().optional().describe("把分身生在另一个聊天里 (list_chats 里的名字)。省略 = 你自己的聊天, 这是绝大多数情况。"),
|
|
541
|
-
cli: z.enum(["claude", "claude-internal", "codebuddy"]).optional().describe("分身用哪个 CLI。省略则继承。"),
|
|
542
|
-
model: z.string().optional().describe("分身跑在哪个模型上 (`--model` 的 slug, 如 'opus' / 'sonnet' / 'haiku')。省略用该 CLI 的默认。分身可以和你跑在不同模型上: 要判断力的那一路给 opus, 跑腿的 (grep、跑测试、照着清单改) 给 haiku —— 一批分身不必齐步走。"),
|
|
543
|
-
job: z
|
|
544
|
-
.string()
|
|
545
|
-
.optional()
|
|
546
|
-
.describe("把这个分身归到某个工单名下 (open_job 给的 id)。归了工单的分身出生/派活不再逐条出气泡 —— 五路 fan-out 就是十条交叉气泡, 人读不出结构; 它们攒到 close_job 那一条里一起交代, 过程照旧在各自的详情页。close_job 还会把它们整批回收掉。"),
|
|
514
|
+
...offspringShape,
|
|
515
|
+
cwd: z.string().optional().describe("它的工作区绝对路径。省略 = 跟你同一个目录。"),
|
|
547
516
|
},
|
|
548
|
-
},
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
517
|
+
}, bear("spawn_wizard", false));
|
|
518
|
+
server.registerTool("clone_wizard", {
|
|
519
|
+
title: "Clone a wizard, context and all",
|
|
520
|
+
description: "**克隆**一个 wizard 出分身 —— fork 它此刻的上下文: 分身开局就拥有它已经读过的一切 (规范、目录结构、刚啃完的那份文档), 不必重读; 被克隆的那个毫发无损。默认克隆你自己; `from` 点名就克隆别的 wizard (比如一个已经把某个模块啃透的同伴) —— 分身仍归你管 (占你的名额、随你的工单回收), 只是上下文来自它。分身留在被克隆者的工作区。\n" +
|
|
521
|
+
"这是编排一组「共享同一批材料」的任务的正确姿势: 先让一个 wizard (你自己或某个同伴) 把公共材料读进上下文, 再从它克隆出 N 个分身, 材料只读一遍却进了 N 份上下文。要白纸一张、或要去别的目录, 用 spawn_wizard。被克隆者正在干活时, 分身拿到的是它此刻为止的上下文 —— 想要它读完再分, 先 wait_peer 等它停下。\n" +
|
|
522
|
+
"带上 `task` 可以在它就位的同时把第一件活私聊派下去 (分叉本来就由第一句话触发, 省一次往返)。" +
|
|
523
|
+
OFFSPRING_TAIL,
|
|
524
|
+
inputSchema: {
|
|
525
|
+
...offspringShape,
|
|
526
|
+
from: z.string().optional().describe("克隆谁 —— wizard 的名字 ('fix' / '.fix')。省略 = 克隆你自己。它必须已经有会话 (说过话), 否则返回 409。"),
|
|
527
|
+
},
|
|
528
|
+
}, bear("clone_wizard", true));
|
|
559
529
|
// ── Job: 一次 fan-out 的工单 ────────────────────────────────────────────────
|
|
560
530
|
// 工单不是第二个编排器: 控制流始终在你自己的上下文里 (你自己 spawn、自己 wait、
|
|
561
531
|
// 自己汇总)。守护进程只替你记一本账 —— 谁属于这个活、谁是临时生的、群里出哪两条
|
|
562
532
|
// 气泡、收工时该回收谁。
|
|
563
533
|
server.registerTool("open_job", {
|
|
564
534
|
title: "Open a job for a fan-out",
|
|
565
|
-
description: "开一个**工单**: 你接下来要同时派出两个以上的分身干同一件事时, 先开它。返回一个 id, 把这个 id 传给
|
|
566
|
-
"开了工单之后有三件事不一样: ①
|
|
535
|
+
description: "开一个**工单**: 你接下来要同时派出两个以上的分身干同一件事时, 先开它。返回一个 id, 把这个 id 传给 spawn_wizard / clone_wizard / send_peer 的 `job` 参数, 它们就归到这个工单名下。\n" +
|
|
536
|
+
"开了工单之后有三件事不一样: ① 群里出「开工」与 close_job 的「收工」两条气泡, 人据此读得出这批活的结构 (过程在各自的 rolepage 里, 收工那条会把成员和各自那段活列出来)。② close_job 会把为这个工单生出来的分身**整批回收**, 不必一个个 stop_wizard —— 忘记回收是常态, 每个分身都占着一个 pane 和一份上下文。③ list_jobs 能看到还开着哪些活。\n" +
|
|
567
537
|
"派活时顺手让每个分身**把结论收口成一行** `RESULT: …` (交付物写进文件就回传路径): wait_peer 会把这一行单独摘出来放进 `result`, 你汇总时不必再从八百字里找结论。\n" +
|
|
568
|
-
"只派一个分身、或者只是推某个同伴一把,
|
|
538
|
+
"只派一个分身、或者只是推某个同伴一把, 不用开工单。",
|
|
569
539
|
inputSchema: {
|
|
570
540
|
title: z.string().describe("一句话说清这个工单要干成什么 —— 它会出现在群里的开工气泡上。"),
|
|
571
541
|
plan: z.string().optional().describe("要在开工气泡里一并说明的计划 (打算分几路、各干什么)。省略则只出标题。"),
|
|
@@ -573,7 +543,7 @@ server.registerTool("open_job", {
|
|
|
573
543
|
}, async ({ title, plan }) => unwrap("open_job", await daemonPost("/jobs/open", { title, ...(plan ? { plan } : {}) })));
|
|
574
544
|
server.registerTool("close_job", {
|
|
575
545
|
title: "Close a job and recycle its clones",
|
|
576
|
-
description: "收工: 把汇总结论发进群 (连同成员清单和各自那段活, 每个名字挂它自己的
|
|
546
|
+
description: "收工: 把汇总结论发进群 (连同成员清单和各自那段活, 每个名字挂它自己的 rolepage), 并**把为这个工单生出来的分身整批回收**。被拉来帮忙的长期 wizard 不在回收之列, 你自己也不会被收。\n" +
|
|
577
547
|
"拿到所有分身的结果、汇总完就调它 —— 分身留着不收, 下一次编排就会撞到分身上限。`stop:false` 只结账不回收 (那些分身后面还有用)。",
|
|
578
548
|
inputSchema: {
|
|
579
549
|
job: z.string().describe("open_job 返回的工单 id。"),
|
|
@@ -588,21 +558,23 @@ server.registerTool("list_jobs", {
|
|
|
588
558
|
}, async () => unwrap("list_jobs", await daemonPost("/jobs/list", {})));
|
|
589
559
|
server.registerTool("stop_wizard", {
|
|
590
560
|
title: "Interrupt or end another wizard",
|
|
591
|
-
description: "收掉一个 wizard/分身。mode='interrupt' 只打断它当前这一轮 (等价于群里的 /stop, 它还活着, 可以继续派活); mode='end' 结束它并回收 tmux pane (等价于 /kill, 之后再找它会重新长出一个空白会话)。活干完了就把临时分身 end 掉 —— 每个分身都占着一个 pane 和一份上下文。加 forget=true 连它的身份记录一起抹掉 (名字、职责、记忆), 只在它彻底不会再回来时用。用户说「让
|
|
561
|
+
description: "收掉一个 wizard/分身。mode='interrupt' 只打断它当前这一轮 (等价于群里的 /stop, 它还活着, 可以继续派活); mode='end' 结束它并回收 tmux pane (等价于 /kill, 之后再找它会重新长出一个空白会话)。活干完了就把临时分身 end 掉 —— 每个分身都占着一个 pane 和一份上下文。加 forget=true 连它的身份记录一起抹掉 (名字、职责、记忆), 只在它彻底不会再回来时用。用户说「让 .x 停下」「把那些分身收了」时调它。终结自己也是合法的 (分身干完活自我了结), 只是这次调用不会返回 —— 群里的通知就是回执。",
|
|
592
562
|
inputSchema: {
|
|
593
|
-
|
|
563
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
594
564
|
mode: z.enum(["end", "interrupt"]).optional().describe("'end' 结束并回收 pane (默认); 'interrupt' 只打断当前这一轮。"),
|
|
595
565
|
forget: z.boolean().optional().describe("仅对 end 有效: 连身份记录 (名字/职责/记忆) 一起删除。默认 false —— 身份留着, 下次它回来还是它。"),
|
|
596
566
|
},
|
|
597
|
-
}, async ({
|
|
567
|
+
}, async ({ name, mode, forget }) => unwrap("stop_wizard", await daemonPost("/wizard/stop", { name, ...(mode ? { mode } : {}), ...(forget ? { forget } : {}) })));
|
|
598
568
|
server.registerTool("wizard_remember", {
|
|
599
569
|
title: "Write something into your long-term memory",
|
|
600
|
-
description: "
|
|
570
|
+
description: "写一条长期记忆。它不在对话里 —— 每次 wizard (重)开会话时重新压进系统提示, 所以能跨 /clear、跨交接、跨重启活下来。三种作用域: `self` (默认) 只属于你; `chat` 是**本群共享**的记忆 (这个群里人的习惯、约定), 在这个群出生的每个 wizard 都会读到; `workspace` 是**本工作区共享**的记忆 (这个仓库的硬约束、踩过的坑、'发版前必须更新 CHANGELOG' 这种规矩), 在这个目录下干活的每个 wizard 都会读到。后两者存成 markdown (`~/.wezard/memory/…`), 人也会直接改; 你写的只是一条**提议**, 由定时的记忆整理者去重、改写、合并 (半小时内), 所以不必先读全文查重, 也别为改一个字反复提交。一条一句话, 越具体越有用; 选对作用域 —— 属于群/仓库的别只记在自己身上。传 forget (子串匹配) 删掉 / 提议删掉过时的那条。别拿它存这次任务的临时状态 —— 那种东西属于交接简报。",
|
|
601
571
|
inputSchema: {
|
|
602
572
|
note: z.string().optional().describe("要记住的一句话。"),
|
|
603
573
|
forget: z.string().optional().describe("要忘掉的记忆里的一个子串, 命中的整条删除。"),
|
|
574
|
+
scope: z.enum(["self", "chat", "workspace"]).optional().describe("记在哪: self (默认, 只属于你) / chat (本群共享) / workspace (本工作区共享)。"),
|
|
604
575
|
},
|
|
605
|
-
}, async ({ note, forget }) => unwrap("wizard_remember", await daemonPost("/wizard/remember", {
|
|
576
|
+
}, async ({ note, forget, scope }) => unwrap("wizard_remember", await daemonPost("/wizard/remember", {
|
|
577
|
+
...(scope ? { scope } : {}),
|
|
606
578
|
...(note ? { note } : {}),
|
|
607
579
|
...(forget ? { forget } : {}),
|
|
608
580
|
})));
|