@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.
- package/README.md +6 -6
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +11 -0
- package/dist/cli-api.js +76 -0
- package/dist/cli.js +97 -0
- package/dist/config.d.ts +4 -3
- package/dist/config.js +20 -3
- package/dist/git-quick-commit.js +47 -1
- package/dist/git-worktree.d.ts +11 -0
- package/dist/git-worktree.js +72 -3
- package/dist/mission-diff.d.ts +8 -0
- package/dist/mission-diff.js +65 -0
- package/dist/mission-types.d.ts +90 -0
- package/dist/mission-types.js +1 -0
- package/dist/missions.d.ts +32 -0
- package/dist/missions.js +344 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +10 -1
- package/dist/path-repair.js +1 -1
- package/dist/process-manager.js +14 -1
- package/dist/prompt-optimizer.d.ts +2 -2
- package/dist/prompt-optimizer.js +11 -32
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/provider-cli-updater.js +8 -0
- package/dist/resume-policy.js +5 -3
- package/dist/server-mission-routes.d.ts +3 -0
- package/dist/server-mission-routes.js +81 -0
- package/dist/server-session-routes.js +5 -3
- package/dist/server-settings-routes.js +5 -0
- package/dist/server-update-routes.js +1 -1
- package/dist/server.js +31 -5
- package/dist/session-ai-context.d.ts +3 -3
- package/dist/session-ai-context.js +14 -18
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +221 -1
- package/dist/structured-pi-adapter.d.ts +11 -0
- package/dist/structured-pi-adapter.js +135 -0
- package/dist/structured-provider-common.d.ts +1 -0
- package/dist/structured-provider-common.js +16 -1
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +21 -7
- package/dist/system-ai.js +1 -1
- package/dist/types.d.ts +9 -4
- package/dist/web-ui/content/scripts.js +146 -69
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/provider-identity.d.ts +2 -1
- package/dist/web-ui/provider-identity.js +9 -1
- 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
|
-
|
|
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
|
-
:
|
|
556
|
-
? "
|
|
557
|
-
: "claude
|
|
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 {};
|