@co0ontty/wand 4.27.1 → 4.28.0

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.
Files changed (49) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli-api.d.ts +11 -0
  4. package/dist/cli-api.js +76 -0
  5. package/dist/cli.js +97 -0
  6. package/dist/config.d.ts +4 -3
  7. package/dist/config.js +20 -3
  8. package/dist/git-quick-commit.js +47 -1
  9. package/dist/git-worktree.d.ts +11 -0
  10. package/dist/git-worktree.js +72 -3
  11. package/dist/mission-diff.d.ts +8 -0
  12. package/dist/mission-diff.js +65 -0
  13. package/dist/mission-types.d.ts +90 -0
  14. package/dist/mission-types.js +1 -0
  15. package/dist/missions.d.ts +32 -0
  16. package/dist/missions.js +344 -0
  17. package/dist/models.d.ts +1 -0
  18. package/dist/models.js +10 -1
  19. package/dist/path-repair.js +1 -1
  20. package/dist/process-manager.js +14 -1
  21. package/dist/prompt-optimizer.d.ts +2 -2
  22. package/dist/prompt-optimizer.js +11 -32
  23. package/dist/provider-cli-updater.d.ts +1 -1
  24. package/dist/provider-cli-updater.js +8 -0
  25. package/dist/resume-policy.js +5 -3
  26. package/dist/server-mission-routes.d.ts +3 -0
  27. package/dist/server-mission-routes.js +81 -0
  28. package/dist/server-session-routes.js +5 -3
  29. package/dist/server-settings-routes.js +5 -0
  30. package/dist/server-update-routes.js +1 -1
  31. package/dist/server.js +31 -5
  32. package/dist/session-ai-context.d.ts +3 -3
  33. package/dist/session-ai-context.js +14 -18
  34. package/dist/storage.d.ts +15 -0
  35. package/dist/storage.js +221 -1
  36. package/dist/structured-pi-adapter.d.ts +11 -0
  37. package/dist/structured-pi-adapter.js +135 -0
  38. package/dist/structured-provider-common.d.ts +1 -0
  39. package/dist/structured-provider-common.js +16 -1
  40. package/dist/structured-session-manager.d.ts +4 -0
  41. package/dist/structured-session-manager.js +21 -7
  42. package/dist/system-ai.js +1 -1
  43. package/dist/types.d.ts +9 -4
  44. package/dist/web-ui/content/scripts.js +146 -69
  45. package/dist/web-ui/embedded-assets.d.ts +1 -1
  46. package/dist/web-ui/embedded-assets.js +2 -2
  47. package/dist/web-ui/provider-identity.d.ts +2 -1
  48. package/dist/web-ui/provider-identity.js +9 -1
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ### Overview
13
13
 
14
- Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), and [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
14
+ Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), and [Pi](https://pi.dev/), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
15
15
 
16
16
  The browser password manager extension source lives in `browser-extension/`; setup and supported baseline features are documented in [docs/browser-extension.md](docs/browser-extension.md).
17
17
 
@@ -56,7 +56,7 @@ wand config:password
56
56
  #### Core
57
57
 
58
58
  - **Dual view modes** — switch between raw terminal output and a structured conversation view for the same session
59
- - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, and Qoder CLI
59
+ - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, and Pi
60
60
  - **Session management** — create, archive, and resume Claude, Codex, OpenCode, Grok, and Qoder sessions with their provider-native context; restore Claude/Codex native history; show summaries in the session list
61
61
  - **Permission control** — visual permission prompts with one-time approval, per-turn memory, and related policies
62
62
 
@@ -73,7 +73,7 @@ wand config:password
73
73
  - **Native clients** — Android, iOS, and macOS clients with encrypted connection codes and update checks
74
74
  - **HTTPS** — optional self-signed certificates for remote or mobile access
75
75
  - **Update channels** — built-in stable/beta update checks and upgrade prompts
76
- - **CLI updates** — check and quickly update Claude Code, Codex, and OpenCode from server settings, with optional automatic updates
76
+ - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, and Pi from server settings, with optional automatic updates
77
77
 
78
78
  ### Configuration
79
79
 
@@ -155,7 +155,7 @@ npm install -g opencode-ai@latest
155
155
 
156
156
  ### 概览
157
157
 
158
- 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
158
+ 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start) 和 [Pi](https://pi.dev/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
159
159
 
160
160
  <p align="center">
161
161
  <img src="docs/screenshots/clients-overview.png" width="100%" alt="Web 居中,iOS 和 Android 客户端在两侧的多端详情总览" />
@@ -205,7 +205,7 @@ npm install -g opencode-ai@latest
205
205
  #### 核心
206
206
 
207
207
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
208
- - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok 和 Qoder CLI 均可创建 PTY 或结构化会话
208
+ - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI Pi 均可创建 PTY 或结构化会话
209
209
  - **会话管理** — 创建、归档并携带原生上下文恢复 Claude、Codex、OpenCode、Grok、Qoder 会话;支持恢复 Claude/Codex 原生历史记录;会话列表显示摘要
210
210
  - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
211
211
 
@@ -222,7 +222,7 @@ npm install -g opencode-ai@latest
222
222
  - **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
223
223
  - **HTTPS** — 可选自签证书,适合远程或移动端访问
224
224
  - **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
225
- - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode,也可开启自动更新
225
+ - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI 和 Pi,也可开启自动更新
226
226
 
227
227
  ### 截图
228
228
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "ccfdc6d997cd39ec9e87d4971ce404385e732dc6",
3
- "builtAt": "2026-08-01T13:34:09.306Z",
4
- "version": "4.27.1",
2
+ "commit": "53c936da2a9d14e6927295c028a77c6d0abd562e",
3
+ "builtAt": "2026-08-05T01:35:22.744Z",
4
+ "version": "4.28.0",
5
5
  "channel": "stable"
6
6
  }
@@ -0,0 +1,11 @@
1
+ import type { WandConfig } from "./types.js";
2
+ export declare class WandCliApi {
3
+ private readonly config;
4
+ private cookie;
5
+ constructor(config: WandConfig);
6
+ get<T>(pathname: string): Promise<T>;
7
+ post<T>(pathname: string, body: unknown): Promise<T>;
8
+ private login;
9
+ private request;
10
+ private rawRequest;
11
+ }
@@ -0,0 +1,76 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { request as httpsRequest } from "node:https";
3
+ export class WandCliApi {
4
+ config;
5
+ cookie = "";
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+ async get(pathname) {
10
+ return this.request("GET", pathname);
11
+ }
12
+ async post(pathname, body) {
13
+ return this.request("POST", pathname, body);
14
+ }
15
+ async login() {
16
+ const result = await this.rawRequest("POST", "/api/login", { password: this.config.password });
17
+ if (result.status < 200 || result.status >= 300)
18
+ throw new Error(`Wand 登录失败 (HTTP ${result.status})`);
19
+ this.cookie = result.cookies.map((value) => value.split(";", 1)[0]).join("; ");
20
+ if (!this.cookie)
21
+ throw new Error("Wand 服务未返回登录会话。");
22
+ }
23
+ async request(method, pathname, body) {
24
+ if (!this.cookie)
25
+ await this.login();
26
+ const result = await this.rawRequest(method, pathname, body, this.cookie);
27
+ let parsed = {};
28
+ try {
29
+ parsed = result.body ? JSON.parse(result.body) : {};
30
+ }
31
+ catch { /* handled below */ }
32
+ const error = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string"
33
+ ? parsed.error
34
+ : null;
35
+ if (result.status < 200 || result.status >= 300 || error) {
36
+ throw new Error(error || `Wand 请求失败 (HTTP ${result.status})`);
37
+ }
38
+ return parsed;
39
+ }
40
+ rawRequest(method, pathname, body, cookie) {
41
+ return new Promise((resolve, reject) => {
42
+ const protocol = this.config.https ? "https:" : "http:";
43
+ const url = new URL(`${protocol}//127.0.0.1:${this.config.port}${pathname}`);
44
+ const payload = body === undefined ? "" : JSON.stringify(body);
45
+ const request = protocol === "https:" ? httpsRequest : httpRequest;
46
+ const req = request(url, {
47
+ method,
48
+ rejectUnauthorized: false,
49
+ timeout: 30_000,
50
+ headers: {
51
+ Accept: "application/json",
52
+ ...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
53
+ ...(cookie ? { Cookie: cookie } : {}),
54
+ },
55
+ }, (res) => {
56
+ let responseBody = "";
57
+ res.setEncoding("utf8");
58
+ res.on("data", (chunk) => {
59
+ responseBody += chunk;
60
+ if (responseBody.length > 20 * 1024 * 1024)
61
+ req.destroy(new Error("Wand 响应超过 20 MB。"));
62
+ });
63
+ res.on("end", () => resolve({
64
+ status: res.statusCode ?? 0,
65
+ body: responseBody,
66
+ cookies: res.headers["set-cookie"] ?? [],
67
+ }));
68
+ });
69
+ req.on("timeout", () => req.destroy(new Error("连接 Wand 服务超时。")));
70
+ req.on("error", reject);
71
+ if (payload)
72
+ req.write(payload);
73
+ req.end();
74
+ });
75
+ }
76
+ }
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import process from "node:process";
9
9
  import { hasConfigFile, isPreferenceKey, loadConfigWithStorage, resolveConfigPath, saveConfig, writePreferenceToStorage, } from "./config.js";
10
10
  import { isPidAlive, readPidfile, removePidfile, removeSocketFile, socketPath, writePidfile, } from "./pidfile.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
+ import { WandCliApi } from "./cli-api.js";
12
13
  async function main() {
13
14
  const args = process.argv.slice(2);
14
15
  const command = args[0] || "help";
@@ -167,6 +168,19 @@ async function main() {
167
168
  }
168
169
  break;
169
170
  }
171
+ case "session:list":
172
+ case "session:read":
173
+ case "session:send":
174
+ case "session:wait":
175
+ case "inbox:list":
176
+ case "mission:list":
177
+ case "mission:create":
178
+ case "mission:diff":
179
+ case "mission:review":
180
+ case "mission:review:send": {
181
+ await runAgentCliCommand(command, args, configPath);
182
+ break;
183
+ }
170
184
  case "service:install":
171
185
  case "service:uninstall":
172
186
  case "service:start":
@@ -203,6 +217,18 @@ Commands:
203
217
  wand config:password Print current login password
204
218
  wand config:set Update a simple config value
205
219
 
220
+ Agent runtime:
221
+ wand session:list List sessions as JSON
222
+ wand session:read <id> Read a complete session snapshot
223
+ wand session:send <id> <text>
224
+ wand session:wait <id> [--timeout 900]
225
+ wand inbox:list List global Agent Inbox items
226
+ wand mission:list List parallel tasks
227
+ wand mission:create --prompt <text> --cwd <path> --providers claude,codex
228
+ wand mission:diff <mission-id> <attempt-id>
229
+ wand mission:review <mission-id> <attempt-id> --file <path> --body <text> [--line N]
230
+ wand mission:review:send <mission-id> <attempt-id>
231
+
206
232
  System service (default = system-wide; pass --user for user-level):
207
233
  wand service:install Register and start the background service (needs sudo for system)
208
234
  wand service:uninstall Stop and remove the service
@@ -220,6 +246,77 @@ Options:
220
246
  --lines <N> (service:logs) Number of log lines (default 80)
221
247
  `);
222
248
  }
249
+ async function runAgentCliCommand(command, args, configPath) {
250
+ const { config } = await loadConfigForCli(configPath);
251
+ const api = new WandCliApi(config);
252
+ const output = (value) => { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); };
253
+ const required = (value, usage) => {
254
+ if (!value)
255
+ throw new Error(`Usage: ${usage}`);
256
+ return value;
257
+ };
258
+ switch (command) {
259
+ case "session:list": return output(await api.get("/api/sessions"));
260
+ case "session:read": {
261
+ const id = required(args[1], "wand session:read <id>");
262
+ return output(await api.get(`/api/sessions/${encodeURIComponent(id)}`));
263
+ }
264
+ case "session:send": {
265
+ const id = required(args[1], "wand session:send <id> <text>");
266
+ const text = required(args[2], "wand session:send <id> <text>");
267
+ return output(await api.post(`/api/sessions/${encodeURIComponent(id)}/input`, { input: text, respondImmediately: true }));
268
+ }
269
+ case "session:wait": {
270
+ const id = required(args[1], "wand session:wait <id> [--timeout 900]");
271
+ const timeoutSeconds = Math.max(1, Number(readFlagValue(args, "--timeout") ?? 900));
272
+ const deadline = Date.now() + timeoutSeconds * 1000;
273
+ while (true) {
274
+ const session = await api.get(`/api/sessions/${encodeURIComponent(id)}`);
275
+ const complete = session.status !== "running" && !session.structuredState?.inFlight;
276
+ if (complete)
277
+ return output(session);
278
+ if (Date.now() >= deadline)
279
+ throw new Error(`等待会话超时:${id}`);
280
+ await new Promise((resolve) => setTimeout(resolve, 500));
281
+ }
282
+ }
283
+ case "inbox:list": return output(await api.get("/api/inbox"));
284
+ case "mission:list": return output(await api.get("/api/missions"));
285
+ case "mission:create": {
286
+ const prompt = required(readFlagValue(args, "--prompt"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
287
+ const cwd = required(readFlagValue(args, "--cwd"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex");
288
+ const providers = required(readFlagValue(args, "--providers"), "wand mission:create --prompt <text> --cwd <path> --providers claude,codex")
289
+ .split(",").map((value) => value.trim()).filter(Boolean);
290
+ return output(await api.post("/api/missions", {
291
+ prompt, cwd, providers,
292
+ title: readFlagValue(args, "--title"),
293
+ baseRef: readFlagValue(args, "--base"),
294
+ sharedDirectories: readFlagValue(args, "--shared")?.split(",").filter(Boolean),
295
+ copyPaths: readFlagValue(args, "--copy")?.split(",").filter(Boolean),
296
+ }));
297
+ }
298
+ case "mission:diff": {
299
+ const missionId = required(args[1], "wand mission:diff <mission-id> <attempt-id>");
300
+ const attemptId = required(args[2], "wand mission:diff <mission-id> <attempt-id>");
301
+ return output(await api.get(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/diff`));
302
+ }
303
+ case "mission:review": {
304
+ const missionId = required(args[1], "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
305
+ const attemptId = required(args[2], "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
306
+ const filePath = required(readFlagValue(args, "--file"), "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
307
+ const body = required(readFlagValue(args, "--body"), "wand mission:review <mission-id> <attempt-id> --file <path> --body <text>");
308
+ const line = readFlagValue(args, "--line");
309
+ return output(await api.post(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/comments`, {
310
+ filePath, body, line: line ? Number(line) : null, side: readFlagValue(args, "--side") === "old" ? "old" : "new",
311
+ }));
312
+ }
313
+ case "mission:review:send": {
314
+ const missionId = required(args[1], "wand mission:review:send <mission-id> <attempt-id>");
315
+ const attemptId = required(args[2], "wand mission:review:send <mission-id> <attempt-id>");
316
+ return output(await api.post(`/api/missions/${encodeURIComponent(missionId)}/attempts/${encodeURIComponent(attemptId)}/review/send`, {}));
317
+ }
318
+ }
319
+ }
223
320
  async function ensureRequiredFiles(configPath, opts = {}) {
224
321
  const { config, dbPath, hadConfig, createdDb, generatedPassword } = await loadConfigForCli(configPath, {
225
322
  ensureInitialPassword: true,
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
12
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
13
  export declare const defaultConfig: () => WandConfig;
@@ -48,11 +48,12 @@ export declare function writePreferenceToStorage(config: WandConfig, storage: Wa
48
48
  export declare function validateCommitAiConfig(_config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
49
49
  export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
50
50
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
51
- export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">): {
51
+ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">): {
52
52
  claude: string;
53
53
  codex: string;
54
54
  opencode: string;
55
55
  grok: string;
56
56
  qoder: string;
57
+ pi: string;
57
58
  };
58
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">, provider: SessionProvider | undefined): string;
59
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">, provider: SessionProvider | undefined): string;
package/dist/config.js CHANGED
@@ -31,6 +31,7 @@ export const PREFERENCE_KEYS = [
31
31
  "defaultOpenCodeModel",
32
32
  "defaultGrokModel",
33
33
  "defaultQoderModel",
34
+ "defaultPiModel",
34
35
  "commitCli",
35
36
  "commitModel",
36
37
  "commitAiSource",
@@ -75,6 +76,7 @@ export const defaultConfig = () => ({
75
76
  defaultOpenCodeModel: "",
76
77
  defaultGrokModel: "",
77
78
  defaultQoderModel: "",
79
+ defaultPiModel: "",
78
80
  commitCli: "claude",
79
81
  commitModel: "",
80
82
  commitAiSource: "cli",
@@ -285,7 +287,7 @@ export function applyStoragePreferences(config, storage) {
285
287
  const defaults = defaultConfig();
286
288
  if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
287
289
  const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
288
- if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder")
290
+ if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder" || v === "pi")
289
291
  config.defaultProvider = v;
290
292
  }
291
293
  if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
@@ -328,6 +330,11 @@ export function applyStoragePreferences(config, storage) {
328
330
  if (typeof v === "string")
329
331
  config.defaultQoderModel = v.trim();
330
332
  }
333
+ if (storage.hasPreference(preferenceStorageKey("defaultPiModel"))) {
334
+ const v = storage.getPreference(preferenceStorageKey("defaultPiModel"), defaults.defaultPiModel ?? "");
335
+ if (typeof v === "string")
336
+ config.defaultPiModel = v.trim();
337
+ }
331
338
  if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
332
339
  const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
333
340
  if (v === "claude" || v === "codex" || v === "opencode")
@@ -378,7 +385,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
378
385
  const dbKey = preferenceStorageKey(key);
379
386
  switch (key) {
380
387
  case "defaultProvider": {
381
- if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder")
388
+ if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder" && value !== "pi")
382
389
  throw new Error(`无效 Provider: ${value}`);
383
390
  storage.setPreference(dbKey, value);
384
391
  config.defaultProvider = value;
@@ -434,6 +441,12 @@ export function writePreferenceToStorage(config, storage, key, value, options =
434
441
  config.defaultQoderModel = v;
435
442
  break;
436
443
  }
444
+ case "defaultPiModel": {
445
+ const v = typeof value === "string" ? value.trim() : "";
446
+ storage.setPreference(dbKey, v);
447
+ config.defaultPiModel = v;
448
+ break;
449
+ }
437
450
  case "commitCli": {
438
451
  if (value !== "claude" && value !== "codex" && value !== "opencode")
439
452
  throw new Error(`无效 commit CLI: ${value}`);
@@ -652,13 +665,14 @@ function mergeWithDefaults(input) {
652
665
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
653
666
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
654
667
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
655
- defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" ? input.defaultProvider : "claude",
668
+ defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" || input.defaultProvider === "pi" ? input.defaultProvider : "claude",
656
669
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
657
670
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
658
671
  defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
659
672
  defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
660
673
  defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
661
674
  defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
675
+ defaultPiModel: typeof input.defaultPiModel === "string" ? input.defaultPiModel.trim() : defaults.defaultPiModel,
662
676
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
663
677
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
664
678
  commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
@@ -677,6 +691,7 @@ export function getProviderDefaultModels(config) {
677
691
  opencode: (config.defaultOpenCodeModel ?? "").trim(),
678
692
  grok: (config.defaultGrokModel ?? "").trim(),
679
693
  qoder: (config.defaultQoderModel ?? "").trim(),
694
+ pi: (config.defaultPiModel ?? "").trim(),
680
695
  };
681
696
  }
682
697
  export function getDefaultModelForProvider(config, provider) {
@@ -689,6 +704,8 @@ export function getDefaultModelForProvider(config, provider) {
689
704
  return defaults.grok;
690
705
  if (provider === "qoder")
691
706
  return defaults.qoder;
707
+ if (provider === "pi")
708
+ return defaults.pi;
692
709
  return defaults.claude;
693
710
  }
694
711
  function normalizePresetCommand(command) {
@@ -4,7 +4,7 @@ import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
4
  import { callSystemAiTextWithFallback } from "./system-ai.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
6
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
7
- import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, } from "./structured-provider-common.js";
7
+ import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
8
8
  const GIT_TIMEOUT_MS = 1500;
9
9
  const GIT_PUSH_TIMEOUT_MS = 30_000;
10
10
  const MAX_FILE_ENTRIES = 200;
@@ -306,6 +306,7 @@ function normalizeProvider(provider) {
306
306
  || provider === "opencode"
307
307
  || provider === "grok"
308
308
  || provider === "qoder"
309
+ || provider === "pi"
309
310
  ? provider
310
311
  : "claude";
311
312
  }
@@ -406,6 +407,35 @@ function extractQoderText(stdout) {
406
407
  }
407
408
  return (resultText || assistantTexts.join("\n")).trim();
408
409
  }
410
+ function extractPiText(stdout) {
411
+ let text = "";
412
+ for (const line of stdout.split(/\r?\n/)) {
413
+ try {
414
+ const event = JSON.parse(line);
415
+ if (event.type !== "message_end" || event.message?.role !== "assistant" || !Array.isArray(event.message.content))
416
+ continue;
417
+ text = event.message.content
418
+ .filter((block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string")
419
+ .map((block) => block.text)
420
+ .join("\n");
421
+ }
422
+ catch { /* ignore non-protocol stdout */ }
423
+ }
424
+ return text.trim();
425
+ }
426
+ function buildPiTextArgs(prompt, opts, allowTools = false) {
427
+ const args = ["--mode", "json", "--print", "--no-session"];
428
+ if (!allowTools)
429
+ args.push("--no-tools");
430
+ const model = opts.model?.trim();
431
+ if (model && model !== "default")
432
+ args.push("--model", model);
433
+ const level = thinkingEffortToPiLevel(opts.thinkingEffort ?? "off");
434
+ if (level)
435
+ args.push("--thinking", level);
436
+ args.push(prompt);
437
+ return args;
438
+ }
409
439
  function buildGrokTextArgs(prompt, opts, allowTools = false) {
410
440
  const args = ["--no-auto-update", "-p", prompt, "--output-format", "streaming-json"];
411
441
  const model = opts.model?.trim();
@@ -532,6 +562,13 @@ async function callQoderText(prompt, cwd, opts) {
532
562
  throw new QuickCommitError("Qoder 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
533
563
  return text;
534
564
  }
565
+ async function callPiText(prompt, cwd, opts) {
566
+ const stdout = await runCliText("pi", buildPiTextArgs(prompt, opts), "", { cwd, timeoutMs: CODEX_MESSAGE_TIMEOUT_MS, inheritEnv: opts.inheritEnv });
567
+ const text = extractPiText(stdout);
568
+ if (!text)
569
+ throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
570
+ return text;
571
+ }
535
572
  async function callCliAiText(prompt, cwd, language, opts) {
536
573
  const provider = normalizeProvider(opts.provider);
537
574
  if (provider === "codex") {
@@ -543,6 +580,8 @@ async function callCliAiText(prompt, cwd, language, opts) {
543
580
  return callGrokText(prompt, cwd, opts);
544
581
  if (provider === "qoder")
545
582
  return callQoderText(prompt, cwd, opts);
583
+ if (provider === "pi")
584
+ return callPiText(prompt, cwd, opts);
546
585
  return callClaudeText(prompt, cwd, language, opts);
547
586
  }
548
587
  async function callDirectApiText(prompt, systemAi) {
@@ -1196,6 +1235,13 @@ async function runQuickCommitFallbackCli(opts, priorError) {
1196
1235
  inheritEnv: opts.inheritEnv,
1197
1236
  });
1198
1237
  }
1238
+ else if (provider === "pi") {
1239
+ await runCliText("pi", buildPiTextArgs(prompt, opts, true), "", {
1240
+ cwd: opts.cwd,
1241
+ timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
1242
+ inheritEnv: opts.inheritEnv,
1243
+ });
1244
+ }
1199
1245
  else {
1200
1246
  const args = [
1201
1247
  "-p",
@@ -1,7 +1,18 @@
1
1
  import { SessionSnapshot, WorktreeMergeCheckResult, WorktreeMergeResult } from "./types.js";
2
+ export interface WorktreeSetupSpec {
3
+ /** Branch, tag, or commit used as the task's immutable starting point. */
4
+ baseRef?: string;
5
+ /** Human-readable task name used in the generated branch. */
6
+ taskName?: string;
7
+ /** Gitignored repo-relative paths copied into the new worktree. */
8
+ copyPaths?: string[];
9
+ /** Gitignored repo-relative paths symlinked from the main checkout. */
10
+ sharedDirectories?: string[];
11
+ }
2
12
  interface WorktreeSetupOptions {
3
13
  cwd: string;
4
14
  sessionId: string;
15
+ spec?: WorktreeSetupSpec;
5
16
  }
6
17
  interface WorktreeSetupResult {
7
18
  cwd: string;
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync } from "node:fs";
1
+ import { cpSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runGit, runGitAsync, getGitErrorMessage } from "./git-utils.js";
4
4
  const WORKTREE_MERGE_ERROR_CODES = {
@@ -39,6 +39,48 @@ function getCurrentBranch(repoRoot, timeoutMs = DEFAULT_WORKTREE_GIT_TIMEOUT_MS)
39
39
  const branch = runWorktreeGit(["branch", "--show-current"], repoRoot, timeoutMs);
40
40
  return branch || "master";
41
41
  }
42
+ function normalizePortableRelativePath(value) {
43
+ const normalized = value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
44
+ if (!normalized || path.isAbsolute(normalized) || normalized === ".git" || normalized.startsWith(".git/") || normalized.split("/").includes("..")) {
45
+ throw new Error(`无效的 worktree 共享路径:${value}`);
46
+ }
47
+ return normalized;
48
+ }
49
+ function ensureIgnoredPath(repoRoot, relativePath) {
50
+ try {
51
+ runGit(["check-ignore", "--quiet", "--", relativePath], repoRoot);
52
+ }
53
+ catch {
54
+ throw new Error(`仅允许复制或共享被 gitignore 忽略的路径:${relativePath}`);
55
+ }
56
+ }
57
+ function populateWorktreePaths(repoRoot, worktreePath, spec) {
58
+ for (const rawPath of spec.copyPaths ?? []) {
59
+ const relativePath = normalizePortableRelativePath(rawPath);
60
+ ensureIgnoredPath(repoRoot, relativePath);
61
+ const source = path.join(repoRoot, relativePath);
62
+ if (!existsSync(source))
63
+ throw new Error(`要复制的路径不存在:${relativePath}`);
64
+ const destination = path.join(worktreePath, relativePath);
65
+ if (existsSync(destination))
66
+ throw new Error(`Worktree 中目标路径已存在:${relativePath}`);
67
+ mkdirSync(path.dirname(destination), { recursive: true });
68
+ cpSync(source, destination, { recursive: true, preserveTimestamps: true });
69
+ }
70
+ for (const rawPath of spec.sharedDirectories ?? []) {
71
+ const relativePath = normalizePortableRelativePath(rawPath);
72
+ ensureIgnoredPath(repoRoot, relativePath);
73
+ const source = path.join(repoRoot, relativePath);
74
+ if (!existsSync(source) || !lstatSync(source).isDirectory()) {
75
+ throw new Error(`要共享的目录不存在:${relativePath}`);
76
+ }
77
+ const destination = path.join(worktreePath, relativePath);
78
+ if (existsSync(destination))
79
+ throw new Error(`Worktree 中目标路径已存在:${relativePath}`);
80
+ mkdirSync(path.dirname(destination), { recursive: true });
81
+ symlinkSync(source, destination, "dir");
82
+ }
83
+ }
42
84
  function ensureWorktreePath(worktree) {
43
85
  const worktreePath = path.resolve(worktree.path);
44
86
  if (!existsSync(worktreePath)) {
@@ -321,18 +363,45 @@ export function prepareSessionWorktree(options) {
321
363
  throw new Error("当前目录不在 git 仓库中,无法启用 worktree 模式。");
322
364
  }
323
365
  const baseBranch = getCurrentBranch(repoRoot);
366
+ const requestedBaseRef = options.spec?.baseRef?.trim() || baseBranch;
367
+ let baseCommit;
368
+ try {
369
+ baseCommit = runGit(["rev-parse", "--verify", `${requestedBaseRef}^{commit}`], repoRoot);
370
+ }
371
+ catch {
372
+ throw new Error(`Worktree 基线不存在:${requestedBaseRef}`);
373
+ }
324
374
  const branchSuffix = sanitizeBranchSegment(options.sessionId.split("-")[0] || options.sessionId);
325
- const branchName = `wand/${sanitizeBranchSegment(baseBranch)}-${branchSuffix}`;
375
+ const branchPrefix = sanitizeBranchSegment(options.spec?.taskName || baseBranch);
376
+ const branchName = `wand/${branchPrefix}-${branchSuffix}`;
326
377
  const worktreesRoot = path.join(repoRoot, ".wand-worktrees");
327
378
  const worktreePath = path.join(worktreesRoot, branchName.replace(/\//g, "-"));
328
379
  mkdirSync(worktreesRoot, { recursive: true });
329
- runGit(["worktree", "add", "-b", branchName, worktreePath, "HEAD"], repoRoot);
380
+ runGit(["worktree", "add", "-b", branchName, worktreePath, baseCommit], repoRoot);
381
+ try {
382
+ populateWorktreePaths(repoRoot, worktreePath, options.spec ?? {});
383
+ }
384
+ catch (error) {
385
+ try {
386
+ runGit(["worktree", "remove", "--force", worktreePath], repoRoot);
387
+ }
388
+ catch { /* preserve setup error */ }
389
+ try {
390
+ runGit(["branch", "-D", branchName], repoRoot);
391
+ }
392
+ catch { /* preserve setup error */ }
393
+ throw error;
394
+ }
330
395
  return {
331
396
  cwd: worktreePath,
332
397
  worktreeEnabled: true,
333
398
  worktree: {
334
399
  branch: branchName,
335
400
  path: worktreePath,
401
+ // Pin the resolved commit so later movement of the source branch cannot
402
+ // change the mission's review baseline.
403
+ baseRef: baseCommit,
404
+ repoRoot,
336
405
  },
337
406
  };
338
407
  }
@@ -0,0 +1,8 @@
1
+ import type { MissionDiff } from "./mission-types.js";
2
+ export declare function buildMissionDiff(options: {
3
+ missionId: string;
4
+ attemptId: string;
5
+ cwd: string;
6
+ baseRef: string;
7
+ maxPatchBytes?: number;
8
+ }): MissionDiff;