@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/dist/storage.js CHANGED
@@ -180,7 +180,7 @@ function parseQueuedMessageSkills(raw, queueLength) {
180
180
  });
181
181
  }
182
182
  function inferSessionProvider(row) {
183
- if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok" || row.provider === "qoder") {
183
+ if (row.provider === "claude" || row.provider === "codex" || row.provider === "opencode" || row.provider === "grok" || row.provider === "qoder" || row.provider === "pi") {
184
184
  return row.provider;
185
185
  }
186
186
  if (row.runner === "claude-cli" || row.runner === "claude-cli-print") {
@@ -196,6 +196,8 @@ function inferSessionProvider(row) {
196
196
  return "grok";
197
197
  if (row.runner === "qoder-cli-print")
198
198
  return "qoder";
199
+ if (row.runner === "pi-cli-json")
200
+ return "pi";
199
201
  if (/^codex\b/i.test(row.command.trim()))
200
202
  return "codex";
201
203
  if (/^opencode\b/i.test(row.command.trim()))
@@ -204,6 +206,8 @@ function inferSessionProvider(row) {
204
206
  return "grok";
205
207
  if (/^qodercli\b/i.test(row.command.trim()))
206
208
  return "qoder";
209
+ if (/^pi\b/i.test(row.command.trim()))
210
+ return "pi";
207
211
  return /^claude\b/i.test(row.command.trim()) ? "claude" : undefined;
208
212
  }
209
213
  function parseWorktreeInfo(raw) {
@@ -476,6 +480,70 @@ const INIT_SQL = `
476
480
  CREATE INDEX IF NOT EXISTS idx_password_items_vault ON password_items(vault_id);
477
481
  CREATE INDEX IF NOT EXISTS idx_password_items_type ON password_items(type);
478
482
  CREATE INDEX IF NOT EXISTS idx_password_items_updated ON password_items(updated_at);
483
+
484
+ CREATE TABLE IF NOT EXISTS missions (
485
+ id TEXT PRIMARY KEY,
486
+ title TEXT NOT NULL,
487
+ prompt TEXT NOT NULL,
488
+ cwd TEXT NOT NULL,
489
+ status TEXT NOT NULL,
490
+ base_ref TEXT,
491
+ shared_directories TEXT NOT NULL DEFAULT '[]',
492
+ copy_paths TEXT NOT NULL DEFAULT '[]',
493
+ created_at TEXT NOT NULL,
494
+ updated_at TEXT NOT NULL
495
+ );
496
+
497
+ CREATE TABLE IF NOT EXISTS mission_attempts (
498
+ id TEXT PRIMARY KEY,
499
+ mission_id TEXT NOT NULL,
500
+ session_id TEXT,
501
+ provider TEXT NOT NULL,
502
+ state TEXT NOT NULL,
503
+ branch TEXT,
504
+ worktree_path TEXT,
505
+ base_ref TEXT,
506
+ summary TEXT,
507
+ error TEXT,
508
+ created_at TEXT NOT NULL,
509
+ updated_at TEXT NOT NULL,
510
+ FOREIGN KEY(mission_id) REFERENCES missions(id)
511
+ );
512
+
513
+ CREATE TABLE IF NOT EXISTS mission_review_comments (
514
+ id TEXT PRIMARY KEY,
515
+ mission_id TEXT NOT NULL,
516
+ attempt_id TEXT NOT NULL,
517
+ file_path TEXT NOT NULL,
518
+ line INTEGER,
519
+ side TEXT NOT NULL DEFAULT 'new',
520
+ body TEXT NOT NULL,
521
+ status TEXT NOT NULL DEFAULT 'pending',
522
+ created_at TEXT NOT NULL,
523
+ sent_at TEXT,
524
+ resolved_at TEXT,
525
+ FOREIGN KEY(mission_id) REFERENCES missions(id),
526
+ FOREIGN KEY(attempt_id) REFERENCES mission_attempts(id)
527
+ );
528
+
529
+ CREATE TABLE IF NOT EXISTS agent_activity (
530
+ session_id TEXT PRIMARY KEY,
531
+ mission_id TEXT,
532
+ attempt_id TEXT,
533
+ state TEXT NOT NULL,
534
+ title TEXT NOT NULL,
535
+ summary TEXT,
536
+ provider TEXT,
537
+ cwd TEXT,
538
+ updated_at TEXT NOT NULL,
539
+ read_at TEXT
540
+ );
541
+
542
+ CREATE INDEX IF NOT EXISTS idx_missions_updated ON missions(updated_at);
543
+ CREATE INDEX IF NOT EXISTS idx_mission_attempts_mission ON mission_attempts(mission_id);
544
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_mission_attempts_session ON mission_attempts(session_id) WHERE session_id IS NOT NULL;
545
+ CREATE INDEX IF NOT EXISTS idx_mission_comments_attempt ON mission_review_comments(attempt_id, status);
546
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_state ON agent_activity(state, updated_at);
479
547
  `;
480
548
  export function ensureDatabaseFile(dbPath) {
481
549
  const dir = path.dirname(dbPath);
@@ -740,6 +808,97 @@ export class WandStorage {
740
808
  deleteExpiredAuthSessions(now) {
741
809
  this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
742
810
  }
811
+ // ============ Missions / Agent Inbox ============
812
+ saveMission(mission) {
813
+ this.db.prepare(`INSERT INTO missions (
814
+ id, title, prompt, cwd, status, base_ref, shared_directories, copy_paths, created_at, updated_at
815
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
816
+ ON CONFLICT(id) DO UPDATE SET
817
+ title = excluded.title, prompt = excluded.prompt, cwd = excluded.cwd,
818
+ status = excluded.status, base_ref = excluded.base_ref,
819
+ shared_directories = excluded.shared_directories, copy_paths = excluded.copy_paths,
820
+ updated_at = excluded.updated_at`).run(mission.id, mission.title, mission.prompt, mission.cwd, mission.status, mission.worktree.baseRef ?? null, JSON.stringify(mission.worktree.sharedDirectories ?? []), JSON.stringify(mission.worktree.copyPaths ?? []), mission.createdAt, mission.updatedAt);
821
+ }
822
+ getMission(id) {
823
+ const row = this.db.prepare("SELECT * FROM missions WHERE id = ?").get(id);
824
+ return row ? mapMissionRow(row) : null;
825
+ }
826
+ listMissions(includeArchived = false) {
827
+ const rows = this.db.prepare(`SELECT * FROM missions ${includeArchived ? "" : "WHERE status <> 'archived'"} ORDER BY updated_at DESC`).all();
828
+ return rows.map(mapMissionRow);
829
+ }
830
+ updateMissionStatus(id, status, updatedAt = nowIso()) {
831
+ this.db.prepare("UPDATE missions SET status = ?, updated_at = ? WHERE id = ?").run(status, updatedAt, id);
832
+ }
833
+ saveMissionAttempt(attempt) {
834
+ this.db.prepare(`INSERT INTO mission_attempts (
835
+ id, mission_id, session_id, provider, state, branch, worktree_path, base_ref,
836
+ summary, error, created_at, updated_at
837
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
838
+ ON CONFLICT(id) DO UPDATE SET
839
+ session_id = excluded.session_id, state = excluded.state, branch = excluded.branch,
840
+ worktree_path = excluded.worktree_path, base_ref = excluded.base_ref,
841
+ summary = excluded.summary, error = excluded.error, updated_at = excluded.updated_at`).run(attempt.id, attempt.missionId, attempt.sessionId, attempt.provider, attempt.state, attempt.branch, attempt.worktreePath, attempt.baseRef, attempt.summary, attempt.error, attempt.createdAt, attempt.updatedAt);
842
+ }
843
+ getMissionAttempt(id) {
844
+ const row = this.db.prepare("SELECT * FROM mission_attempts WHERE id = ?").get(id);
845
+ return row ? mapMissionAttemptRow(row) : null;
846
+ }
847
+ getMissionAttemptBySession(sessionId) {
848
+ const row = this.db.prepare("SELECT * FROM mission_attempts WHERE session_id = ?").get(sessionId);
849
+ return row ? mapMissionAttemptRow(row) : null;
850
+ }
851
+ listMissionAttempts(missionId) {
852
+ const rows = this.db.prepare("SELECT * FROM mission_attempts WHERE mission_id = ? ORDER BY created_at ASC").all(missionId);
853
+ return rows.map(mapMissionAttemptRow);
854
+ }
855
+ saveMissionReviewComment(comment) {
856
+ this.db.prepare(`INSERT INTO mission_review_comments (
857
+ id, mission_id, attempt_id, file_path, line, side, body, status, created_at, sent_at, resolved_at
858
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
859
+ ON CONFLICT(id) DO UPDATE SET
860
+ file_path = excluded.file_path, line = excluded.line, side = excluded.side,
861
+ body = excluded.body, status = excluded.status, sent_at = excluded.sent_at,
862
+ resolved_at = excluded.resolved_at`).run(comment.id, comment.missionId, comment.attemptId, comment.filePath, comment.line, comment.side, comment.body, comment.status, comment.createdAt, comment.sentAt, comment.resolvedAt);
863
+ }
864
+ listMissionReviewComments(missionId, attemptId) {
865
+ const rows = attemptId
866
+ ? this.db.prepare("SELECT * FROM mission_review_comments WHERE mission_id = ? AND attempt_id = ? ORDER BY created_at ASC").all(missionId, attemptId)
867
+ : this.db.prepare("SELECT * FROM mission_review_comments WHERE mission_id = ? ORDER BY created_at ASC").all(missionId);
868
+ return rows.map(mapMissionReviewCommentRow);
869
+ }
870
+ updateMissionReviewStatus(ids, status, at = nowIso()) {
871
+ if (ids.length === 0)
872
+ return;
873
+ const placeholders = ids.map(() => "?").join(", ");
874
+ const timestampColumn = status === "sent" ? "sent_at" : status === "resolved" ? "resolved_at" : null;
875
+ const timestampSql = timestampColumn ? `, ${timestampColumn} = ?` : "";
876
+ this.db.prepare(`UPDATE mission_review_comments SET status = ?${timestampSql} WHERE id IN (${placeholders})`)
877
+ .run(status, ...(timestampColumn ? [at] : []), ...ids);
878
+ }
879
+ upsertAgentActivity(item) {
880
+ this.db.prepare(`INSERT INTO agent_activity (
881
+ session_id, mission_id, attempt_id, state, title, summary, provider, cwd, updated_at, read_at
882
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
883
+ ON CONFLICT(session_id) DO UPDATE SET
884
+ mission_id = excluded.mission_id, attempt_id = excluded.attempt_id,
885
+ state = excluded.state, title = excluded.title, summary = excluded.summary,
886
+ provider = excluded.provider, cwd = excluded.cwd, updated_at = excluded.updated_at,
887
+ read_at = CASE WHEN agent_activity.state = excluded.state THEN agent_activity.read_at ELSE excluded.read_at END`).run(item.sessionId, item.missionId, item.attemptId, item.state, item.title, item.summary, item.provider, item.cwd, item.updatedAt, item.readAt);
888
+ }
889
+ listAgentActivity() {
890
+ const rows = this.db.prepare(`SELECT * FROM agent_activity
891
+ ORDER BY CASE state WHEN 'needs_permission' THEN 0 WHEN 'needs_input' THEN 1 WHEN 'working' THEN 2 WHEN 'failed' THEN 3 ELSE 4 END,
892
+ updated_at DESC`).all();
893
+ return rows.map(mapAgentActivityRow);
894
+ }
895
+ markAgentActivityRead(sessionId) {
896
+ const at = nowIso();
897
+ if (sessionId)
898
+ this.db.prepare("UPDATE agent_activity SET read_at = ? WHERE session_id = ?").run(at, sessionId);
899
+ else
900
+ this.db.prepare("UPDATE agent_activity SET read_at = ? WHERE read_at IS NULL").run(at);
901
+ }
743
902
  saveSession(snapshot) {
744
903
  // A single SQLite statement is already atomic. Avoid BEGIN IMMEDIATE in
745
904
  // this hot path so streaming checkpoints do not take an unnecessary write
@@ -820,6 +979,67 @@ export class WandStorage {
820
979
  this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
821
980
  }
822
981
  }
982
+ function mapMissionRow(row) {
983
+ return {
984
+ id: String(row.id),
985
+ title: String(row.title),
986
+ prompt: String(row.prompt),
987
+ cwd: String(row.cwd),
988
+ status: String(row.status),
989
+ worktree: {
990
+ baseRef: typeof row.base_ref === "string" ? row.base_ref : undefined,
991
+ sharedDirectories: safeJsonParse(typeof row.shared_directories === "string" ? row.shared_directories : null) ?? [],
992
+ copyPaths: safeJsonParse(typeof row.copy_paths === "string" ? row.copy_paths : null) ?? [],
993
+ },
994
+ createdAt: String(row.created_at),
995
+ updatedAt: String(row.updated_at),
996
+ };
997
+ }
998
+ function mapMissionAttemptRow(row) {
999
+ return {
1000
+ id: String(row.id),
1001
+ missionId: String(row.mission_id),
1002
+ sessionId: typeof row.session_id === "string" ? row.session_id : null,
1003
+ provider: String(row.provider),
1004
+ state: String(row.state),
1005
+ branch: typeof row.branch === "string" ? row.branch : null,
1006
+ worktreePath: typeof row.worktree_path === "string" ? row.worktree_path : null,
1007
+ baseRef: typeof row.base_ref === "string" ? row.base_ref : null,
1008
+ summary: typeof row.summary === "string" ? row.summary : null,
1009
+ error: typeof row.error === "string" ? row.error : null,
1010
+ createdAt: String(row.created_at),
1011
+ updatedAt: String(row.updated_at),
1012
+ };
1013
+ }
1014
+ function mapMissionReviewCommentRow(row) {
1015
+ return {
1016
+ id: String(row.id),
1017
+ missionId: String(row.mission_id),
1018
+ attemptId: String(row.attempt_id),
1019
+ filePath: String(row.file_path),
1020
+ line: typeof row.line === "number" ? row.line : null,
1021
+ side: row.side === "old" ? "old" : "new",
1022
+ body: String(row.body),
1023
+ status: String(row.status),
1024
+ createdAt: String(row.created_at),
1025
+ sentAt: typeof row.sent_at === "string" ? row.sent_at : null,
1026
+ resolvedAt: typeof row.resolved_at === "string" ? row.resolved_at : null,
1027
+ };
1028
+ }
1029
+ function mapAgentActivityRow(row) {
1030
+ return {
1031
+ sessionId: String(row.session_id),
1032
+ missionId: typeof row.mission_id === "string" ? row.mission_id : null,
1033
+ attemptId: typeof row.attempt_id === "string" ? row.attempt_id : null,
1034
+ state: String(row.state),
1035
+ title: String(row.title),
1036
+ summary: typeof row.summary === "string" ? row.summary : null,
1037
+ provider: typeof row.provider === "string" ? row.provider : null,
1038
+ cwd: typeof row.cwd === "string" ? row.cwd : null,
1039
+ updatedAt: String(row.updated_at),
1040
+ readAt: typeof row.read_at === "string" ? row.read_at : null,
1041
+ };
1042
+ }
823
1043
  function mapPasswordVaultRow(row) {
824
1044
  return {
825
1045
  id: row.id,
@@ -0,0 +1,11 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
3
+ import type { SessionSnapshot } from "./types.js";
4
+ export declare function buildPiArgs(session: SessionSnapshot, prompt: string): string[];
5
+ export declare function piToolName(name: string): string;
6
+ export declare function applyPiEvent(state: StructuredRunnerTurnState, event: Record<string, unknown>): string | null;
7
+ export declare class PiRunner implements StructuredRunnerAdapter {
8
+ private readonly spawnProcess;
9
+ constructor(spawnProcess?: typeof spawn);
10
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
11
+ }
@@ -0,0 +1,135 @@
1
+ import { spawn } from "node:child_process";
2
+ import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
3
+ function record(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5
+ }
6
+ function textContent(value) {
7
+ if (typeof value === "string")
8
+ return value;
9
+ if (Array.isArray(value))
10
+ return value.map((item) => textContent(item)).filter(Boolean).join("\n");
11
+ const item = record(value);
12
+ if (!item)
13
+ return "";
14
+ if (item.type === "text" && typeof item.text === "string")
15
+ return item.text;
16
+ return textContent(item.content);
17
+ }
18
+ export function buildPiArgs(session, prompt) {
19
+ const args = ["--mode", "json", "--print"];
20
+ const model = session.selectedModel?.trim();
21
+ if (model && model !== "default")
22
+ args.push("--model", model);
23
+ const thinking = thinkingEffortToPiLevel(session.thinkingEffort);
24
+ if (thinking)
25
+ args.push("--thinking", thinking);
26
+ if (session.claudeSessionId)
27
+ args.push("--session", session.claudeSessionId);
28
+ args.push(prompt);
29
+ return args;
30
+ }
31
+ export function piToolName(name) {
32
+ const mapped = { bash: "Bash", read: "Read", edit: "Edit", write: "Write", grep: "Grep", find: "Glob", ls: "Glob" };
33
+ return mapped[name.toLowerCase()] ?? `Pi/${name}`;
34
+ }
35
+ export function applyPiEvent(state, event) {
36
+ if (event.type === "session" && typeof event.id === "string")
37
+ state.sessionId = event.id;
38
+ if (event.type === "message_update") {
39
+ const update = record(event.assistantMessageEvent);
40
+ const delta = typeof update?.delta === "string" ? update.delta : "";
41
+ if (update?.type === "text_delta" && delta) {
42
+ const last = state.blocks.at(-1);
43
+ if (last?.type === "text")
44
+ last.text += delta;
45
+ else
46
+ state.blocks.push({ type: "text", text: delta });
47
+ state.result += delta;
48
+ }
49
+ else if (update?.type === "thinking_delta" && delta) {
50
+ const last = state.blocks.at(-1);
51
+ if (last?.type === "thinking")
52
+ last.thinking += delta;
53
+ else
54
+ state.blocks.push({ type: "thinking", thinking: delta });
55
+ }
56
+ }
57
+ if (event.type === "tool_execution_start") {
58
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : crypto.randomUUID();
59
+ const name = typeof event.toolName === "string" ? event.toolName : "tool";
60
+ state.blocks.push({ type: "tool_use", id, name: piToolName(name), input: record(event.args) ?? {} });
61
+ }
62
+ if (event.type === "tool_execution_end") {
63
+ const id = typeof event.toolCallId === "string" ? event.toolCallId : "unknown";
64
+ state.blocks.push({ type: "tool_result", tool_use_id: id, content: textContent(event.result), is_error: event.isError === true });
65
+ }
66
+ if (event.type === "message_end") {
67
+ const message = record(event.message);
68
+ if (message?.role === "assistant") {
69
+ if (typeof message.model === "string")
70
+ state.model = message.model;
71
+ const usage = record(message.usage);
72
+ const cost = record(usage?.cost);
73
+ if (usage)
74
+ state.usage = {
75
+ inputTokens: typeof usage.input === "number" ? usage.input : 0,
76
+ outputTokens: typeof usage.output === "number" ? usage.output : 0,
77
+ cacheReadInputTokens: typeof usage.cacheRead === "number" ? usage.cacheRead : 0,
78
+ cacheCreationInputTokens: typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0,
79
+ totalCostUsd: typeof cost?.total === "number" ? cost.total : 0,
80
+ };
81
+ if (message.stopReason === "error")
82
+ return typeof message.errorMessage === "string" ? message.errorMessage : "Pi CLI execution failed";
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ export class PiRunner {
88
+ spawnProcess;
89
+ constructor(spawnProcess = spawn) {
90
+ this.spawnProcess = spawnProcess;
91
+ }
92
+ start(context, observer) {
93
+ const args = buildPiArgs(context.session, context.prompt);
94
+ const spawnedAt = new Date().toISOString();
95
+ const child = this.spawnProcess("pi", args, { cwd: context.session.cwd, env: context.env, stdio: ["ignore", "pipe", "pipe"] });
96
+ const state = { blocks: [], result: "", sessionId: context.session.claudeSessionId, model: context.session.selectedModel ?? undefined };
97
+ let lineBuffer = "", stderr = "", primaryError = null, settled = false;
98
+ const result = (exitCode, signal, spawnError) => ({ state, exitCode, signal, stderr, primaryError, ...(spawnError ? { spawnError } : {}) });
99
+ const processLine = (line) => {
100
+ if (!observer.isActive() || !line.trim())
101
+ return;
102
+ try {
103
+ const event = JSON.parse(line);
104
+ observer.onEvent?.(event);
105
+ primaryError = applyPiEvent(state, event) ?? primaryError;
106
+ observer.onUpdate(state);
107
+ }
108
+ catch { /* Pi stdout is NDJSON; ignore non-protocol noise. */ }
109
+ };
110
+ const completion = new Promise((resolve) => {
111
+ child.stdout?.on("data", (chunk) => {
112
+ const text = chunk.toString();
113
+ observer.onStdout?.(text);
114
+ lineBuffer += text;
115
+ const lines = lineBuffer.split("\n");
116
+ lineBuffer = lines.pop() ?? "";
117
+ lines.forEach(processLine);
118
+ });
119
+ child.stderr?.on("data", (chunk) => { const text = chunk.toString(); observer.onStderr?.(text); stderr += text; });
120
+ child.on("error", (error) => { if (!settled) {
121
+ settled = true;
122
+ resolve(result(null, null, error));
123
+ } });
124
+ child.on("close", (code, signal) => { if (!settled) {
125
+ settled = true;
126
+ processLine(lineBuffer);
127
+ resolve(result(code, signal));
128
+ } });
129
+ });
130
+ return { args, spawnedAt, pid: child.pid ?? null, completion, interrupt: () => { try {
131
+ child.kill("SIGTERM");
132
+ }
133
+ catch { /* best effort */ } } };
134
+ }
135
+ }
@@ -11,3 +11,4 @@ export declare function thinkingEffortToCodexReasoningEffort(effort: SessionSnap
11
11
  export declare function thinkingEffortToOpenCodeVariant(effort: SessionSnapshot["thinkingEffort"]): string | null;
12
12
  export declare function thinkingEffortToGrokEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
13
13
  export declare function thinkingEffortToQoderEffort(effort: SessionSnapshot["thinkingEffort"]): string | null;
14
+ export declare function thinkingEffortToPiLevel(effort: SessionSnapshot["thinkingEffort"]): string | null;
@@ -7,7 +7,9 @@ export function isStructuredRunnerForProvider(provider, runner) {
7
7
  return runner === "opencode-cli-run";
8
8
  if (provider === "grok")
9
9
  return runner === "grok-cli-headless";
10
- return runner === "qoder-cli-print";
10
+ if (provider === "qoder")
11
+ return runner === "qoder-cli-print";
12
+ return runner === "pi-cli-json";
11
13
  }
12
14
  export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli") {
13
15
  if (provider === "codex")
@@ -18,6 +20,8 @@ export function defaultStructuredRunner(provider, configuredClaudeRunner = "cli"
18
20
  return "grok-cli-headless";
19
21
  if (provider === "qoder")
20
22
  return "qoder-cli-print";
23
+ if (provider === "pi")
24
+ return "pi-cli-json";
21
25
  return configuredClaudeRunner === "sdk" ? "claude-sdk" : "claude-cli-print";
22
26
  }
23
27
  export function resolveStructuredRunner(provider, requestedRunner, configuredClaudeRunner = "cli") {
@@ -105,3 +109,14 @@ export function thinkingEffortToQoderEffort(effort) {
105
109
  return "max";
106
110
  return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
107
111
  }
112
+ export function thinkingEffortToPiLevel(effort) {
113
+ if (!effort || effort === "off")
114
+ return "off";
115
+ if (effort === "standard")
116
+ return "low";
117
+ if (effort === "deep")
118
+ return "high";
119
+ if (effort === "max")
120
+ return "xhigh";
121
+ return effort.startsWith("codex:") ? effort.slice("codex:".length) || null : null;
122
+ }
@@ -1,4 +1,5 @@
1
1
  import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
2
+ import { type WorktreeSetupSpec } from "./git-worktree.js";
2
3
  import { SessionLogger } from "./session-logger.js";
3
4
  import { WandStorage } from "./storage.js";
4
5
  import { ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
@@ -10,6 +11,7 @@ export interface StructuredSessionManagerRunners {
10
11
  opencode?: StructuredRunnerAdapter;
11
12
  grok?: StructuredRunnerAdapter;
12
13
  qoder?: StructuredRunnerAdapter;
14
+ pi?: StructuredRunnerAdapter;
13
15
  }
14
16
  interface CreateStructuredSessionOptions {
15
17
  cwd: string;
@@ -17,6 +19,7 @@ interface CreateStructuredSessionOptions {
17
19
  provider?: SessionProvider;
18
20
  runner?: SessionRunner;
19
21
  worktreeEnabled?: boolean;
22
+ worktreeSpec?: WorktreeSetupSpec;
20
23
  /** 用户指定的模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
21
24
  model?: string;
22
25
  /** 用户预设的思考深度。留空 / null 视为 off。 */
@@ -86,6 +89,7 @@ export declare class StructuredSessionManager {
86
89
  private readonly openCodeRunner;
87
90
  private readonly grokRunner;
88
91
  private readonly qoderRunner;
92
+ private readonly piRunner;
89
93
  private disposed;
90
94
  constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null, sdkQueryFactory?: typeof sdkQuery, runners?: StructuredSessionManagerRunners);
91
95
  private archiveExpiredSessions;
@@ -15,6 +15,7 @@ import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName,
15
15
  import { OpenCodeRunner } from "./structured-opencode-adapter.js";
16
16
  import { GrokRunner } from "./structured-grok-adapter.js";
17
17
  import { QoderRunner } from "./structured-qoder-adapter.js";
18
+ import { PiRunner } from "./structured-pi-adapter.js";
18
19
  import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
19
20
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
20
21
  export { isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToSdkBudget, } from "./structured-provider-common.js";
@@ -209,6 +210,7 @@ export class StructuredSessionManager {
209
210
  openCodeRunner;
210
211
  grokRunner;
211
212
  qoderRunner;
213
+ piRunner;
212
214
  disposed = false;
213
215
  constructor(storage, config, logger = null, sdkQueryFactory = sdkQuery, runners = {}) {
214
216
  this.storage = storage;
@@ -220,12 +222,13 @@ export class StructuredSessionManager {
220
222
  this.openCodeRunner = runners.opencode ?? new OpenCodeRunner();
221
223
  this.grokRunner = runners.grok ?? new GrokRunner();
222
224
  this.qoderRunner = runners.qoder ?? new QoderRunner();
225
+ this.piRunner = runners.pi ?? new PiRunner();
223
226
  for (const snapshot of this.storage.loadSessions()) {
224
227
  if ((snapshot.sessionKind ?? "pty") !== "structured")
225
228
  continue;
226
229
  const restoredStatus = snapshot.status === "running" ? "idle" : snapshot.status;
227
230
  const storedProvider = snapshot.provider ?? snapshot.structuredState?.provider;
228
- const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok" || storedProvider === "qoder"
231
+ const provider = storedProvider === "codex" || storedProvider === "opencode" || storedProvider === "grok" || storedProvider === "qoder" || storedProvider === "pi"
229
232
  ? storedProvider
230
233
  : "claude";
231
234
  const storedRunner = snapshot.runner ?? snapshot.structuredState?.runner;
@@ -526,14 +529,14 @@ export class StructuredSessionManager {
526
529
  const id = randomUUID();
527
530
  const startedAt = new Date().toISOString();
528
531
  const requestedProvider = options.provider ?? "claude";
529
- if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok" && requestedProvider !== "qoder") {
532
+ if (requestedProvider !== "claude" && requestedProvider !== "codex" && requestedProvider !== "opencode" && requestedProvider !== "grok" && requestedProvider !== "qoder" && requestedProvider !== "pi") {
530
533
  throw new Error(`不支持的结构化 provider: ${String(requestedProvider)}`);
531
534
  }
532
535
  const provider = requestedProvider;
533
536
  const runner = resolveStructuredRunner(provider, options.runner, this.config.structuredRunner);
534
537
  const baseCwd = resolveSessionCwd(options.cwd, this.config.defaultCwd);
535
538
  const worktreeSetup = options.worktreeEnabled
536
- ? prepareSessionWorktree({ cwd: baseCwd, sessionId: id })
539
+ ? prepareSessionWorktree({ cwd: baseCwd, sessionId: id, spec: options.worktreeSpec })
537
540
  : null;
538
541
  const selectedModel = options.model?.trim() || null;
539
542
  const initialThinkingEffort = normalizeThinkingEffort(options.thinkingEffort);
@@ -552,9 +555,11 @@ export class StructuredSessionManager {
552
555
  ? "grok -p --output-format streaming-json"
553
556
  : provider === "qoder"
554
557
  ? "qodercli -p --output-format stream-json"
555
- : runner === "claude-sdk"
556
- ? "claude-agent-sdk (stream-json)"
557
- : "claude -p --output-format stream-json",
558
+ : provider === "pi"
559
+ ? "pi --mode json --print"
560
+ : runner === "claude-sdk"
561
+ ? "claude-agent-sdk (stream-json)"
562
+ : "claude -p --output-format stream-json",
558
563
  cwd: worktreeSetup?.cwd ?? baseCwd,
559
564
  mode: options.mode,
560
565
  worktreeEnabled: Boolean(worktreeSetup),
@@ -775,6 +780,15 @@ export class StructuredSessionManager {
775
780
  installHint: "请安装 @qoder-ai/qodercli,或重跑 `wand service:install` 刷新服务的 PATH",
776
781
  });
777
782
  }
783
+ else if (provider === "pi") {
784
+ await this.runClaudeStreaming(id, updated, prompt, requestId, {
785
+ runner: this.piRunner,
786
+ provider: "pi",
787
+ commandLabel: "pi --mode json --print",
788
+ logKind: "pi-json",
789
+ installHint: "请安装 @mariozechner/pi-coding-agent(或兼容的 Pi CLI),或重跑 `wand service:install` 刷新服务的 PATH",
790
+ });
791
+ }
778
792
  else if (runner === "claude-sdk") {
779
793
  await this.runClaudeSdkStreaming(id, updated, prompt, requestId, skills);
780
794
  }
@@ -1806,7 +1820,7 @@ export class StructuredSessionManager {
1806
1820
  flushEmit();
1807
1821
  if (result.spawnError) {
1808
1822
  const hint = result.spawnError.code === "ENOENT"
1809
- ? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
1823
+ ? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
1810
1824
  : "";
1811
1825
  throw new Error(`${commandLabel} 启动失败:${result.spawnError.message}${hint}`);
1812
1826
  }
package/dist/system-ai.js CHANGED
@@ -260,7 +260,7 @@ export function discoverCliSystemAiConfigs(preferred, home = os.homedir()) {
260
260
  const found = [];
261
261
  const seen = new Set();
262
262
  for (const provider of [...new Set(order)]) {
263
- if (provider === "qoder")
263
+ if (provider === "qoder" || provider === "pi")
264
264
  continue;
265
265
  let discovered;
266
266
  try {
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type SessionKind = "pty" | "structured";
2
- export type SessionProvider = "claude" | "codex" | "opencode" | "grok" | "qoder";
2
+ export type SessionProvider = "claude" | "codex" | "opencode" | "grok" | "qoder" | "pi";
3
3
  export type CommitAiSource = "cli" | "api";
4
- export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "qoder-cli-print" | "pty";
4
+ export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "opencode-cli-run" | "grok-cli-headless" | "qoder-cli-print" | "pi-cli-json" | "pty";
5
5
  export type SessionSource = "interactive" | "automation" | "startup";
6
6
  export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
7
7
  export type AutonomyPolicy = "assist" | "agent" | "agent-max";
@@ -116,6 +116,8 @@ export interface WandConfig {
116
116
  defaultGrokModel?: string;
117
117
  /** 新建 Qoder 会话时默认使用的模型层级。留空则由 qodercli 自行决定。 */
118
118
  defaultQoderModel?: string;
119
+ /** 新建 Pi 会话时默认使用的 provider/model pattern。 */
120
+ defaultPiModel?: string;
119
121
  /** 快捷提交生成 commit message / tag 时使用的 CLI。 */
120
122
  commitCli?: SessionProvider;
121
123
  /** 快捷提交专用模型。留空则跟随所选 CLI 的默认模型。 */
@@ -187,9 +189,13 @@ export interface ReasoningEffortInfo {
187
189
  * 避免 `max`(旧值代表 xhigh)与 Codex 新增的原生 max 档位冲突。
188
190
  */
189
191
  export type ThinkingEffort = "off" | "standard" | "deep" | "max" | `codex:${string}`;
190
- interface WorktreeInfo {
192
+ export interface WorktreeInfo {
191
193
  branch: string;
192
194
  path: string;
195
+ /** Git ref each task worktree was created from. */
196
+ baseRef?: string;
197
+ /** Main checkout root, used by task review without rediscovering it. */
198
+ repoRoot?: string;
193
199
  }
194
200
  export interface WorktreeMergeInfo {
195
201
  targetBranch?: string;
@@ -604,4 +610,3 @@ export interface TaskData {
604
610
  export interface SessionEndData {
605
611
  exitCode: number | null;
606
612
  }
607
- export {};