@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
@@ -0,0 +1,65 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { runGit, runGitRaw } from "./git-utils.js";
4
+ const DEFAULT_MAX_PATCH_BYTES = 2 * 1024 * 1024;
5
+ function appendBounded(current, next, maxBytes) {
6
+ const available = maxBytes - Buffer.byteLength(current);
7
+ if (available <= 0)
8
+ return { value: current, truncated: true };
9
+ if (Buffer.byteLength(next) <= available)
10
+ return { value: current + next, truncated: false };
11
+ return { value: current + Buffer.from(next).subarray(0, available).toString("utf8"), truncated: true };
12
+ }
13
+ function parseNameStatus(raw) {
14
+ return raw.split("\n").filter(Boolean).map((line) => {
15
+ const fields = line.split("\t");
16
+ return { status: fields[0] || "M", path: fields[fields.length - 1] || line };
17
+ });
18
+ }
19
+ function untrackedPatch(cwd, relativePath) {
20
+ const result = spawnSync("git", ["diff", "--no-index", "--no-color", "--", "/dev/null", relativePath], {
21
+ cwd,
22
+ encoding: "utf8",
23
+ maxBuffer: DEFAULT_MAX_PATCH_BYTES,
24
+ env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
25
+ });
26
+ if (result.status !== 0 && result.status !== 1) {
27
+ throw new Error(result.stderr?.trim() || `无法读取未跟踪文件 diff:${relativePath}`);
28
+ }
29
+ return result.stdout || "";
30
+ }
31
+ export function buildMissionDiff(options) {
32
+ if (!existsSync(options.cwd))
33
+ throw new Error("任务 worktree 不存在,无法生成 diff。");
34
+ runGit(["rev-parse", "--verify", `${options.baseRef}^{commit}`], options.cwd);
35
+ const maxBytes = options.maxPatchBytes ?? DEFAULT_MAX_PATCH_BYTES;
36
+ const trackedNames = parseNameStatus(runGitRaw(["diff", "--name-status", options.baseRef, "--", "."], options.cwd));
37
+ const untracked = runGitRaw(["ls-files", "--others", "--exclude-standard", "-z"], options.cwd)
38
+ .split("\0")
39
+ .filter(Boolean);
40
+ const files = [...trackedNames, ...untracked.map((file) => ({ path: file, status: "?" }))];
41
+ let patch = runGitRaw(["diff", "--no-color", "--find-renames", options.baseRef, "--", "."], options.cwd);
42
+ let truncated = false;
43
+ if (Buffer.byteLength(patch) > maxBytes) {
44
+ patch = Buffer.from(patch).subarray(0, maxBytes).toString("utf8");
45
+ truncated = true;
46
+ }
47
+ if (!truncated) {
48
+ for (const file of untracked) {
49
+ const appended = appendBounded(patch, untrackedPatch(options.cwd, file), maxBytes);
50
+ patch = appended.value;
51
+ if (appended.truncated) {
52
+ truncated = true;
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ return {
58
+ missionId: options.missionId,
59
+ attemptId: options.attemptId,
60
+ baseRef: options.baseRef,
61
+ files,
62
+ patch,
63
+ truncated,
64
+ };
65
+ }
@@ -0,0 +1,90 @@
1
+ import type { SessionProvider } from "./types.js";
2
+ export type AgentActivityState = "working" | "needs_input" | "needs_permission" | "done" | "failed";
3
+ export type MissionStatus = "dispatching" | "running" | "needs_input" | "completed" | "failed" | "archived";
4
+ export type MissionAttemptState = AgentActivityState | "queued";
5
+ export type MissionReviewStatus = "pending" | "sent" | "resolved";
6
+ export interface MissionWorktreeOptions {
7
+ baseRef?: string;
8
+ sharedDirectories?: string[];
9
+ copyPaths?: string[];
10
+ }
11
+ export interface Mission {
12
+ id: string;
13
+ title: string;
14
+ prompt: string;
15
+ cwd: string;
16
+ status: MissionStatus;
17
+ worktree: MissionWorktreeOptions;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+ export interface MissionAttempt {
22
+ id: string;
23
+ missionId: string;
24
+ sessionId: string | null;
25
+ provider: SessionProvider;
26
+ state: MissionAttemptState;
27
+ branch: string | null;
28
+ worktreePath: string | null;
29
+ baseRef: string | null;
30
+ summary: string | null;
31
+ error: string | null;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+ export interface MissionReviewComment {
36
+ id: string;
37
+ missionId: string;
38
+ attemptId: string;
39
+ filePath: string;
40
+ line: number | null;
41
+ side: "old" | "new";
42
+ body: string;
43
+ status: MissionReviewStatus;
44
+ createdAt: string;
45
+ sentAt: string | null;
46
+ resolvedAt: string | null;
47
+ }
48
+ export interface AgentActivityItem {
49
+ sessionId: string;
50
+ missionId: string | null;
51
+ attemptId: string | null;
52
+ state: AgentActivityState;
53
+ title: string;
54
+ summary: string | null;
55
+ provider: SessionProvider | null;
56
+ cwd: string | null;
57
+ updatedAt: string;
58
+ readAt: string | null;
59
+ }
60
+ export interface MissionDetails extends Mission {
61
+ attempts: MissionAttempt[];
62
+ comments: MissionReviewComment[];
63
+ }
64
+ export interface MissionDiffFile {
65
+ path: string;
66
+ status: string;
67
+ }
68
+ export interface MissionDiff {
69
+ missionId: string;
70
+ attemptId: string;
71
+ baseRef: string;
72
+ files: MissionDiffFile[];
73
+ patch: string;
74
+ truncated: boolean;
75
+ }
76
+ export interface CreateMissionInput {
77
+ prompt: string;
78
+ title?: string;
79
+ cwd: string;
80
+ providers: SessionProvider[];
81
+ baseRef?: string;
82
+ sharedDirectories?: string[];
83
+ copyPaths?: string[];
84
+ }
85
+ export interface CreateReviewCommentInput {
86
+ filePath: string;
87
+ line?: number | null;
88
+ side?: "old" | "new";
89
+ body: string;
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
2
+ import type { SessionRegistry } from "./session-registry.js";
3
+ import type { StructuredSessionManager } from "./structured-session-manager.js";
4
+ import type { WandStorage } from "./storage.js";
5
+ import type { ProcessEvent } from "./types.js";
6
+ /**
7
+ * Deep task-orchestration module. Callers create missions, observe one inbox,
8
+ * and review diffs without coordinating session/worktree/storage details.
9
+ */
10
+ export declare class Missions {
11
+ private readonly storage;
12
+ private readonly structured;
13
+ private readonly sessions;
14
+ constructor(storage: WandStorage, structured: StructuredSessionManager, sessions: SessionRegistry);
15
+ list(): MissionDetails[];
16
+ get(id: string): MissionDetails | null;
17
+ inbox(): AgentActivityItem[];
18
+ markInboxRead(sessionId?: string): void;
19
+ create(input: CreateMissionInput): MissionDetails;
20
+ ingest(event: ProcessEvent): AgentActivityItem | null;
21
+ diff(missionId: string, attemptId: string): MissionDiff;
22
+ addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
23
+ sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
24
+ resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
25
+ archive(id: string): MissionDetails;
26
+ private dispatchAttempt;
27
+ private seedInbox;
28
+ private details;
29
+ private getIncludingArchived;
30
+ private requireAttempt;
31
+ private refreshMissionStatus;
32
+ }
@@ -0,0 +1,344 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { buildMissionDiff } from "./mission-diff.js";
5
+ const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
6
+ const MAX_ATTEMPTS = 6;
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function normalizeStringList(value, field) {
11
+ const result = [...new Set((value ?? []).map((item) => item.trim()).filter(Boolean))];
12
+ if (result.length > 20)
13
+ throw new Error(`${field} 最多允许 20 个路径。`);
14
+ for (const item of result) {
15
+ if (item.length > 500)
16
+ throw new Error(`${field} 中的路径过长。`);
17
+ }
18
+ return result;
19
+ }
20
+ function firstPromptLine(prompt) {
21
+ const line = prompt.split("\n").map((part) => part.trim()).find(Boolean) || "新任务";
22
+ return line.length > 72 ? `${line.slice(0, 69)}…` : line;
23
+ }
24
+ function sessionSummary(snapshot) {
25
+ if (snapshot.description?.trim())
26
+ return snapshot.description.trim();
27
+ if (snapshot.currentTaskTitle?.trim())
28
+ return snapshot.currentTaskTitle.trim();
29
+ const messages = snapshot.messages ?? [];
30
+ for (let turnIndex = messages.length - 1; turnIndex >= 0; turnIndex--) {
31
+ const text = messages[turnIndex].content
32
+ .filter((block) => block.type === "text")
33
+ .map((block) => block.text.trim())
34
+ .filter(Boolean)
35
+ .join(" ");
36
+ if (text)
37
+ return text.length > 240 ? `${text.slice(0, 237)}…` : text;
38
+ }
39
+ return snapshot.summary?.trim() || null;
40
+ }
41
+ function hasUnansweredQuestion(messages) {
42
+ if (!messages?.length || messages[messages.length - 1]?.role === "user")
43
+ return false;
44
+ const answered = new Set();
45
+ for (const turn of messages) {
46
+ for (const block of turn.content) {
47
+ if (block.type === "tool_result")
48
+ answered.add(block.tool_use_id);
49
+ }
50
+ }
51
+ for (let turnIndex = messages.length - 1; turnIndex >= 0; turnIndex--) {
52
+ const turn = messages[turnIndex];
53
+ if (turn.role === "user" && turn.content.some((block) => block.type === "text"))
54
+ break;
55
+ if (turn.content.some((block) => block.type === "tool_use"
56
+ && (block.name === "AskUserQuestion" || block.semantic?.kind === "question_request")
57
+ && !answered.has(block.id)))
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ function activityState(snapshot, event) {
63
+ if (snapshot.pendingEscalation || snapshot.permissionBlocked)
64
+ return "needs_permission";
65
+ if (hasUnansweredQuestion(snapshot.messages))
66
+ return "needs_input";
67
+ if (snapshot.status === "failed" || (event?.type === "ended" && snapshot.exitCode !== null && snapshot.exitCode !== 0))
68
+ return "failed";
69
+ if (snapshot.status === "running" || snapshot.structuredState?.inFlight)
70
+ return "working";
71
+ return "done";
72
+ }
73
+ function missionStatus(attempts) {
74
+ if (attempts.length === 0)
75
+ return "dispatching";
76
+ if (attempts.some((attempt) => attempt.state === "needs_input" || attempt.state === "needs_permission"))
77
+ return "needs_input";
78
+ if (attempts.some((attempt) => attempt.state === "working" || attempt.state === "queued"))
79
+ return "running";
80
+ if (attempts.some((attempt) => attempt.state === "done"))
81
+ return "completed";
82
+ return "failed";
83
+ }
84
+ function reviewPrompt(comments) {
85
+ const lines = comments.map((comment, index) => {
86
+ const location = comment.line === null ? comment.filePath : `${comment.filePath}:${comment.line}`;
87
+ return `${index + 1}. ${location} [${comment.side}] — ${comment.body}`;
88
+ });
89
+ return [
90
+ "Please address the following review feedback for this task.",
91
+ "Apply all requested changes in the current worktree, preserve unrelated work, and run focused verification before replying.",
92
+ "",
93
+ ...lines,
94
+ ].join("\n");
95
+ }
96
+ /**
97
+ * Deep task-orchestration module. Callers create missions, observe one inbox,
98
+ * and review diffs without coordinating session/worktree/storage details.
99
+ */
100
+ export class Missions {
101
+ storage;
102
+ structured;
103
+ sessions;
104
+ constructor(storage, structured, sessions) {
105
+ this.storage = storage;
106
+ this.structured = structured;
107
+ this.sessions = sessions;
108
+ }
109
+ list() {
110
+ return this.storage.listMissions().map((mission) => this.details(mission));
111
+ }
112
+ get(id) {
113
+ const mission = this.storage.getMission(id);
114
+ return mission ? this.details(mission) : null;
115
+ }
116
+ inbox() {
117
+ this.seedInbox();
118
+ return this.storage.listAgentActivity();
119
+ }
120
+ markInboxRead(sessionId) {
121
+ this.storage.markAgentActivityRead(sessionId);
122
+ }
123
+ create(input) {
124
+ const prompt = input.prompt?.trim();
125
+ if (!prompt)
126
+ throw new Error("任务提示词不能为空。");
127
+ if (prompt.length > 200_000)
128
+ throw new Error("任务提示词不能超过 200000 个字符。");
129
+ const cwd = path.resolve(input.cwd || "");
130
+ if (!existsSync(cwd) || !statSync(cwd).isDirectory())
131
+ throw new Error("任务工作目录不存在。");
132
+ const providers = [...new Set(input.providers ?? [])];
133
+ if (providers.length === 0 || providers.length > MAX_ATTEMPTS || providers.some((provider) => !PROVIDERS.has(provider))) {
134
+ throw new Error(`请选择 1-${MAX_ATTEMPTS} 个有效 provider。`);
135
+ }
136
+ const createdAt = nowIso();
137
+ const mission = {
138
+ id: randomUUID(),
139
+ title: input.title?.trim().slice(0, 120) || firstPromptLine(prompt),
140
+ prompt,
141
+ cwd,
142
+ status: "dispatching",
143
+ worktree: {
144
+ baseRef: input.baseRef?.trim() || undefined,
145
+ sharedDirectories: normalizeStringList(input.sharedDirectories, "sharedDirectories"),
146
+ copyPaths: normalizeStringList(input.copyPaths, "copyPaths"),
147
+ },
148
+ createdAt,
149
+ updatedAt: createdAt,
150
+ };
151
+ this.storage.saveMission(mission);
152
+ for (const provider of providers)
153
+ this.dispatchAttempt(mission, provider);
154
+ this.refreshMissionStatus(mission.id);
155
+ return this.get(mission.id);
156
+ }
157
+ ingest(event) {
158
+ if (!event.sessionId || event.sessionId === "__system__")
159
+ return null;
160
+ const snapshot = this.sessions.getLatest(event.sessionId);
161
+ if (!snapshot)
162
+ return null;
163
+ const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
164
+ const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
165
+ const state = activityState(snapshot, event);
166
+ const updatedAt = nowIso();
167
+ const item = {
168
+ sessionId: snapshot.id,
169
+ missionId: attempt?.missionId ?? null,
170
+ attemptId: attempt?.id ?? null,
171
+ state,
172
+ title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
173
+ summary: sessionSummary(snapshot),
174
+ provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
175
+ cwd: snapshot.cwd || null,
176
+ updatedAt,
177
+ readAt: null,
178
+ };
179
+ this.storage.upsertAgentActivity(item);
180
+ if (attempt) {
181
+ this.storage.saveMissionAttempt({
182
+ ...attempt,
183
+ state: state,
184
+ summary: item.summary,
185
+ error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
186
+ updatedAt,
187
+ });
188
+ this.refreshMissionStatus(attempt.missionId);
189
+ }
190
+ return item;
191
+ }
192
+ diff(missionId, attemptId) {
193
+ const attempt = this.requireAttempt(missionId, attemptId);
194
+ if (!attempt.worktreePath || !attempt.baseRef)
195
+ throw new Error("这个 attempt 没有可审查的 worktree。");
196
+ return buildMissionDiff({ missionId, attemptId, cwd: attempt.worktreePath, baseRef: attempt.baseRef });
197
+ }
198
+ addReviewComment(missionId, attemptId, input) {
199
+ this.requireAttempt(missionId, attemptId);
200
+ const body = input.body?.trim();
201
+ const filePath = input.filePath?.trim();
202
+ if (!body || !filePath)
203
+ throw new Error("文件和 review 内容不能为空。");
204
+ const createdAt = nowIso();
205
+ const comment = {
206
+ id: randomUUID(), missionId, attemptId, filePath,
207
+ line: typeof input.line === "number" && Number.isFinite(input.line) ? Math.max(1, Math.floor(input.line)) : null,
208
+ side: input.side === "old" ? "old" : "new",
209
+ body, status: "pending", createdAt, sentAt: null, resolvedAt: null,
210
+ };
211
+ this.storage.saveMissionReviewComment(comment);
212
+ return comment;
213
+ }
214
+ sendReview(missionId, attemptId, commentIds) {
215
+ const attempt = this.requireAttempt(missionId, attemptId);
216
+ if (!attempt.sessionId)
217
+ throw new Error("这个 attempt 没有关联会话。");
218
+ const selected = this.storage.listMissionReviewComments(missionId, attemptId)
219
+ .filter((comment) => comment.status === "pending" && (!commentIds?.length || commentIds.includes(comment.id)));
220
+ if (selected.length === 0)
221
+ throw new Error("没有待发送的 review 意见。");
222
+ const session = this.structured.get(attempt.sessionId);
223
+ if (!session)
224
+ throw new Error("任务会话当前不可用。");
225
+ const completion = this.structured.sendMessage(attempt.sessionId, reviewPrompt(selected));
226
+ completion.catch((error) => {
227
+ console.error(`[Missions] Review dispatch failed for ${attempt.id}:`, error);
228
+ });
229
+ this.storage.updateMissionReviewStatus(selected.map((comment) => comment.id), "sent");
230
+ this.storage.saveMissionAttempt({ ...attempt, state: "working", updatedAt: nowIso() });
231
+ this.refreshMissionStatus(missionId);
232
+ return this.storage.listMissionReviewComments(missionId, attemptId);
233
+ }
234
+ resolveReview(missionId, attemptId, commentIds) {
235
+ this.requireAttempt(missionId, attemptId);
236
+ this.storage.updateMissionReviewStatus(commentIds, "resolved");
237
+ return this.storage.listMissionReviewComments(missionId, attemptId);
238
+ }
239
+ archive(id) {
240
+ const mission = this.storage.getMission(id);
241
+ if (!mission)
242
+ throw new Error("任务不存在。");
243
+ this.storage.updateMissionStatus(id, "archived");
244
+ return this.getIncludingArchived(id);
245
+ }
246
+ dispatchAttempt(mission, provider) {
247
+ const attemptId = randomUUID();
248
+ const createdAt = nowIso();
249
+ let attempt = {
250
+ id: attemptId, missionId: mission.id, sessionId: null, provider, state: "queued",
251
+ branch: null, worktreePath: null, baseRef: mission.worktree.baseRef ?? null,
252
+ summary: null, error: null, createdAt, updatedAt: createdAt,
253
+ };
254
+ this.storage.saveMissionAttempt(attempt);
255
+ try {
256
+ const session = this.structured.createSession({
257
+ cwd: mission.cwd,
258
+ mode: "agent",
259
+ provider,
260
+ worktreeEnabled: true,
261
+ worktreeSpec: {
262
+ baseRef: mission.worktree.baseRef,
263
+ taskName: `${mission.title}-${provider}`,
264
+ sharedDirectories: mission.worktree.sharedDirectories,
265
+ copyPaths: mission.worktree.copyPaths,
266
+ },
267
+ sessionSource: "automation",
268
+ automationId: mission.id,
269
+ });
270
+ attempt = {
271
+ ...attempt,
272
+ sessionId: session.id,
273
+ state: "working",
274
+ branch: session.worktree?.branch ?? null,
275
+ worktreePath: session.worktree?.path ?? null,
276
+ baseRef: session.worktree?.baseRef ?? mission.worktree.baseRef ?? null,
277
+ updatedAt: nowIso(),
278
+ };
279
+ this.storage.saveMissionAttempt(attempt);
280
+ this.storage.upsertAgentActivity({
281
+ sessionId: session.id, missionId: mission.id, attemptId, state: "working",
282
+ title: mission.title, summary: `已分派给 ${provider}`, provider, cwd: session.cwd,
283
+ updatedAt: attempt.updatedAt, readAt: null,
284
+ });
285
+ const completion = this.structured.sendMessage(session.id, mission.prompt);
286
+ completion.catch((error) => {
287
+ console.error(`[Missions] Attempt ${attemptId} failed after dispatch:`, error);
288
+ });
289
+ }
290
+ catch (error) {
291
+ this.storage.saveMissionAttempt({
292
+ ...attempt,
293
+ state: "failed",
294
+ error: error instanceof Error ? error.message : String(error),
295
+ updatedAt: nowIso(),
296
+ });
297
+ }
298
+ }
299
+ seedInbox() {
300
+ const existing = new Set(this.storage.listAgentActivity().map((item) => item.sessionId));
301
+ for (const snapshot of this.sessions.listSlim()) {
302
+ if (existing.has(snapshot.id))
303
+ continue;
304
+ const attempt = this.storage.getMissionAttemptBySession(snapshot.id);
305
+ const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
306
+ const at = nowIso();
307
+ this.storage.upsertAgentActivity({
308
+ sessionId: snapshot.id,
309
+ missionId: attempt?.missionId ?? null,
310
+ attemptId: attempt?.id ?? null,
311
+ state: activityState(snapshot),
312
+ title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
313
+ summary: sessionSummary(snapshot),
314
+ provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
315
+ cwd: snapshot.cwd || null,
316
+ updatedAt: snapshot.endedAt || snapshot.startedAt || at,
317
+ readAt: at,
318
+ });
319
+ }
320
+ }
321
+ details(mission) {
322
+ return {
323
+ ...mission,
324
+ attempts: this.storage.listMissionAttempts(mission.id),
325
+ comments: this.storage.listMissionReviewComments(mission.id),
326
+ };
327
+ }
328
+ getIncludingArchived(id) {
329
+ const mission = this.storage.getMission(id);
330
+ return mission ? this.details(mission) : null;
331
+ }
332
+ requireAttempt(missionId, attemptId) {
333
+ const attempt = this.storage.getMissionAttempt(attemptId);
334
+ if (!attempt || attempt.missionId !== missionId)
335
+ throw new Error("任务 attempt 不存在。");
336
+ return attempt;
337
+ }
338
+ refreshMissionStatus(missionId) {
339
+ const mission = this.storage.getMission(missionId);
340
+ if (!mission || mission.status === "archived")
341
+ return;
342
+ this.storage.updateMissionStatus(missionId, missionStatus(this.storage.listMissionAttempts(missionId)));
343
+ }
344
+ }
package/dist/models.d.ts CHANGED
@@ -42,6 +42,7 @@ export interface ModelCache {
42
42
  opencodeModels: ClaudeModelInfo[];
43
43
  grokModels: ClaudeModelInfo[];
44
44
  qoderModels: ClaudeModelInfo[];
45
+ piModels: ClaudeModelInfo[];
45
46
  claudeVersion: string | null;
46
47
  opencodeVersion: string | null;
47
48
  refreshedAt: string;
package/dist/models.js CHANGED
@@ -53,6 +53,9 @@ const QODER_FALLBACK_MODELS = [
53
53
  { id: "performance", label: "Performance" },
54
54
  { id: "ultimate", label: "Ultimate" },
55
55
  ];
56
+ const PI_FALLBACK_MODELS = [
57
+ { id: "default", label: "跟随 Pi 默认", alias: true },
58
+ ];
56
59
  function cloneModels(models) {
57
60
  return models.map((model) => ({
58
61
  ...model,
@@ -68,6 +71,7 @@ function cloneCache(cache) {
68
71
  opencodeModels: cloneModels(cache.opencodeModels),
69
72
  grokModels: cloneModels(cache.grokModels),
70
73
  qoderModels: cloneModels(cache.qoderModels),
74
+ piModels: cloneModels(cache.piModels),
71
75
  claudeVersion: cache.claudeVersion,
72
76
  opencodeVersion: cache.opencodeVersion,
73
77
  refreshedAt: cache.refreshedAt,
@@ -235,6 +239,7 @@ function createInitialCache(options) {
235
239
  opencodeModels: cloneModels(OPENCODE_FALLBACK_MODELS),
236
240
  grokModels: cloneModels(GROK_FALLBACK_MODELS),
237
241
  qoderModels: cloneModels(QODER_FALLBACK_MODELS),
242
+ piModels: cloneModels(PI_FALLBACK_MODELS),
238
243
  claudeVersion: null,
239
244
  opencodeVersion: null,
240
245
  refreshedAt: now.toISOString(),
@@ -523,6 +528,7 @@ function catalogRevision(cache) {
523
528
  opencodeModels: cache.opencodeModels,
524
529
  grokModels: cache.grokModels,
525
530
  qoderModels: cache.qoderModels,
531
+ piModels: cache.piModels,
526
532
  claudeVersion: cache.claudeVersion,
527
533
  opencodeVersion: cache.opencodeVersion,
528
534
  });
@@ -603,8 +609,9 @@ function parsePersistedModelCatalog(value) {
603
609
  const opencodeModels = parsePersistedModelList(catalog.opencodeModels);
604
610
  const grokModels = parsePersistedModelList(catalog.grokModels);
605
611
  const qoderModels = parsePersistedModelList(catalog.qoderModels);
612
+ const piModels = parsePersistedModelList(catalog.piModels);
606
613
  const refreshedAt = safePersistedString(catalog.refreshedAt, 64);
607
- if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels
614
+ if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels || !piModels
608
615
  || !refreshedAt || Number.isNaN(Date.parse(refreshedAt))) {
609
616
  return null;
610
617
  }
@@ -619,6 +626,7 @@ function parsePersistedModelCatalog(value) {
619
626
  opencodeModels,
620
627
  grokModels,
621
628
  qoderModels,
629
+ piModels,
622
630
  claudeVersion,
623
631
  opencodeVersion,
624
632
  refreshedAt,
@@ -696,6 +704,7 @@ async function discoverModelCache(options, previous) {
696
704
  opencodeModels: opencodeProbe.models.ok ? opencodeProbe.models.value : cloneModels(previous.opencodeModels),
697
705
  grokModels: grokProbe.ok ? grokProbe.value : cloneModels(previous.grokModels),
698
706
  qoderModels: qoderProbe.ok ? qoderProbe.value : cloneModels(previous.qoderModels),
707
+ piModels: cloneModels(previous.piModels),
699
708
  claudeVersion,
700
709
  opencodeVersion: opencodeProbe.version.ok ? opencodeProbe.version.value : previous.opencodeVersion,
701
710
  refreshedAt: now.toISOString(),
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
8
  /** 关键的 CLI 工具,会被诊断输出。 */
9
- const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli"];
9
+ const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli", "pi"];
10
10
  const DEEP_PROBE_TIMEOUT_MS = 4000;
11
11
  /**
12
12
  * 构造候选 bin 目录列表(按优先级,前面的更可信)。
@@ -15,7 +15,7 @@ import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
15
15
  import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
16
16
  import { prepareSessionWorktree } from "./git-worktree.js";
17
17
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
18
- import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
18
+ import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
19
19
  import { SessionTopicCoordinator } from "./session-topic.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
21
  import { resolveSystemAiContext } from "./session-ai-context.js";
@@ -28,6 +28,8 @@ function resolveProviderFromCommand(command) {
28
28
  return "opencode";
29
29
  if (/^grok\b/.test(command.trim()))
30
30
  return "grok";
31
+ if (/^pi\b/.test(command.trim()))
32
+ return "pi";
31
33
  return /^qodercli\b/.test(command.trim()) ? "qoder" : "claude";
32
34
  }
33
35
  /**
@@ -2210,6 +2212,17 @@ export class ProcessManager extends EventEmitter {
2210
2212
  }
2211
2213
  return result;
2212
2214
  }
2215
+ if (provider === "pi") {
2216
+ let result = command;
2217
+ const trimmedModel = model?.trim();
2218
+ if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result)) {
2219
+ result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2220
+ }
2221
+ const level = thinkingEffortToPiLevel(thinkingEffort ?? null);
2222
+ if (level && !/--thinking(?:\s|=)/.test(result))
2223
+ result += ` --thinking '${level}'`;
2224
+ return result;
2225
+ }
2213
2226
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2214
2227
  if (!isClaudeCmd)
2215
2228
  return command;
@@ -1,6 +1,6 @@
1
- import type { SystemAiConfig } from "./types.js";
1
+ import { type QuickCommitAiOptions } from "./git-quick-commit.js";
2
2
  export declare class PromptOptimizeError extends Error {
3
3
  readonly code: string;
4
4
  constructor(message: string, code: string);
5
5
  }
6
- export declare function optimizePrompt(rawText: string, language: string, cwd?: string, systemAi?: SystemAiConfig): Promise<string>;
6
+ export declare function optimizePrompt(rawText: string, language: string, cwd?: string, ai?: QuickCommitAiOptions): Promise<string>;