@co0ontty/wand 4.83.1 → 4.84.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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
+ import { setLoginShellEnv } from "./env-utils.js";
2
3
  import { compareSemver } from "./version-utils.js";
3
4
  import { getErrorMessage } from "./error-utils.js";
4
5
  import { providerCliCommand, SESSION_PROVIDERS } from "./session-provider.js";
@@ -177,6 +178,7 @@ export async function deepRepairRuntimePath(result, opts = {}) {
177
178
  result.deepProbe = "skipped";
178
179
  return result;
179
180
  }
181
+ result.shell = shell;
180
182
  let probe;
181
183
  try {
182
184
  probe = await probeLoginShell(shell, opts.timeoutMs ?? DEEP_PROBE_TIMEOUT_MS);
@@ -186,6 +188,13 @@ export async function deepRepairRuntimePath(result, opts = {}) {
186
188
  result.deepProbe = "failed";
187
189
  return result;
188
190
  }
191
+ if (process.env.WAND_SHELL_ENV_DISABLE === "1") {
192
+ result.warnings.push("跳过登录 shell 环境回收:WAND_SHELL_ENV_DISABLE=1");
193
+ }
194
+ else {
195
+ setLoginShellEnv(probe.env);
196
+ result.shellEnvVars = Object.keys(probe.env).filter((key) => !(key in process.env)).length;
197
+ }
189
198
  const delim = path.delimiter;
190
199
  const currentSegments = (process.env.PATH ?? "")
191
200
  .split(delim)
@@ -237,6 +246,32 @@ export async function deepRepairRuntimePath(result, opts = {}) {
237
246
  result.deepProbe = "success";
238
247
  return result;
239
248
  }
249
+ /** 探测用的 shell 自身状态,不回填给会话(探测把 PS1 清空只为解析 stdout)。 */
250
+ const PROBE_ONLY_ENV_KEYS = ["PS0", "PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"];
251
+ const ENV_LINE = /^[A-Za-z_][A-Za-z0-9_]*=/;
252
+ const ENV_BEGIN_MARKER = "WAND_ENV_BEGIN_7c1f";
253
+ const ENV_END_MARKER = "WAND_ENV_END_7c1f";
254
+ /**
255
+ * 解析 env 输出。只接受形如 `NAME=value` 的行:交互式 rc 可能往 stdout 喷无关内容,
256
+ * 把噪声行当"上一个值的续行"会污染变量,宁可丢掉内嵌换行这种罕见值。
257
+ */
258
+ function parseEnvDump(stdout) {
259
+ const begin = stdout.indexOf(`${ENV_BEGIN_MARKER}\n`);
260
+ const end = stdout.lastIndexOf(`\n${ENV_END_MARKER}`);
261
+ const env = {};
262
+ if (begin < 0 || end < begin)
263
+ return env;
264
+ for (const line of stdout.slice(begin + ENV_BEGIN_MARKER.length + 1, end).split("\n")) {
265
+ if (!ENV_LINE.test(line))
266
+ continue;
267
+ const idx = line.indexOf("=");
268
+ const key = line.slice(0, idx);
269
+ if (PROBE_ONLY_ENV_KEYS.includes(key))
270
+ continue;
271
+ env[key] = line.slice(idx + 1);
272
+ }
273
+ return env;
274
+ }
240
275
  function pickProbeShell(configured) {
241
276
  const candidates = [
242
277
  configured,
@@ -254,8 +289,8 @@ function pickProbeShell(configured) {
254
289
  return null;
255
290
  }
256
291
  /**
257
- * 用与 PTY provider 相同的交互式 login shell 跑最小脚本,拿到实际的 PATH 和 CLI 路径。
258
- * 用 \x1f(ASCII Unit Separator)作字段分隔符避免和路径里的字符冲突。
292
+ * 用与 PTY provider 相同的交互式 login shell 跑最小脚本,拿到实际的 PATH、CLI 路径和
293
+ * rc 生效后的完整环境。用 \x1f(ASCII Unit Separator)作字段分隔符避免和路径里的字符冲突。
259
294
  */
260
295
  function probeLoginShell(shell, timeoutMs) {
261
296
  const script = `printf 'PATH\\x1f%s\\n' "$PATH"; ` +
@@ -263,7 +298,8 @@ function probeLoginShell(shell, timeoutMs) {
263
298
  `printf 'CODEX\\x1f%s\\n' "$(command -v codex 2>/dev/null)"; ` +
264
299
  `printf 'OPENCODE\\x1f%s\\n' "$(command -v opencode 2>/dev/null)"; ` +
265
300
  `printf 'GROK\\x1f%s\\n' "$(command -v grok 2>/dev/null)"; ` +
266
- `printf 'QODERCLI\\x1f%s\\n' "$(command -v qodercli 2>/dev/null)"`;
301
+ `printf 'QODERCLI\\x1f%s\\n' "$(command -v qodercli 2>/dev/null)"; ` +
302
+ `printf '\\n${ENV_BEGIN_MARKER}\\n'; command env; printf '${ENV_END_MARKER}\\n'`;
267
303
  return new Promise((resolve, reject) => {
268
304
  const child = spawn(shell, ["-lic", script], {
269
305
  env: {
@@ -303,7 +339,15 @@ function probeLoginShell(shell, timeoutMs) {
303
339
  reject(new Error(`login shell exited ${code}: ${stderr.trim().slice(0, 200)}`));
304
340
  return;
305
341
  }
306
- const out = { path: "", claude: null, codex: null, opencode: null, grok: null, qodercli: null };
342
+ const out = {
343
+ path: "",
344
+ claude: null,
345
+ codex: null,
346
+ opencode: null,
347
+ grok: null,
348
+ qodercli: null,
349
+ env: parseEnvDump(stdout),
350
+ };
307
351
  for (const line of stdout.split("\n")) {
308
352
  const idx = line.indexOf("\x1f");
309
353
  if (idx < 0)
@@ -378,7 +422,8 @@ export function formatPathRepairSummary(result) {
378
422
  parts.push(`deep-probe: ${result.warnings[0]}`);
379
423
  }
380
424
  else if (result.deepProbe === "success") {
381
- parts.push("deep-probe: ok");
425
+ const envSuffix = result.shellEnvVars ? `, +${result.shellEnvVars} env vars` : "";
426
+ parts.push(`deep-probe: ok${result.shell ? ` (${result.shell})` : ""}${envSuffix}`);
382
427
  }
383
428
  return parts.join("; ");
384
429
  }
@@ -0,0 +1,12 @@
1
+ import { type PiResourceInventory } from "./pi-resource-catalog.js";
2
+ import { type PiResourceEvaluator } from "./pi-resource-recommendation.js";
3
+ import type { StructuredRunnerAdapter } from "./structured-runner.js";
4
+ import type { WandConfig } from "./types.js";
5
+ interface AutoResourceOptions {
6
+ evaluate(): PiResourceEvaluator | undefined;
7
+ inventory?: (cwd: string) => Promise<PiResourceInventory>;
8
+ timeoutMs?: number;
9
+ }
10
+ /** Owned by the submitted runner, not by the composer. Manual settings are never overwritten. */
11
+ export declare function withAutomaticPiResources(runner: StructuredRunnerAdapter, config: Pick<WandConfig, "harness">, options: AutoResourceOptions): StructuredRunnerAdapter;
12
+ export {};
@@ -0,0 +1,151 @@
1
+ import { DecisionError } from "./decision-types.js";
2
+ import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
3
+ import { decidePiCodemode, parsePiRecommendationRequest, recommendPiResources } from "./pi-resource-recommendation.js";
4
+ import { patchPiResourceSelection } from "./pi-session-settings.js";
5
+ /** Owned by the submitted runner, not by the composer. Manual settings are never overwritten. */
6
+ export function withAutomaticPiResources(runner, config, options) {
7
+ return { start(context, observer) {
8
+ if (context.session.provider !== "pi" || context.session.piSettings?.autoResources !== true) {
9
+ return runner.start(context, observer);
10
+ }
11
+ const abort = new AbortController();
12
+ let interrupted = false;
13
+ let execution = null;
14
+ let notice = { status: "selecting", label: "正在自动选择 Skills / MCP / CodeMode…", skills: [], mcpServers: [] };
15
+ const withNotice = (state) => ({ ...state, resourceSelection: notice });
16
+ const publish = () => {
17
+ if (observer.isActive() && !interrupted)
18
+ observer.onUpdate(withNotice({ blocks: [], result: "", sessionId: context.session.claudeSessionId }));
19
+ };
20
+ // The manager receives ownership synchronously; all asynchronous work starts in the next microtask.
21
+ const spawnedAt = new Date().toISOString();
22
+ const completion = Promise.resolve().then(async () => {
23
+ const checkActive = () => {
24
+ if (interrupted || !observer.isActive())
25
+ throw new DecisionError("CANCELLED", "自动选择已取消,尚未启动 Pi。", 499);
26
+ };
27
+ checkActive();
28
+ publish();
29
+ const manual = context.session.piSettings?.resources ?? { skills: [], mcpServers: [] };
30
+ const locked = new Set(context.session.piSettings?.lockedSkills ?? []);
31
+ let selected = manual;
32
+ const manualCodemode = context.session.piSettings?.codemodeOverride;
33
+ let automaticCodemode;
34
+ let inventory;
35
+ let fallback = "";
36
+ let timer;
37
+ try {
38
+ inventory = await (options.inventory?.(context.session.cwd) ?? discoverPiResources(config, context.session.cwd));
39
+ checkActive();
40
+ // A missing manual resource must fail as usual, never be silently replaced by an automatic pick.
41
+ resolvePiResourceSelection(inventory, manual);
42
+ const evaluate = context.session.piSettings?.localDecision === false ? undefined : options.evaluate();
43
+ if (!evaluate)
44
+ throw new DecisionError("UNAVAILABLE", "本地决策不可用");
45
+ const candidates = {
46
+ skills: inventory.catalog.skills.filter((item) => !locked.has(item.id)).map((item) => item.id),
47
+ mcpServers: inventory.catalog.mcpServers.filter((item) => inventory.servers.get(item.id)?.config.enabled !== false)
48
+ .map((item) => item.id),
49
+ };
50
+ const request = parsePiRecommendationRequest({ prompt: context.prompt, candidates });
51
+ timer = setTimeout(() => abort.abort(), options.timeoutMs ?? 15_000);
52
+ const boundedEvaluate = (value, caller, signal) => new Promise((resolve, reject) => {
53
+ const cancelled = () => reject(new DecisionError("CANCELLED", "本地选择已取消"));
54
+ if (signal?.aborted) {
55
+ cancelled();
56
+ return;
57
+ }
58
+ signal?.addEventListener("abort", cancelled, { once: true });
59
+ Promise.resolve().then(() => { checkActive(); return evaluate(value, caller, signal); }).then(resolve, reject)
60
+ .finally(() => signal?.removeEventListener("abort", cancelled));
61
+ });
62
+ if (!manualCodemode)
63
+ automaticCodemode = await decidePiCodemode({
64
+ prompt: request.prompt, evaluate: boundedEvaluate, caller: `pi-auto-resources:${context.session.id}`, signal: abort.signal,
65
+ });
66
+ const result = await recommendPiResources({ ...request, catalog: inventory.catalog, evaluate: boundedEvaluate,
67
+ caller: `pi-auto-resources:${context.session.id}`, signal: abort.signal });
68
+ checkActive();
69
+ if (abort.signal.aborted)
70
+ throw new DecisionError("TIMEOUT", "本地选择超时");
71
+ selected = patchPiResourceSelection({
72
+ skills: [...new Set([...manual.skills.filter((id) => locked.has(id)),
73
+ ...result.selection.skills.filter((id) => !locked.has(id))])],
74
+ mcpServers: [...new Set([...manual.mcpServers, ...result.selection.mcpServers])],
75
+ });
76
+ resolvePiResourceSelection(inventory, selected);
77
+ }
78
+ catch (error) {
79
+ checkActive();
80
+ selected = manual;
81
+ automaticCodemode = undefined;
82
+ // Explicit manual constraints remain authoritative even when inference failed.
83
+ if (inventory)
84
+ resolvePiResourceSelection(inventory, manual);
85
+ fallback = error instanceof DecisionError ? error.code === "CANDIDATE_LIMIT" ? "候选过多"
86
+ : error.code === "INVALID_REQUEST" ? "提示词超出本地选择范围"
87
+ : error.code === "TIMEOUT" || abort.signal.aborted ? "本地选择超时" : "本地决策不可用"
88
+ : "资源目录暂不可用";
89
+ }
90
+ finally {
91
+ if (timer)
92
+ clearTimeout(timer);
93
+ }
94
+ checkActive();
95
+ const names = (kind) => selected[kind].map((id) => (inventory?.catalog[kind].find((item) => item.id === id)?.name ?? "已手选资源").slice(0, 80));
96
+ const skills = names("skills");
97
+ const mcpServers = names("mcpServers");
98
+ const codemode = {
99
+ mode: manualCodemode ?? automaticCodemode?.override ?? "follow",
100
+ source: manualCodemode ? "manual" : fallback ? "fallback" : automaticCodemode?.override ? "automatic" : "uncertain",
101
+ };
102
+ const codemodeLabel = codemode.mode === "on" ? "启用" : codemode.mode === "off" ? "关闭"
103
+ : codemode.mode === "only" ? "仅 CodeMode" : "跟随 Pi";
104
+ const chosen = [skills.length ? `Skills:${skills.join("、")}` : "", mcpServers.length ? `MCP:${mcpServers.join("、")}` : "",
105
+ `CodeMode:${codemodeLabel}${codemode.source === "manual" ? "(手动)" : codemode.source === "uncertain" ? "(判断不确定)" : ""}`].filter(Boolean).join(";");
106
+ notice = { status: fallback ? "fallback" : "selected", skills, mcpServers, codemode,
107
+ label: fallback ? `自动选择:${fallback},沿用手选(${chosen})` : `本轮选择 · ${chosen}` };
108
+ if (notice.label.length > 1024)
109
+ notice = { ...notice, label: `${notice.label.slice(0, 1023)}…` };
110
+ // This is a server-owned selection record, not a fabricated provider tool call.
111
+ observer.onEvent?.({ type: "wand_pi_resource_selection", ...notice });
112
+ publish();
113
+ const scoped = { ...context, session: { ...context.session,
114
+ piSettings: { ...context.session.piSettings, resources: selected,
115
+ ...(!manualCodemode && automaticCodemode?.override ? { codemodeOverride: automaticCodemode.override } : {}) } } };
116
+ const scopedObserver = { ...observer, onUpdate: (state) => {
117
+ if (!interrupted && observer.isActive())
118
+ observer.onUpdate(withNotice(state));
119
+ } };
120
+ checkActive();
121
+ execution = runner.start(scoped, scopedObserver);
122
+ const result = await execution.completion;
123
+ // Selection notices are metadata, not model execution. A fallback tool starts with
124
+ // its own configuration; cancelled/failed preparation below is still never retried.
125
+ return { ...result, state: withNotice(result.state) };
126
+ }).catch((error) => {
127
+ if (notice.status === "selecting") {
128
+ notice = interrupted || error instanceof DecisionError && error.code === "CANCELLED"
129
+ ? { ...notice, status: "cancelled", label: "本轮自动选择已取消" }
130
+ : { ...notice, status: "fallback", label: "自动配置准备失败,尚未启动 Pi" };
131
+ }
132
+ return {
133
+ state: withNotice({ blocks: [], result: "", sessionId: context.session.claudeSessionId }),
134
+ exitCode: 1, signal: null, stderr: "", primaryError: error instanceof Error ? error.message : "自动选择准备失败。",
135
+ inputAccepted: false, retryForbidden: true,
136
+ };
137
+ });
138
+ return { get args() { return execution?.args ?? []; }, get pid() { return execution?.pid ?? null; },
139
+ spawnedAt, completion, interrupt: () => {
140
+ if (interrupted)
141
+ return;
142
+ if (notice.status === "selecting") {
143
+ notice = { ...notice, status: "cancelled", label: "本轮自动选择已取消" };
144
+ publish();
145
+ }
146
+ interrupted = true;
147
+ abort.abort();
148
+ execution?.interrupt();
149
+ } };
150
+ } };
151
+ }
@@ -0,0 +1,48 @@
1
+ export type PiExecutionState = "pending" | "running" | "background" | "completed" | "failed" | "paused" | "stopped" | "detached" | "rejected" | "skipped" | "partial" | "unknown";
2
+ export interface PiExecutionNode {
3
+ id: string;
4
+ parentId?: string;
5
+ label: string;
6
+ kind: string;
7
+ state: PiExecutionState;
8
+ phase?: string;
9
+ agent?: string;
10
+ model?: string;
11
+ runId?: string;
12
+ task?: string;
13
+ currentTool?: string;
14
+ durationMs?: number;
15
+ tokens?: number;
16
+ toolCount?: number;
17
+ error?: string;
18
+ output?: string;
19
+ }
20
+ export interface PiExecutionSnapshot {
21
+ version: 1;
22
+ source: "tool-progress" | "tool-result" | "status-file";
23
+ mode: string;
24
+ name?: string;
25
+ runId?: string;
26
+ state: PiExecutionState;
27
+ inventoryComplete: boolean;
28
+ updatedAt?: number;
29
+ nodes: PiExecutionNode[];
30
+ trace: Array<{
31
+ key: string;
32
+ operation: string;
33
+ state: string;
34
+ phase?: string;
35
+ durationMs?: number;
36
+ error?: string;
37
+ }>;
38
+ omittedNodes: number;
39
+ omittedTrace: number;
40
+ }
41
+ export interface PiExecutionResponse {
42
+ toolId: string;
43
+ toolName: string;
44
+ input: Record<string, unknown>;
45
+ script?: string;
46
+ snapshot: PiExecutionSnapshot | null;
47
+ notice?: string;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { ContentBlock, ConversationTurn } from "./types.js";
2
+ import type { PiExecutionSnapshot, PiExecutionState, PiExecutionResponse } from "./pi-execution-types.js";
3
+ export declare function isPiExecutionTool(name: string): boolean;
4
+ /** Only execution fields cross the host boundary; never forward arbitrary extension details. */
5
+ export declare function projectPiExecution(raw: unknown, source: PiExecutionSnapshot["source"], fallbackState: PiExecutionState): PiExecutionSnapshot | null;
6
+ /** Partial updates update the invocation, never create a final tool_result or imply delivery. */
7
+ export declare function capturePiExecutionEvent(blocks: ContentBlock[], event: Record<string, unknown>): void;
8
+ /** Inspect only a run referenced by this session. Old receipts use the package's fixed temp roots. */
9
+ export declare function inspectPiExecution(messages: ConversationTurn[], toolId: string): Promise<PiExecutionResponse | null>;
@@ -0,0 +1,187 @@
1
+ import { readdir, open } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const MAX_NODES = 80;
6
+ const MAX_TRACE = 128;
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f-]{27,55}$/i;
8
+ const record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
9
+ const text = (v, max = 256) => typeof v === "string" && v.trim() ? v.slice(0, max) : undefined;
10
+ const number = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : undefined;
11
+ const array = (v) => Array.isArray(v) ? v : [];
12
+ export function isPiExecutionTool(name) {
13
+ return ["subagent", "workflow"].includes(name.replace(/^Pi\//, ""));
14
+ }
15
+ function state(v) {
16
+ const aliases = { complete: "completed", done: "completed", error: "failed", queued: "pending", started: "running" };
17
+ const s = String(v ?? "");
18
+ return aliases[s] ?? (["pending", "running", "background", "completed", "failed", "paused", "stopped", "detached", "rejected", "skipped", "partial"].includes(s) ? s : "unknown");
19
+ }
20
+ /** Only execution fields cross the host boundary; never forward arbitrary extension details. */
21
+ export function projectPiExecution(raw, source, fallbackState) {
22
+ const d = record(raw);
23
+ const children = record(d.workflowChildren);
24
+ if (!Object.keys(d).length || !(d.mode || d.workflowChildren || d.workflowGraph || Array.isArray(d.agents) || Array.isArray(d.progress)))
25
+ return null;
26
+ const workflow = record(d.workflow);
27
+ const steps = array(d.steps);
28
+ const trace = array(workflow.trace);
29
+ const rows = [];
30
+ function add(rawRow, id, parentId) {
31
+ const r = record(rawRow);
32
+ const activity = record(r.activity);
33
+ const node = {
34
+ id: text(id) ?? String(rows.length), label: text(r.label ?? r.sessionName ?? r.agent ?? r.childId ?? r.key) ?? id,
35
+ kind: text(r.kind) ?? "agent", state: state(r.state ?? r.status),
36
+ };
37
+ if (parentId)
38
+ node.parentId = parentId;
39
+ for (const [key, value] of Object.entries({ phase: r.phase, agent: r.agent, model: r.model, runId: r.runId,
40
+ task: r.task ?? r.description ?? r.prompt, currentTool: activity.currentTool ?? r.currentTool,
41
+ error: r.error, output: r.resultPreview ?? r.recentOutput })) {
42
+ const valueText = text(value, ["task", "error", "output"].includes(key) ? 2000 : 256);
43
+ if (valueText)
44
+ node[key] = valueText;
45
+ }
46
+ for (const key of ["durationMs", "tokens", "toolCount"]) {
47
+ const value = number(activity[key] ?? r[key]);
48
+ if (value !== undefined)
49
+ node[key] = value;
50
+ }
51
+ rows.push(node);
52
+ }
53
+ function graphNodes(nodes, parentId, depth = 0) {
54
+ if (depth > 8)
55
+ return;
56
+ for (const r of nodes.slice(0, 256)) {
57
+ const id = text(r?.id) ?? `node-${rows.length}`;
58
+ add(r, id, parentId);
59
+ graphNodes(array(r?.children), id, depth + 1);
60
+ }
61
+ }
62
+ if (array(d.workflowGraph?.nodes).length)
63
+ graphNodes(d.workflowGraph.nodes);
64
+ else if (Array.isArray(children.children)) {
65
+ for (const child of children.children.slice(0, 256)) {
66
+ const step = steps.find(s => s.workflowKey === child.childId) ?? {};
67
+ const entry = [...trace].reverse().find(t => t.operation === "run" && t.key === child.childId) ?? {};
68
+ add({ ...entry, ...step, ...child }, String(child.childId));
69
+ }
70
+ }
71
+ else {
72
+ const sourceRows = Array.isArray(d.agents) ? d.agents : steps.length ? steps : array(d.progress).length ? d.progress : array(d.results);
73
+ sourceRows.slice(0, 256).forEach((r, i) => add({ ...r,
74
+ state: r.state ?? r.status ?? (r.detached ? "detached" : r.interrupted ? "paused" : typeof r.exitCode === "number" ? (r.exitCode === 0 ? "completed" : "failed") : undefined),
75
+ }, String(r.workflowKey ?? r.id ?? `step-${i}`)));
76
+ }
77
+ const totalNodes = Math.max(rows.length, array(children.children).length, steps.length, array(d.agents).length);
78
+ return {
79
+ version: 1, source, mode: text(d.mode) ?? "workflow", name: text(d.name),
80
+ runId: text(d.runId ?? children.workflowRunId ?? d.asyncId),
81
+ state: state(children.workflowState ?? d.state ?? (d.asyncId ? "background" : fallbackState)),
82
+ inventoryComplete: typeof children.inventoryComplete === "boolean" ? children.inventoryComplete : source === "tool-result" && !d.asyncId,
83
+ updatedAt: number(d.lastUpdate ?? d.updatedAt), nodes: rows.slice(0, MAX_NODES),
84
+ trace: trace.slice(-MAX_TRACE).map(t => ({ key: text(t.key) ?? "", operation: text(t.operation) ?? "", state: text(t.state) ?? "unknown",
85
+ phase: text(t.phase), durationMs: number(t.durationMs), error: text(t.error, 1000) })),
86
+ omittedNodes: Math.max(0, totalNodes - MAX_NODES), omittedTrace: Math.max(0, trace.length - MAX_TRACE),
87
+ };
88
+ }
89
+ /** Partial updates update the invocation, never create a final tool_result or imply delivery. */
90
+ export function capturePiExecutionEvent(blocks, event) {
91
+ if (event.type !== "tool_execution_update" && event.type !== "tool_execution_end")
92
+ return;
93
+ const use = blocks.find((b) => b.type === "tool_use" && b.id === event.toolCallId);
94
+ if (!use || !isPiExecutionTool(use.name) || use.input.action)
95
+ return;
96
+ const result = record(event.type === "tool_execution_update" ? event.partialResult : event.result);
97
+ const details = record(result.details);
98
+ const source = event.type === "tool_execution_update" ? "tool-progress" : "tool-result";
99
+ const snapshot = projectPiExecution(details, source, event.isError ? "failed" : source === "tool-progress" ? "running" : "completed");
100
+ if (snapshot)
101
+ use.execution = snapshot;
102
+ const runId = text(details.asyncId);
103
+ const directory = text(details.asyncDir, 4096);
104
+ if (runId && UUID.test(runId) && directory && path.isAbsolute(directory) && path.basename(directory) === runId) {
105
+ use.piExecutionRef = { runId, directory };
106
+ }
107
+ }
108
+ function resultText(result) {
109
+ return typeof result?.content === "string" ? result.content : array(result?.content).map(p => text(p?.text, 8000) ?? "").join("\n");
110
+ }
111
+ function legacyReceiptRunId(result) {
112
+ if (result?.is_error)
113
+ return;
114
+ const lines = resultText(result).trim().split("\n");
115
+ if (!/^Run fan-out:\s*\d+\/\d+ used\b/.test(lines[0] ?? ""))
116
+ return;
117
+ for (const line of lines.slice(1, 25)) {
118
+ if (!/^Async(?: workflow\b|:)/.test(line.trim()))
119
+ continue;
120
+ const id = [...line.matchAll(/\[([0-9a-f-]{36,64})\]/gi)].at(-1)?.[1];
121
+ if (id && UUID.test(id))
122
+ return id;
123
+ }
124
+ }
125
+ async function readStatus(directory, runId, toolId) {
126
+ // References come from this session's extension result, never an HTTP path parameter.
127
+ const handle = await open(path.join(directory, "status.json"), constants.O_RDONLY | constants.O_NOFOLLOW);
128
+ try {
129
+ const stat = await handle.stat();
130
+ if (!stat.isFile() || stat.size > 2 * 1024 * 1024)
131
+ throw new Error("执行状态超出读取上限");
132
+ const raw = JSON.parse(await handle.readFile("utf8"));
133
+ if ((raw.runId ?? raw.id) !== runId || (raw.toolCallId && raw.toolCallId !== toolId))
134
+ throw new Error("执行身份不匹配");
135
+ return raw;
136
+ }
137
+ finally {
138
+ await handle.close();
139
+ }
140
+ }
141
+ /** Inspect only a run referenced by this session. Old receipts use the package's fixed temp roots. */
142
+ export async function inspectPiExecution(messages, toolId) {
143
+ let use;
144
+ let result;
145
+ let dispatchTurn;
146
+ for (const turn of messages)
147
+ for (const b of turn.content) {
148
+ if (b.type === "tool_use" && b.id === toolId) {
149
+ use = b;
150
+ dispatchTurn = turn;
151
+ }
152
+ if (b.type === "tool_result" && b.tool_use_id === toolId)
153
+ result = b;
154
+ }
155
+ if (!use || !isPiExecutionTool(use.name) || use.input.action)
156
+ return null;
157
+ let snapshot = use.execution ?? null;
158
+ let notice;
159
+ const runId = use.piExecutionRef?.runId ?? legacyReceiptRunId(result);
160
+ if (runId) {
161
+ let directories = use.piExecutionRef ? [use.piExecutionRef.directory] : [];
162
+ if (!directories.length) {
163
+ const entries = await readdir(os.tmpdir(), { withFileTypes: true }).catch(() => []);
164
+ directories = entries.filter(e => e.isDirectory() && /^pi-subagents-[\w-]+$/.test(e.name)).slice(0, 64)
165
+ .map(e => path.join(os.tmpdir(), e.name, "async-subagent-runs", runId));
166
+ }
167
+ let found = false;
168
+ for (const directory of directories) {
169
+ try {
170
+ const raw = await readStatus(directory, runId, toolId);
171
+ snapshot = projectPiExecution(raw, "status-file", "unknown");
172
+ found = !!snapshot;
173
+ if (found)
174
+ break;
175
+ }
176
+ catch { /* Retention, old versions and temporary writes leave the captured snapshot available. */ }
177
+ }
178
+ if (!found)
179
+ notice = "后台状态文件不可读取或已清理;以下保留最后一次工具快照,不代表当前运行状态。";
180
+ }
181
+ if (!snapshot)
182
+ notice = "这次调用没有可读取的结构数据;保留派发参数,无法确认子任务或完成状态。";
183
+ const inlineScript = typeof use.input.script === "string" ? use.input.script : undefined;
184
+ const replyScript = use.input.workflow === true || use.input.workflow === "true"
185
+ ? dispatchTurn?.content.filter(b => b.type === "text").map(b => b.text).join("\n").match(/```(?:js|javascript) workflow\s*\n([\s\S]*?)\n```/)?.[1] : undefined;
186
+ return { toolId, toolName: use.name, input: use.input, script: (inlineScript ?? replyScript)?.slice(0, 64_000), snapshot, notice };
187
+ }
@@ -0,0 +1,13 @@
1
+ import { loadPiCodingAgent } from "./harness-engine.js";
2
+ import type { WandConfig } from "./types.js";
3
+ import type { PiSessionSettings } from "./pi-session-settings.js";
4
+ import type { ResourceLoader } from "@earendil-works/pi-coding-agent";
5
+ type CodingAgent = Awaited<ReturnType<typeof loadPiCodingAgent>>;
6
+ /** 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
+ path: string;
9
+ name: string;
10
+ goal: boolean;
11
+ }>>;
12
+ export declare function createPiExtensionResources(pi: CodingAgent, config: WandConfig, cwd: string, settings: PiSessionSettings, systemPrompt: string): Promise<ResourceLoader>;
13
+ export {};
@@ -0,0 +1,71 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { coreHarnessAgentDir, loadPiCodingAgent } from "./harness-engine.js";
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");
22
+ return resolved.extensions.filter((item) => item.enabled && item.metadata.scope === "user").map((item) => {
23
+ const name = item.metadata.source === "local" || item.metadata.source === "auto"
24
+ ? path.basename(item.path) : item.metadata.source.replace(/^npm:/, "");
25
+ return { path: item.path, name, goal: /(?:^|[/:])pi-goal(?:[/@]|$)/.test(`${item.metadata.source}/${item.path}`) };
26
+ });
27
+ }
28
+ /**
29
+ * The pinned SDK's public ResourceLoader caches imported factories by cwd. Many user extensions
30
+ * (including pi-goal) keep module-level state; sharing those factories leaks across Wand sessions.
31
+ * Use its uncached file loader, at this one version-locked boundary, with explicit installed paths.
32
+ */
33
+ async function nativeExtensionLoader() {
34
+ const url = new URL("./core/extensions/loader.js", import.meta.resolve("@earendil-works/pi-coding-agent"));
35
+ const loaded = await import(url.href);
36
+ if (typeof loaded.loadExtensions !== "function" || typeof loaded.loadExtensionFromFactory !== "function") {
37
+ throw new Error("当前 Pi SDK 不支持隔离扩展加载;请检查锁定的 SDK 版本。");
38
+ }
39
+ return loaded;
40
+ }
41
+ export async function createPiExtensionResources(pi, config, cwd, settings, systemPrompt) {
42
+ const resources = settings.globalTools || settings.goalMode ? await installedPiExtensions(config, cwd) : [];
43
+ if (settings.goalMode && !resources.some((item) => item.goal)) {
44
+ throw new Error("目标模式不可用:请先在 Pi 中安装并启用 pi-goal。");
45
+ }
46
+ const paths = resources.filter((item) => item.goal ? settings.goalMode : settings.globalTools).map((item) => item.path);
47
+ const loader = await nativeExtensionLoader();
48
+ const runtime = pi.createExtensionRuntime();
49
+ const events = pi.createEventBus();
50
+ const result = await loader.loadExtensions(paths, cwd, events, runtime);
51
+ if (result.errors.length)
52
+ throw new Error(`Pi 扩展加载失败:${result.errors.map((item) => `${path.basename(item.path)}: ${item.error}`).join(";")}`);
53
+ if (settings.codemode !== "off") {
54
+ result.extensions.push(await loader.loadExtensionFromFactory(
55
+ // Only orchestration of enabled tools. No implicit classifier/image API access.
56
+ pi.createCodemodeExtension({ mode: settings.codemode, models: false }), cwd, events, runtime, "wand:codemode"));
57
+ }
58
+ return {
59
+ getExtensions: () => result,
60
+ getSkills: () => ({ skills: [], diagnostics: [] }),
61
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
62
+ getThemes: () => ({ themes: [], diagnostics: [] }),
63
+ getAgentsFiles: () => ({ agentsFiles: [] }),
64
+ getSystemPrompt: () => systemPrompt,
65
+ getSystemPromptSource: () => undefined,
66
+ getAppendSystemPrompt: () => [],
67
+ getAppendSystemPromptSources: () => [],
68
+ extendResources: () => { },
69
+ reload: async () => { throw new Error("原生 Pi 扩展按回合加载;请在下一轮重载。"); },
70
+ };
71
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export default function wandOpenRouterFree(pi: ExtensionAPI): void;