@co0ontty/wand 4.90.2 → 4.91.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 (71) hide show
  1. package/README.md +5 -0
  2. package/dist/agent-dispatch.js +1 -1
  3. package/dist/build-info.json +3 -3
  4. package/dist/conversation-service.d.ts +4 -1
  5. package/dist/conversation-service.js +35 -6
  6. package/dist/employee-text.js +3 -3
  7. package/dist/pi-auto-resources.js +1 -1
  8. package/dist/pi-cli-runtime.d.ts +8 -0
  9. package/dist/pi-cli-runtime.js +105 -0
  10. package/dist/pi-extension-resources.d.ts +3 -2
  11. package/dist/pi-extension-resources.js +6 -20
  12. package/dist/pi-openrouter-free-bridge.d.ts +1 -1
  13. package/dist/pi-openrouter-free-bridge.js +7 -3
  14. package/dist/pi-resource-bridge.d.ts +1 -1
  15. package/dist/pi-resource-bridge.js +17 -3
  16. package/dist/pi-resource-catalog.d.ts +3 -3
  17. package/dist/pi-resource-catalog.js +13 -28
  18. package/dist/pi-resource-run.js +1 -1
  19. package/dist/pi-user-resources.d.ts +5 -0
  20. package/dist/pi-user-resources.js +37 -0
  21. package/dist/server-session-context.js +6 -4
  22. package/dist/server-session-lifecycle-routes.js +5 -3
  23. package/dist/server-workspace-routes.js +1 -0
  24. package/dist/server.js +3 -9
  25. package/dist/storage/conversations-repository.d.ts +4 -1
  26. package/dist/storage/conversations-repository.js +40 -0
  27. package/dist/structured-session-manager.d.ts +1 -1
  28. package/dist/structured-session-manager.js +11 -8
  29. package/dist/web-ui/content/module-manifest.json +1 -1
  30. package/dist/web-ui/content/modules/{74fb50402b39f99a/chat-render-OGSERP3P.js → d70a22b9c0aa5244/chat-render-QXOBJZYF.js} +1 -1
  31. package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-DJAVXJ5F.js → d70a22b9c0aa5244/chunk-2SZRSM2C.js} +1 -1
  32. package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-T7ARYEMC.js → d70a22b9c0aa5244/chunk-LKQKEUHR.js} +1 -1
  33. package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-IEK55PZU.js → d70a22b9c0aa5244/chunk-NRYBDZJQ.js} +1 -1
  34. package/dist/web-ui/content/modules/{74fb50402b39f99a/employee-card-FDPCQJSS.js → d70a22b9c0aa5244/employee-card-JRCEGKOW.js} +1 -1
  35. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-C3IWMIFZ.js → d70a22b9c0aa5244/host-3YSTASO7.js} +1 -1
  36. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-Q47YC3FG.js → d70a22b9c0aa5244/host-EAFCE2FW.js} +1 -1
  37. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-3OXG7VBJ.js → d70a22b9c0aa5244/host-HSG7HCKW.js} +1 -1
  38. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-WL7ZZS7E.js → d70a22b9c0aa5244/host-O3FQO2WF.js} +1 -1
  39. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-Z6HYEEDJ.js → d70a22b9c0aa5244/host-PULYRKUO.js} +1 -1
  40. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-R2PN4PWJ.js → d70a22b9c0aa5244/host-QKLN6BQT.js} +1 -1
  41. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-YZ2YF43V.js → d70a22b9c0aa5244/host-TFWV4EL3.js} +1 -1
  42. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-5JX4XA3B.js → d70a22b9c0aa5244/host-TOKT2KEL.js} +1 -1
  43. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-QECJ4JY2.js → d70a22b9c0aa5244/host-UHPSRXOS.js} +1 -1
  44. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-IOQX22WS.js → d70a22b9c0aa5244/host-VQKHZ4XY.js} +1 -1
  45. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-QLTSBAD7.js → d70a22b9c0aa5244/host-VSUTKITC.js} +1 -1
  46. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-X5JHUBCU.js → d70a22b9c0aa5244/host-XTI24R46.js} +1 -1
  47. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-PL4SWWF5.js → d70a22b9c0aa5244/host-ZTK4NJBZ.js} +1 -1
  48. package/dist/web-ui/content/modules/{74fb50402b39f99a/host-bridge-KMY7UWDM.js → d70a22b9c0aa5244/host-bridge-YU4CDKM6.js} +1 -1
  49. package/dist/web-ui/content/modules/{74fb50402b39f99a/library-bridge-CRHLRGUS.js → d70a22b9c0aa5244/library-bridge-E4G3TTAM.js} +1 -1
  50. package/dist/web-ui/content/modules/{74fb50402b39f99a/page-5YCNWWVQ.js → d70a22b9c0aa5244/page-EL25SU2L.js} +1 -1
  51. package/dist/web-ui/content/modules/{74fb50402b39f99a/panel-6VM3TZD3.js → d70a22b9c0aa5244/panel-5FP5SRNS.js} +1 -1
  52. package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/runtime.js +130 -129
  53. package/dist/web-ui/content/modules/{74fb50402b39f99a/task-board-host-OGNS7ZZX.js → d70a22b9c0aa5244/task-board-host-EYEPEKBJ.js} +1 -1
  54. package/dist/web-ui/content/modules/{74fb50402b39f99a/workspace-agent-picker-KWY5JOSF.js → d70a22b9c0aa5244/workspace-agent-picker-TA2VJ2QW.js} +1 -1
  55. package/dist/web-ui/content/styles.css +1 -1
  56. package/dist/web-ui/embedded-assets.d.ts +27 -27
  57. package/dist/web-ui/embedded-assets.js +3 -3
  58. package/package.json +8 -1
  59. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-JAYB4SD7.js → d70a22b9c0aa5244/chunk-4OECFENI.js} +0 -0
  60. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-W62HMLPT.js → d70a22b9c0aa5244/chunk-4RVBPCZ2.js} +0 -0
  61. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-YF6I5MJN.js → d70a22b9c0aa5244/chunk-4TWMU7ZU.js} +0 -0
  62. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-CUQQJPJA.js → d70a22b9c0aa5244/chunk-A3PREDLN.js} +0 -0
  63. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-GUG57JJQ.js → d70a22b9c0aa5244/chunk-AN34B6OX.js} +0 -0
  64. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-35637AFU.js → d70a22b9c0aa5244/chunk-LROINIFZ.js} +0 -0
  65. /package/dist/web-ui/content/modules/{74fb50402b39f99a/chunk-TJEUKQ34.js → d70a22b9c0aa5244/chunk-PFPY4NTX.js} +0 -0
  66. /package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/chunk-XGVT4ZWT.js +0 -0
  67. /package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/chunk-YOXXNXOX.js +0 -0
  68. /package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/chunk-YX475LXO.js +0 -0
  69. /package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/host-5QOHKHNC.js +0 -0
  70. /package/dist/web-ui/content/modules/{74fb50402b39f99a → d70a22b9c0aa5244}/sha256-ANXV72UC.js +0 -0
  71. /package/dist/web-ui/content/modules/{74fb50402b39f99a/workspace-window-UUFMGL25.js → d70a22b9c0aa5244/workspace-window-QDI7YABJ.js} +0 -0
package/README.md CHANGED
@@ -160,6 +160,9 @@ also reads generated iOS/macOS AppIcon PNGs, which on macOS come from
160
160
  `swift <platform>/scripts/generate-icons.swift` (run `npm run sync:brand-assets`
161
161
  first; CI does the same in `.github/workflows/ci.yml`). To preview removable
162
162
  compiler caches, run `npm run clean:build-cache`; add `-- --apply` to remove them.
163
+ For macOS daily cleanup of inactive archived compiler intermediates (seven-day retention),
164
+ use `npm run clean:build-cache:install -- --install`; see [automatic cleanup](docs/build-cache-cleanup.md).
165
+ This does not install Git hooks or remove live native build trees.
163
166
 
164
167
  Runtime data is stored under `~/.wand/`: `config.json`, `wand.db`, and `sessions/`.
165
168
 
@@ -387,6 +390,8 @@ git submodule update --init -- render-bin
387
390
  全量 `npm test` 包含跨端源码契约检查,运行前另检出 `android ios macos render`。品牌一致性用例还会读 iOS/macOS 的 AppIcon PNG(生成物),在 macOS 上先跑 `npm run sync:brand-assets`,再用 `swift <平台>/scripts/generate-icons.swift` 生成(CI 的 `.github/workflows/ci.yml` 做同样的事)。
388
391
  `npm run clean:build-cache` 预览可清理的编译缓存,追加 `-- --apply` 才执行删除;
389
392
  保留分发包、Release Render 二进制、签名、依赖和会话数据。
393
+ macOS 每日清理历史归档编译中间物(保留7天)用 `npm run clean:build-cache:install -- --install`;
394
+ 范围、并发保护、日志和停用方式见[自动清理说明](docs/build-cache-cleanup.md)。不修改Git hook,不自动删除现用原生构建树。
390
395
 
391
396
  数据存储在 `~/.wand/` 下:`config.json`(配置)、`wand.db`(SQLite)、`sessions/`(日志)。
392
397
 
@@ -70,7 +70,7 @@ export async function dispatchAgentForTask(deps, input) {
70
70
  // 直接派发 Wand Agent(不进员工候选链):先裁决进程内引擎是否可用,
71
71
  // 不可用就让这次派发明确失败,不静默退回 CLI 冒充。
72
72
  ...(agent.engine === "sdk"
73
- ? { engine: structured.resolveNewSessionPiEngine("sdk").engine }
73
+ ? { engine: (await structured.resolveNewSessionPiEngine("sdk")).engine }
74
74
  : {}),
75
75
  worktreeEnabled: false,
76
76
  sessionSource: "automation",
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "6f48f3ff2587f94d7339d5c1a263a04dc16e9e92",
3
- "builtAt": "2026-10-13T02:11:29.396Z",
4
- "version": "4.90.2",
2
+ "commit": "0adcac1ecb8447d91e5abc44d2dd6a8b2f807243",
3
+ "builtAt": "2026-10-13T05:03:40.007Z",
4
+ "version": "4.91.0",
5
5
  "channel": "stable"
6
6
  }
@@ -3,7 +3,7 @@ import { type AiTeamRunner } from "./ai-team-runner.js";
3
3
  import { type ConversationDetail, type ConversationInstance, type ConversationReceipt, type ConversationSummary, type ConversationTarget, type ConversationSessionUpdate } from "./conversation-types.js";
4
4
  import type { WandStorage } from "./storage.js";
5
5
  import type { StructuredSessionManager } from "./structured-session-manager.js";
6
- import type { ConversationTurn, ProcessEvent, WandConfig } from "./types.js";
6
+ import type { ConversationTurn, ProcessEvent, SessionSnapshot, WandConfig } from "./types.js";
7
7
  export interface GroupInput {
8
8
  employeeIds?: string[];
9
9
  templateId?: string;
@@ -40,6 +40,7 @@ export declare class ConversationService {
40
40
  private readonly removals;
41
41
  private readonly pendingOperations;
42
42
  private readonly previewTimers;
43
+ private readonly legacyChannels;
43
44
  /** Reuse session events; send only a bounded IM projection, never a second execution protocol. */
44
45
  ingestSessionEvent(event: ProcessEvent): void;
45
46
  dispose(): void;
@@ -48,6 +49,8 @@ export declare class ConversationService {
48
49
  /** Explicit startup/event migration, never a GET repair and never an invented group. */
49
50
  importRun(run: AiTeamRun): void;
50
51
  migrateExistingChats(): void;
52
+ /** Startup + session events, never GET repair. Stable employee identity is the only attribution source. */
53
+ importEmployeeSession(session: SessionSnapshot): void;
51
54
  pendingEmployee(employeeId: string): ConversationInstance;
52
55
  private requireInstance;
53
56
  /** 会话里「我自己」的署名与头像:跟随当前设置,改设置不改写历史回合。 */
@@ -54,22 +54,28 @@ export class ConversationService {
54
54
  removals = new Map();
55
55
  pendingOperations = new Map();
56
56
  previewTimers = new Map();
57
+ legacyChannels = new Set();
57
58
  /** Reuse session events; send only a bounded IM projection, never a second execution protocol. */
58
59
  ingestSessionEvent(event) {
59
60
  const session = this.deps.structured.get(event.sessionId);
60
- if (!this.deps.notifySession || !session?.automationId?.startsWith(CONVERSATION_SESSION_PREFIX)
61
- || this.previewTimers.has(event.sessionId))
61
+ if (session)
62
+ this.importEmployeeSession(session);
63
+ if (!this.deps.notifySession || !session || this.previewTimers.has(event.sessionId)
64
+ || (!session.automationId?.startsWith(CONVERSATION_SESSION_PREFIX)
65
+ && !this.storage.employeeConversationForSession(session.id)))
62
66
  return;
63
67
  const timer = setTimeout(() => {
64
68
  this.previewTimers.delete(event.sessionId);
65
69
  const current = this.snapshot(event.sessionId);
66
70
  const requestId = current?.automationId?.slice(CONVERSATION_SESSION_PREFIX.length);
67
71
  const receipt = requestId ? this.receipt(requestId) : null;
68
- if (!receipt || receipt.sessionId !== event.sessionId || !this.storage.getConversation(receipt.conversationId)
69
- || this.storage.conversationListState(receipt.conversationId).deleting)
72
+ const conversationId = receipt?.sessionId === event.sessionId ? receipt.conversationId
73
+ : this.storage.employeeConversationForSession(event.sessionId);
74
+ if (!conversationId || !this.storage.getConversation(conversationId)
75
+ || this.storage.conversationListState(conversationId).deleting)
70
76
  return;
71
- this.deps.notifySession?.({ conversationId: receipt.conversationId, sessionId: event.sessionId,
72
- preview: conversationSessionPreview(current, receipt) });
77
+ this.deps.notifySession?.({ conversationId, sessionId: event.sessionId,
78
+ preview: conversationSessionPreview(current, receipt ?? undefined) });
73
79
  }, 100);
74
80
  timer.unref?.();
75
81
  this.previewTimers.set(event.sessionId, timer);
@@ -115,6 +121,29 @@ export class ConversationService {
115
121
  migrateExistingChats() {
116
122
  for (const run of this.storage.listAiTeamRuns().reverse())
117
123
  this.importRun(run);
124
+ // Preserve legacy channels and already accepted IM links; only unbound interactive work is imported.
125
+ const instances = this.storage.listConversations();
126
+ for (const instance of instances)
127
+ for (const id of [instance.sessionId, instance.communicationSessionId, ...(instance.historySessionIds ?? [])]) {
128
+ if (id)
129
+ this.legacyChannels.add(id);
130
+ }
131
+ const channels = new Set([...this.legacyChannels, ...instances.flatMap(instance => this.storage.conversationSessionIds(instance.id))]);
132
+ for (const session of this.storage.loadSessionsSlim()) {
133
+ if (!channels.has(session.id))
134
+ this.importEmployeeSession(session);
135
+ }
136
+ }
137
+ /** Startup + session events, never GET repair. Stable employee identity is the only attribution source. */
138
+ importEmployeeSession(session) {
139
+ if (this.legacyChannels.has(session.id) || session.sessionKind !== "structured" || !session.employeeId || session.automationId
140
+ || (session.sessionSource && session.sessionSource !== "interactive") || session.workspaceTaskId)
141
+ return;
142
+ const instance = this.storage.getConversation(employeeConversationId(session.employeeId))
143
+ ?? this.pendingEmployee(session.employeeId);
144
+ if (this.storage.conversationListState(instance.id).deleting)
145
+ return;
146
+ this.storage.linkEmployeeSession(instance, session);
118
147
  }
119
148
  pendingEmployee(employeeId) {
120
149
  const employee = this.storage.getSiliconEmployee(employeeId);
@@ -1,4 +1,3 @@
1
- import { normalizeContext } from "@earendil-works/pi-ai";
2
1
  import { coreHarnessRuntime, loadPiAi, resolveCoreModel } from "./harness-engine.js";
3
2
  import { agentKey } from "./ai-team-types.js";
4
3
  import { getDefaultModelForProvider } from "./config.js";
@@ -87,7 +86,8 @@ export async function prepareEmployeeTextCandidate(deps, agent, request, signal)
87
86
  }
88
87
  if (agent.provider !== "pi")
89
88
  throw new Error("Wand Agent只支持Pi provider。");
90
- const context = normalizeContext({ systemPrompt: request.system,
89
+ const pi = await loadPiAi();
90
+ const context = pi.normalizeContext({ systemPrompt: request.system,
91
91
  messages: [{ role: "user", content: request.prompt, timestamp: Date.now() }], tools: [] });
92
92
  const reasoning = thinkingEffortToPiLevel(agent.thinkingEffort ?? "off");
93
93
  if (isOpenRouterFreeSelector(agent.model)) {
@@ -111,7 +111,7 @@ export async function prepareEmployeeTextCandidate(deps, agent, request, signal)
111
111
  const resolved = await resolveCoreModel(config.harness, agent.model, { cwd: config.defaultCwd });
112
112
  if (!resolved)
113
113
  throw new Error("Wand Agent模型不可用。");
114
- const runtime = await coreHarnessRuntime(config.harness), pi = await loadPiAi();
114
+ const runtime = await coreHarnessRuntime(config.harness);
115
115
  const level = pi.clampThinkingLevel(resolved.model, (reasoning ?? "off"));
116
116
  return async () => {
117
117
  signal.throwIfAborted();
@@ -35,7 +35,7 @@ export function withAutomaticPiResources(runner, config, options) {
35
35
  let fallback = "";
36
36
  let timer;
37
37
  try {
38
- inventory = await (options.inventory?.(context.session.cwd) ?? discoverPiResources(config, context.session.cwd));
38
+ inventory = await (options.inventory?.(context.session.cwd) ?? discoverPiResources(config, context.session.cwd, context.env));
39
39
  checkActive();
40
40
  // A missing manual resource must fail as usual, never be silently replaced by an automatic pick.
41
41
  resolvePiResourceSelection(inventory, manual);
@@ -0,0 +1,8 @@
1
+ /** Extension code uses the running host, including a CLI update between preparation and spawn. */
2
+ export declare function runningPiCliPackage(): string;
3
+ /** Read-only npm/managed CLI discovery using the same environment as the actual child. */
4
+ export declare function resolvePiCliPackage(env?: NodeJS.ProcessEnv, cwd?: string): string;
5
+ /** Resolve a host-provided leaf API against the running CLI, never against the extension's Wand package. */
6
+ export declare function importPiCliAiModule<T>(root: string, relative: string): Promise<T>;
7
+ /** One compatibility boundary for CLI-owned built-ins/read-only parsers, not the SDK entry/harness. */
8
+ export declare function importPiCliModule<T>(root: string, relative: string): Promise<T>;
@@ -0,0 +1,105 @@
1
+ import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { findPackageJSON } from "node:module";
4
+ import { pathToFileURL } from "node:url";
5
+ import { buildChildEnv } from "./env-utils.js";
6
+ const PACKAGE_NAME = "@earendil-works/pi-coding-agent";
7
+ const UNSUPPORTED = "当前 Pi CLI 无法提供会话级资源接口;请使用官方 npm 或 managed 安装。没有加载 Wand SDK 或改用其他 CLI。";
8
+ /** Identify the package owning an actual CLI entry, never resolve from Wand's node_modules. */
9
+ function packageOfEntry(entry) {
10
+ let actual;
11
+ try {
12
+ actual = realpathSync(entry);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ let dir = path.dirname(actual);
18
+ for (let depth = 0; depth < 6; depth++) {
19
+ try {
20
+ const manifestPath = path.join(dir, "package.json");
21
+ if (statSync(manifestPath).size > 128 * 1024)
22
+ throw new Error("manifest too large");
23
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
24
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.pi;
25
+ if (manifest.name === PACKAGE_NAME && typeof bin === "string"
26
+ && realpathSync(path.resolve(dir, bin)) === actual)
27
+ return dir;
28
+ }
29
+ catch { /* Not a package-owned CLI entry. */ }
30
+ const parent = path.dirname(dir);
31
+ if (parent === dir)
32
+ break;
33
+ dir = parent;
34
+ }
35
+ return null;
36
+ }
37
+ /** Extension code uses the running host, including a CLI update between preparation and spawn. */
38
+ export function runningPiCliPackage() {
39
+ const root = process.argv[1] && packageOfEntry(process.argv[1]);
40
+ if (!root)
41
+ throw new Error(UNSUPPORTED);
42
+ return root;
43
+ }
44
+ /** Read-only npm/managed CLI discovery using the same environment as the actual child. */
45
+ export function resolvePiCliPackage(env = buildChildEnv(true), cwd = process.cwd()) {
46
+ const names = process.platform === "win32" ? ["pi.exe", "pi.cmd", "pi"] : ["pi"];
47
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
48
+ for (const name of names) {
49
+ const candidate = path.resolve(cwd, directory || ".", name);
50
+ try {
51
+ accessSync(candidate, constants.X_OK);
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ const direct = packageOfEntry(candidate);
57
+ if (direct)
58
+ return direct;
59
+ // Official managed launcher: inspect its own active release. Do not evaluate
60
+ // arbitrary shell wrappers or pick a different install later on PATH.
61
+ try {
62
+ const launcher = realpathSync(candidate);
63
+ const install = path.resolve(path.dirname(launcher), "../install");
64
+ const versionFile = path.join(install, "current-version");
65
+ if (statSync(launcher).size > 64 * 1024 || statSync(versionFile).size > 256)
66
+ throw new Error("unsupported launcher");
67
+ const source = readFileSync(launcher, "utf8");
68
+ const version = readFileSync(versionFile, "utf8").trim();
69
+ if (source.includes("PI_MANAGED_INSTALL_ROOT") && source.includes("install/current-version")
70
+ && /^[0-9A-Za-z][0-9A-Za-z._+-]{0,127}$/.test(version)) {
71
+ const managed = packageOfEntry(path.join(install, "releases", version, "node_modules/.bin/pi"));
72
+ if (managed)
73
+ return managed;
74
+ }
75
+ }
76
+ catch { /* Unsupported launcher; fail closed. */ }
77
+ throw new Error(UNSUPPORTED);
78
+ }
79
+ }
80
+ throw new Error("未找到 Pi CLI;请先安装 Pi。没有加载 Wand SDK。");
81
+ }
82
+ /** Resolve a host-provided leaf API against the running CLI, never against the extension's Wand package. */
83
+ export async function importPiCliAiModule(root, relative) {
84
+ try {
85
+ const manifest = findPackageJSON("@earendil-works/pi-ai", pathToFileURL(path.join(root, "package.json")));
86
+ if (!manifest)
87
+ throw new Error("CLI peer missing");
88
+ return await import(pathToFileURL(path.join(path.dirname(manifest), "dist", relative)).href);
89
+ }
90
+ catch {
91
+ throw new Error("Pi CLI 的 provider 接口加载失败,请检查 CLI 安装。没有改用 Wand SDK。");
92
+ }
93
+ }
94
+ /** One compatibility boundary for CLI-owned built-ins/read-only parsers, not the SDK entry/harness. */
95
+ export async function importPiCliModule(root, relative) {
96
+ const file = path.join(root, "dist", relative);
97
+ if (!existsSync(file))
98
+ throw new Error(UNSUPPORTED);
99
+ try {
100
+ return await import(pathToFileURL(file).href);
101
+ }
102
+ catch {
103
+ throw new Error("Pi CLI 的会话级资源接口加载失败,请检查 CLI 安装。没有改用 Wand SDK。");
104
+ }
105
+ }
@@ -1,10 +1,11 @@
1
- import { loadPiCodingAgent } from "./harness-engine.js";
1
+ import type { loadPiCodingAgent } from "./harness-engine.js";
2
+ import { type PiResourceReader } from "./pi-user-resources.js";
2
3
  import type { WandConfig } from "./types.js";
3
4
  import type { PiSessionSettings } from "./pi-session-settings.js";
4
5
  import type { ResourceLoader } from "@earendil-works/pi-coding-agent";
5
6
  type CodingAgent = Awaited<ReturnType<typeof loadPiCodingAgent>>;
6
7
  /** Resolve already-installed USER resources only. Never install, import code, or trust project packages. */
7
- export declare function installedPiExtensions(config: WandConfig, cwd: string): Promise<Array<{
8
+ export declare function installedPiExtensions(config: WandConfig, cwd: string, reader?: PiResourceReader): Promise<Array<{
8
9
  path: string;
9
10
  name: string;
10
11
  goal: boolean;
@@ -1,24 +1,10 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import path from "node:path";
3
- import { coreHarnessAgentDir, loadPiCodingAgent } from "./harness-engine.js";
2
+ import { coreHarnessAgentDir } from "./harness-engine.js";
3
+ import { cliPiResourceReader, installedPiUserResources } from "./pi-user-resources.js";
4
4
  /** Resolve already-installed USER resources only. Never install, import code, or trust project packages. */
5
- export async function installedPiExtensions(config, cwd) {
6
- const pi = await loadPiCodingAgent();
7
- const agentDir = coreHarnessAgentDir(config.harness);
8
- let raw = {};
9
- try {
10
- raw = JSON.parse(await readFile(path.join(agentDir, "settings.json"), "utf8"));
11
- }
12
- catch (error) {
13
- if (error.code !== "ENOENT")
14
- throw error;
15
- }
16
- const settings = pi.SettingsManager.inMemory({
17
- packages: Array.isArray(raw.packages) ? raw.packages : [],
18
- extensions: Array.isArray(raw.extensions) ? raw.extensions.filter((item) => typeof item === "string") : [],
19
- }, { projectTrusted: false });
20
- const manager = new pi.DefaultPackageManager({ cwd, agentDir, settingsManager: settings });
21
- const resolved = await manager.resolve(async () => "skip");
5
+ export async function installedPiExtensions(config, cwd, reader) {
6
+ const pi = reader ?? await cliPiResourceReader(cwd);
7
+ const resolved = await installedPiUserResources(pi, coreHarnessAgentDir(config.harness), cwd);
22
8
  return resolved.extensions.filter((item) => item.enabled && item.metadata.scope === "user").map((item) => {
23
9
  const name = item.metadata.source === "local" || item.metadata.source === "auto"
24
10
  ? path.basename(item.path) : item.metadata.source.replace(/^npm:/, "");
@@ -39,7 +25,7 @@ async function nativeExtensionLoader() {
39
25
  return loaded;
40
26
  }
41
27
  export async function createPiExtensionResources(pi, config, cwd, settings, systemPrompt) {
42
- const resources = settings.globalTools || settings.goalMode ? await installedPiExtensions(config, cwd) : [];
28
+ const resources = settings.globalTools || settings.goalMode ? await installedPiExtensions(config, cwd, pi) : [];
43
29
  if (settings.goalMode && !resources.some((item) => item.goal)) {
44
30
  throw new Error("目标模式不可用:请先在 Pi 中安装并启用 pi-goal。");
45
31
  }
@@ -1,2 +1,2 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- export default function wandOpenRouterFree(pi: ExtensionAPI): void;
2
+ export default function wandOpenRouterFree(pi: ExtensionAPI): Promise<void>;
@@ -4,10 +4,10 @@
4
4
  /** Pi CLI provider bridge. No Agent/SDK runner and no changes to the user's Pi auth/models files. */
5
5
  import { readFileSync, rmSync } from "node:fs";
6
6
  import { dirname } from "node:path";
7
- import { createAssistantMessageEventStream, lazyApi } from "@earendil-works/pi-ai";
7
+ import { importPiCliAiModule, runningPiCliPackage } from "./pi-cli-runtime.js";
8
8
  import { OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_ROUTING } from "./openrouter-free-models.js";
9
9
  import { openOpenRouterFreeCliAccess, revokeOpenRouterFreeCliAccess } from "./openrouter-free-cli.js";
10
- export default function wandOpenRouterFree(pi) {
10
+ export default async function wandOpenRouterFree(pi) {
11
11
  const file = process.env.WAND_PI_FREE_POLICY;
12
12
  const token = process.env.WAND_PI_FREE_TOKEN ?? "";
13
13
  delete process.env.WAND_PI_FREE_POLICY;
@@ -15,9 +15,13 @@ export default function wandOpenRouterFree(pi) {
15
15
  if (!file)
16
16
  throw new Error("免费模型调用缺少本轮策略。");
17
17
  const policy = JSON.parse(readFileSync(file, "utf8"));
18
+ const host = runningPiCliPackage();
19
+ const [{ createAssistantMessageEventStream }, api] = await Promise.all([
20
+ importPiCliAiModule(host, "utils/event-stream.js"),
21
+ importPiCliAiModule(host, "api/openai-completions.js"),
22
+ ]);
18
23
  // Pi applies its HTTP/proxy configuration after factories load; use the current fetch per request.
19
24
  const access = openOpenRouterFreeCliAccess(policy.databasePath, token, (url, init) => globalThis.fetch(url, init));
20
- const api = lazyApi(() => import("@earendil-works/pi-ai/api/openai-completions"));
21
25
  let activeModel = policy.model;
22
26
  const streamSimple = (model, transcript, options) => {
23
27
  const stream = createAssistantMessageEventStream();
@@ -1,2 +1,2 @@
1
- import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  export default function wandResourceSelection(pi: ExtensionAPI): Promise<void>;
@@ -4,12 +4,15 @@
4
4
  /** Explicit CLI extension: replaces built-in MCP discovery without changing the user's agent directory. */
5
5
  import { readFileSync, rmSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
- import { createCodemodeExtension, createMcpExtension, createToolSearchExtension } from "@earendil-works/pi-coding-agent";
7
+ import { importPiCliModule, runningPiCliPackage } from "./pi-cli-runtime.js";
8
8
  export default async function wandResourceSelection(pi) {
9
9
  const policyFile = process.env.WAND_PI_RESOURCE_POLICY;
10
10
  if (!policyFile)
11
11
  throw new Error("Wand 资源选择缺少本轮策略。");
12
12
  const policy = JSON.parse(readFileSync(policyFile, "utf8"));
13
+ // Use the running CLI's built-ins, not Wand's pinned SDK entry point. An update
14
+ // between preparation and spawn therefore still uses one matching CLI runtime.
15
+ const host = runningPiCliPackage();
13
16
  // Entries were validated by Pi's parser immediately before spawning. Read only this user's file;
14
17
  // do not replace the agent directory or discover trusted-project/extension servers implicitly.
15
18
  const allowed = new Set(policy.mcpServers ?? []);
@@ -20,7 +23,10 @@ export default async function wandResourceSelection(pi) {
20
23
  const value = Reflect.get(target, key);
21
24
  return typeof value === "function" ? value.bind(target) : value;
22
25
  } });
23
- if (policy.mcpServers)
26
+ if (policy.mcpServers) {
27
+ const { createMcpExtension } = await importPiCliModule(host, "extensions/mcp/index.js");
28
+ if (typeof createMcpExtension !== "function")
29
+ throw new Error("Pi CLI 不支持会话级 MCP 选择。");
24
30
  createMcpExtension({
25
31
  loadConfig: () => {
26
32
  if (!allowed.size)
@@ -42,7 +48,11 @@ export default async function wandResourceSelection(pi) {
42
48
  },
43
49
  updateConfig: () => { throw new Error("请通过 Wand 输入框中的 Skills / MCP 设置修改本会话选择。"); },
44
50
  })(scoped);
51
+ }
45
52
  if (policy.codemodeOverride) {
53
+ const { createCodemodeExtension } = await importPiCliModule(host, "extensions/codemode/index.js");
54
+ if (typeof createCodemodeExtension !== "function")
55
+ throw new Error("Pi CLI 不支持会话级 CodeMode 设置。");
46
56
  // This explicit factory replaces the CLI builtin and can express `only` without modifying
47
57
  // settings.json. Active-tool changes preserve every common tool and selected MCP connection.
48
58
  const codemodeApi = policy.codemodeOverride === "off" ? new Proxy(pi, { get(target, key) {
@@ -54,8 +64,12 @@ export default async function wandResourceSelection(pi) {
54
64
  createCodemodeExtension({ mode: policy.codemodeOverride === "only" ? "only" : "on" })(codemodeApi);
55
65
  // MCP tools with indirect exposure still need an entrance when CodeMode is explicitly off.
56
66
  const searchInstead = policy.codemodeOverride === "off" && !!policy.mcpServers?.length;
57
- if (searchInstead)
67
+ if (searchInstead) {
68
+ const { createToolSearchExtension } = await importPiCliModule(host, "extensions/tool-search/index.js");
69
+ if (typeof createToolSearchExtension !== "function")
70
+ throw new Error("Pi CLI 不支持关闭 CodeMode 后的 MCP 工具发现。");
58
71
  createToolSearchExtension()(pi);
72
+ }
59
73
  const apply = () => {
60
74
  const other = pi.getActiveTools().filter((name) => name !== "codemode");
61
75
  pi.setActiveTools(policy.codemodeOverride === "off"
@@ -7,10 +7,10 @@ export interface PiResourceInventory {
7
7
  skills: Map<string, Skill>;
8
8
  servers: Map<string, McpConfig["servers"][number]>;
9
9
  }
10
- /** Pinned, read-only parser boundary; never connects, runs credential commands or loads extension code. */
11
- export declare function readUserPiMcpConfig(agentDir: string, cwd: string): Promise<McpConfig>;
10
+ /** CLI-owned, read-only parser: no connection, credential command, extension code or Wand SDK. */
11
+ export declare function readUserPiMcpConfig(agentDir: string, cwd: string, env?: NodeJS.ProcessEnv): Promise<McpConfig>;
12
12
  /** Installed USER resources only. No package downloads and no project config mutation/trust elevation. */
13
- export declare function discoverPiResources(config: Pick<WandConfig, "harness">, cwd: string): Promise<PiResourceInventory>;
13
+ export declare function discoverPiResources(config: Pick<WandConfig, "harness">, cwd: string, env?: NodeJS.ProcessEnv): Promise<PiResourceInventory>;
14
14
  export declare function resolvePiResourceSelection(inventory: PiResourceInventory, selection: PiResourceSelection): {
15
15
  skills: Skill[];
16
16
  servers: McpConfig["servers"];
@@ -1,38 +1,23 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFile, realpath, stat } from "node:fs/promises";
3
- import path from "node:path";
4
- import { coreHarnessAgentDir, loadPiCodingAgent } from "./harness-engine.js";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { coreHarnessAgentDir } from "./harness-engine.js";
4
+ import { importPiCliModule, resolvePiCliPackage } from "./pi-cli-runtime.js";
5
+ import { cliPiResourceReader, installedPiUserResources } from "./pi-user-resources.js";
5
6
  function resourceId(kind, identity) {
6
7
  return `${kind}-${createHash("sha256").update(identity).digest("hex").slice(0, 24)}`;
7
8
  }
8
- /** Pinned, read-only parser boundary; never connects, runs credential commands or loads extension code. */
9
- export async function readUserPiMcpConfig(agentDir, cwd) {
10
- const url = new URL("./extensions/mcp/config.js", import.meta.resolve("@earendil-works/pi-coding-agent"));
11
- const parser = await import(url.href);
9
+ /** CLI-owned, read-only parser: no connection, credential command, extension code or Wand SDK. */
10
+ export async function readUserPiMcpConfig(agentDir, cwd, env) {
11
+ const parser = await importPiCliModule(resolvePiCliPackage(env, cwd), "extensions/mcp/config.js");
12
+ if (typeof parser.loadMcpConfig !== "function")
13
+ throw new Error("Pi CLI 不支持只读 MCP 配置发现。");
12
14
  return parser.loadMcpConfig({ agentDir, cwd, projectTrusted: false });
13
15
  }
14
16
  /** Installed USER resources only. No package downloads and no project config mutation/trust elevation. */
15
- export async function discoverPiResources(config, cwd) {
16
- const pi = await loadPiCodingAgent();
17
+ export async function discoverPiResources(config, cwd, env) {
18
+ const pi = await cliPiResourceReader(cwd, env);
17
19
  const agentDir = coreHarnessAgentDir(config.harness);
18
- let raw = {};
19
- try {
20
- const content = await readFile(path.join(agentDir, "settings.json"), "utf8");
21
- if (content.length > 1_048_576)
22
- throw new Error("配置过大");
23
- raw = JSON.parse(content);
24
- }
25
- catch (error) {
26
- if (error.code !== "ENOENT")
27
- throw new Error("Pi 资源配置无法读取,请检查 settings.json。");
28
- }
29
- const settings = pi.SettingsManager.inMemory({
30
- packages: Array.isArray(raw.packages) ? raw.packages : [],
31
- skills: Array.isArray(raw.skills) ? raw.skills.filter((item) => typeof item === "string") : [],
32
- extensions: Array.isArray(raw.extensions) ? raw.extensions.filter((item) => typeof item === "string") : [],
33
- }, { projectTrusted: false });
34
- const manager = new pi.DefaultPackageManager({ cwd, agentDir, settingsManager: settings });
35
- const resolved = await manager.resolve(async () => "skip");
20
+ const resolved = await installedPiUserResources(pi, agentDir, cwd);
36
21
  const paths = resolved.skills.filter((item) => item.enabled && item.metadata.scope === "user").map((item) => item.path);
37
22
  const loaded = pi.loadSkills({ cwd, agentDir, skillPaths: paths, includeDefaults: false });
38
23
  const skills = new Map();
@@ -47,7 +32,7 @@ export async function discoverPiResources(config, cwd) {
47
32
  if (skills.size >= 200)
48
33
  break;
49
34
  }
50
- const mcp = await readUserPiMcpConfig(agentDir, cwd);
35
+ const mcp = await readUserPiMcpConfig(agentDir, cwd, env);
51
36
  const servers = new Map();
52
37
  for (const server of mcp.servers.slice(0, 64))
53
38
  servers.set(resourceId("mcp", server.name), server);
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
6
6
  /** Re-resolve at execution time: removed resources fail, never silently enable defaults. */
7
7
  export async function preparePiResources(agentDir, cwd, selection, baseEnv, codemodeOverride) {
8
- const selected = selection ? resolvePiResourceSelection(await discoverPiResources({ harness: { engine: "cli", agentDir } }, cwd), selection) : undefined;
8
+ const selected = selection ? resolvePiResourceSelection(await discoverPiResources({ harness: { engine: "cli", agentDir } }, cwd, baseEnv), selection) : undefined;
9
9
  const privateDir = mkdtempSync(path.join(os.tmpdir(), "wand-pi-resources-"));
10
10
  chmodSync(privateDir, 0o700);
11
11
  try {
@@ -0,0 +1,5 @@
1
+ export type PiResourceReader = Pick<typeof import("@earendil-works/pi-coding-agent"), "SettingsManager" | "DefaultPackageManager" | "loadSkills">;
2
+ /** CLI inventory imports only the executing CLI's read-only resource modules, never the Wand SDK. */
3
+ export declare function cliPiResourceReader(cwd: string, env?: NodeJS.ProcessEnv): Promise<PiResourceReader>;
4
+ /** Resolve installed USER resources only: no download, extension import or project trust elevation. */
5
+ export declare function installedPiUserResources(pi: PiResourceReader, agentDir: string, cwd: string): Promise<import("@earendil-works/pi-coding-agent").ResolvedPaths>;
@@ -0,0 +1,37 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { importPiCliModule, resolvePiCliPackage } from "./pi-cli-runtime.js";
4
+ /** CLI inventory imports only the executing CLI's read-only resource modules, never the Wand SDK. */
5
+ export async function cliPiResourceReader(cwd, env) {
6
+ const root = resolvePiCliPackage(env, cwd);
7
+ const [settings, packages, skills] = await Promise.all([
8
+ importPiCliModule(root, "core/settings-manager.js"),
9
+ importPiCliModule(root, "core/package-manager.js"),
10
+ importPiCliModule(root, "core/skills.js"),
11
+ ]);
12
+ if (typeof settings.SettingsManager?.inMemory !== "function" || typeof packages.DefaultPackageManager !== "function"
13
+ || typeof skills.loadSkills !== "function")
14
+ throw new Error("Pi CLI 不支持只读资源发现;没有加载 Wand SDK。");
15
+ return { ...settings, ...packages, ...skills };
16
+ }
17
+ /** Resolve installed USER resources only: no download, extension import or project trust elevation. */
18
+ export async function installedPiUserResources(pi, agentDir, cwd) {
19
+ let raw = {};
20
+ try {
21
+ const content = await readFile(path.join(agentDir, "settings.json"), "utf8");
22
+ if (content.length > 1_048_576)
23
+ throw new Error("配置过大");
24
+ raw = JSON.parse(content);
25
+ }
26
+ catch (error) {
27
+ if (error.code !== "ENOENT")
28
+ throw new Error("Pi 资源配置无法读取,请检查 settings.json。");
29
+ }
30
+ const settings = pi.SettingsManager.inMemory({
31
+ packages: Array.isArray(raw.packages) ? raw.packages : [],
32
+ skills: Array.isArray(raw.skills) ? raw.skills.filter((item) => typeof item === "string") : [],
33
+ extensions: Array.isArray(raw.extensions) ? raw.extensions.filter((item) => typeof item === "string") : [],
34
+ }, { projectTrusted: false });
35
+ const manager = new pi.DefaultPackageManager({ cwd, agentDir, settingsManager: settings });
36
+ return await manager.resolve(async () => "skip");
37
+ }
@@ -23,15 +23,17 @@ export function createSessionRouteContext(deps) {
23
23
  const piSettingsResponse = async (id) => {
24
24
  const { settings, resolution } = structured.getPiSettings(id);
25
25
  const snapshot = structured.get(id);
26
- const installed = await installedPiExtensions(config, snapshot.cwd);
27
26
  const sdk = resolution.engine === "core";
27
+ const sdkReader = sdk ? await (await import("./harness-engine.js")).loadPiCodingAgent() : undefined;
28
+ const installed = await installedPiExtensions(config, snapshot.cwd, sdkReader);
28
29
  const archived = snapshot.archived;
29
30
  const modifiable = !archived;
30
31
  const goalAvailable = installed.some((item) => item.goal);
31
32
  const localDecisionAvailable = config.localDecision?.enabled === true || decisions?.status().enabled === true;
32
- const inventory = await discoverPiResources(config, snapshot.cwd);
33
- const resourceCatalog = { ...inventory.catalog, supported: modifiable && !sdk && inventory.catalog.supported,
34
- reason: archived ? "请先恢复已归档的会话。" : sdk ? "Wand Agent 尚不支持 Skills / MCP 会话选择。" : inventory.catalog.reason };
33
+ const catalog = sdk ? { supported: false, reason: "Wand Agent 尚不支持 Skills / MCP 会话选择。", skills: [], mcpServers: [] }
34
+ : (await discoverPiResources(config, snapshot.cwd)).catalog;
35
+ const resourceCatalog = { ...catalog, supported: modifiable && !sdk && catalog.supported,
36
+ reason: archived ? "请先恢复已归档的会话。" : sdk ? "Wand Agent 尚不支持 Skills / MCP 会话选择。" : catalog.reason };
35
37
  const controls = {
36
38
  // 基础工具与 CodeMode 在两种引擎下都能表达(CLI 通过 --tools 白名单)。
37
39
  tools: modifiable,
@@ -61,7 +61,7 @@ export function registerSessionLifecycleRoutes(app, ctx) {
61
61
  res.status(400).json({ error: "Wand Agent 只支持 Pi 结构化会话。" });
62
62
  return;
63
63
  }
64
- const engineDecision = requestedEngine ? structured.resolveNewSessionPiEngine(requestedEngine) : null;
64
+ const engineDecision = requestedEngine ? await structured.resolveNewSessionPiEngine(requestedEngine) : null;
65
65
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
66
66
  const origin = parseSessionCreationOrigin(body);
67
67
  // Explicit employees/custom role rules and internal automation retain their own role.
@@ -175,7 +175,7 @@ export function registerSessionLifecycleRoutes(app, ctx) {
175
175
  sendRouteError(res, error, "切换员工失败。");
176
176
  }
177
177
  });
178
- app.post("/api/sessions/:id/provider", (req, res) => {
178
+ app.post("/api/sessions/:id/provider", asyncRoute(async (req, res) => {
179
179
  const provider = req.body?.provider;
180
180
  const engine = req.body?.engine;
181
181
  if (engine !== undefined && engine !== "cli" && engine !== "sdk") {
@@ -201,12 +201,14 @@ export function registerSessionLifecycleRoutes(app, ctx) {
201
201
  return;
202
202
  }
203
203
  try {
204
+ if (engine === "sdk")
205
+ await structured.resolveNewSessionPiEngine(engine);
204
206
  res.json(sessionResponseDTO(structured.setSessionProvider(id, provider, engine), req));
205
207
  }
206
208
  catch (error) {
207
209
  sendRouteError(res, error, "切换工具失败。");
208
210
  }
209
- });
211
+ }));
210
212
  app.get("/api/sessions/:id/pi-settings", asyncRoute(async (req, res) => {
211
213
  try {
212
214
  requirePiSession(req.params.id);