@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

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 (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -0,0 +1,388 @@
1
+ /**
2
+ * pi-package-resolver — shared helper that resolves a pi peer package
3
+ * name to its install directory and importable entry path by walking
4
+ * pi's own settings.json files.
5
+ *
6
+ * Pi-coding-agent installs packages into three different filesystem
7
+ * layouts depending on how the user added them to `settings.json#packages[]`:
8
+ *
9
+ * npm:<name>[@version] → ~/.pi/agent/node_modules/<name>/ (user scope)
10
+ * <cwd>/.pi/npm/node_modules/<name>/ (project)
11
+ * git+https://… / git@…/ → ~/.pi/agent/git/<host>/<owner>/<repo>/ (user)
12
+ * <cwd>/.pi/git/<host>/<owner>/<repo>/ (project)
13
+ * /abs/path → the path itself
14
+ * ./rel/path → resolved against settings dir
15
+ *
16
+ * None of these locations are on Node's `node_modules` walk from
17
+ * `process.cwd()`, so `createRequire(cwd).resolve(spec)` fails for every
18
+ * pi-installed peer. This module walks pi's own settings + applies the
19
+ * same path arithmetic pi-coding-agent uses internally, then matches by
20
+ * `package.json#name` to expose an `await import(absPath)`-ready result.
21
+ *
22
+ * Read-on-call contract — performs no installs, mutates nothing, holds
23
+ * no module-level cache. Two settings reads + N package.json reads per
24
+ * resolution; each settings file is ~1 KB. Designed to be called from
25
+ * plugin bridges (`packages/shared/`-only imports allowed) on every
26
+ * probe without amortization.
27
+ *
28
+ * Scope only: walks `packages[]` from `~/.pi/agent/settings.json` and
29
+ * `<cwd>/.pi/settings.json`. Does NOT walk `extensions[]`/`skills[]`/
30
+ * `prompts[]` top-level arrays — those hold file paths to individual
31
+ * extension entry files, not package roots, so `package.json#name`
32
+ * lookup doesn't apply.
33
+ *
34
+ * See change: add-shared-pi-package-resolver.
35
+ */
36
+ import * as fs from "node:fs";
37
+ import * as os from "node:os";
38
+ import * as path from "node:path";
39
+
40
+ import { rootGlobalOr } from "./platform/npm.js";
41
+
42
+ // ── Public surface ──────────────────────────────────────────────────
43
+
44
+ export interface ResolvePiPackageOptions {
45
+ /** Override `~/.pi/agent`. Default: `path.join(os.homedir(), ".pi", "agent")`. */
46
+ agentDir?: string;
47
+ /** Project-scope cwd (enables reading `<cwd>/.pi/settings.json`). */
48
+ cwd?: string;
49
+ /** "user" | "project" | "any" (default). "any" reads project first, then user. */
50
+ scope?: "user" | "project" | "any";
51
+ /**
52
+ * Override `npm root -g`. Defaults to `npm.rootGlobalOr("")`. Tests pass
53
+ * a tmp path to avoid shelling out and to make `npm:` resolution
54
+ * hermetic.
55
+ */
56
+ npmRoot?: string;
57
+ }
58
+
59
+ export interface ResolvedPiPackage {
60
+ /** Absolute path to the package root. */
61
+ packageDir: string;
62
+ /**
63
+ * Absolute path to the importable entry file (from `exports["."]` →
64
+ * `main` → `pi.extensions[0]` → `index.js` → `index.ts`), or `null`
65
+ * when no candidate exists on disk.
66
+ */
67
+ entryPath: string | null;
68
+ /** Which scope matched. */
69
+ scope: "user" | "project";
70
+ /** Original `settings.json#packages[]` entry that produced the match. */
71
+ source: string;
72
+ /** Parsed `package.json#name`, or `null` when no readable package.json. */
73
+ packageJsonName: string | null;
74
+ }
75
+
76
+ export function resolvePiPackage(
77
+ spec: string,
78
+ opts: ResolvePiPackageOptions = {},
79
+ ): ResolvedPiPackage | null {
80
+ const agentDir = opts.agentDir ?? path.join(os.homedir(), ".pi", "agent");
81
+ const scope = opts.scope ?? "any";
82
+ const cwd = opts.cwd;
83
+ const npmRoot = opts.npmRoot ?? rootGlobalOr("");
84
+
85
+ // Project scope first (matches deepMergeSettings precedence).
86
+ if ((scope === "project" || scope === "any") && cwd) {
87
+ const hit = findInScope(spec, "project", { agentDir, cwd, npmRoot });
88
+ if (hit) return hit;
89
+ }
90
+ if (scope === "user" || scope === "any") {
91
+ const hit = findInScope(spec, "user", { agentDir, cwd, npmRoot });
92
+ if (hit) return hit;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ export function resolvePiPackageEntry(
98
+ spec: string,
99
+ opts: ResolvePiPackageOptions = {},
100
+ ): string | null {
101
+ return resolvePiPackage(spec, opts)?.entryPath ?? null;
102
+ }
103
+
104
+ /**
105
+ * List every resolved pi package across the requested scopes. Iterates
106
+ * `settings.packages[]` in project then user scope (matching
107
+ * `resolvePiPackage`'s precedence) and yields a `ResolvedPiPackage` for
108
+ * every entry whose computed install path exists on disk.
109
+ *
110
+ * Unlike `resolvePiPackage`, this performs no name match — callers that
111
+ * need to find any package satisfying a per-directory predicate (e.g.
112
+ * "contains `bridge.ts`") consume the list directly.
113
+ *
114
+ * Order: project-scope hits first, then user-scope. Duplicates (same
115
+ * absolute packageDir) are de-duplicated keeping the first occurrence.
116
+ *
117
+ * Added by change: fix-electron-cold-launch-probe-cascade (Bug A).
118
+ * Used by `launch-source.ts::probePiExtension` to iterate the actual
119
+ * `packages[]` schema instead of the non-existent `extensions[]`.
120
+ */
121
+ export function listPiPackages(opts: ResolvePiPackageOptions = {}): ResolvedPiPackage[] {
122
+ const agentDir = opts.agentDir ?? path.join(os.homedir(), ".pi", "agent");
123
+ const scope = opts.scope ?? "any";
124
+ const cwd = opts.cwd;
125
+ const npmRoot = opts.npmRoot ?? rootGlobalOr("");
126
+
127
+ const out: ResolvedPiPackage[] = [];
128
+ const seen = new Set<string>();
129
+ const collect = (s: "user" | "project") => {
130
+ for (const r of iterateInScope(s, { agentDir, cwd, npmRoot })) {
131
+ if (seen.has(r.packageDir)) continue;
132
+ seen.add(r.packageDir);
133
+ out.push(r);
134
+ }
135
+ };
136
+ if ((scope === "project" || scope === "any") && cwd) collect("project");
137
+ if (scope === "user" || scope === "any") collect("user");
138
+ return out;
139
+ }
140
+
141
+ // ── Internals ───────────────────────────────────────────────────────
142
+
143
+ interface ScopeContext {
144
+ agentDir: string;
145
+ cwd?: string;
146
+ npmRoot: string;
147
+ }
148
+
149
+ interface ParsedSource {
150
+ kind: "npm" | "git" | "local-abs" | "local-rel";
151
+ /** For npm: package name (without version). For git: cleaned URL path. For local: raw path. */
152
+ value: string;
153
+ /** Original entry as it appeared in settings.json. */
154
+ original: string;
155
+ }
156
+
157
+ function findInScope(
158
+ spec: string,
159
+ scope: "user" | "project",
160
+ ctx: ScopeContext,
161
+ ): ResolvedPiPackage | null {
162
+ for (const r of iterateInScope(scope, ctx)) {
163
+ if (r.packageJsonName === spec) return r;
164
+ }
165
+ return null;
166
+ }
167
+
168
+ /**
169
+ * Shared iteration core used by both `findInScope` (name-matched lookup)
170
+ * and `listPiPackages` (no name filter). Yields every entry whose computed
171
+ * install path exists on disk; reads `package.json` lazily.
172
+ */
173
+ function* iterateInScope(
174
+ scope: "user" | "project",
175
+ ctx: ScopeContext,
176
+ ): Generator<ResolvedPiPackage> {
177
+ const settingsPath =
178
+ scope === "user"
179
+ ? path.join(ctx.agentDir, "settings.json")
180
+ : ctx.cwd
181
+ ? path.join(ctx.cwd, ".pi", "settings.json")
182
+ : null;
183
+ if (!settingsPath) return;
184
+
185
+ const packages = readSettingsPackages(settingsPath);
186
+ if (packages.length === 0) return;
187
+
188
+ const settingsDir = path.dirname(settingsPath);
189
+ for (const entry of packages) {
190
+ const parsed = parseSource(entry);
191
+ if (!parsed) continue;
192
+ const packageDir = computeInstallPath(parsed, scope, ctx, settingsDir);
193
+ if (!packageDir || !fs.existsSync(packageDir)) continue;
194
+
195
+ const pkgJson = readPackageJson(packageDir);
196
+ const name = pkgJson?.name ?? null;
197
+ const entryPath = resolveEntryPath(packageDir, pkgJson ?? {});
198
+ yield {
199
+ packageDir,
200
+ entryPath,
201
+ scope,
202
+ source: parsed.original,
203
+ packageJsonName: name,
204
+ };
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Parse a `settings.json#packages[]` entry. Mirrors
210
+ * `packages/server/src/pi-resource-scanner.ts::resolvePackagePath`
211
+ * parsing arms and pi-coding-agent `package-manager.js::parseSource`.
212
+ */
213
+ function parseSource(entry: unknown): ParsedSource | null {
214
+ // Pi accepts both string entries and `{source: "..."}` objects.
215
+ const original =
216
+ typeof entry === "string"
217
+ ? entry
218
+ : typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string"
219
+ ? (entry as { source: string }).source
220
+ : "";
221
+ if (!original) return null;
222
+
223
+ if (original.startsWith("npm:")) {
224
+ const pkgName = original.slice(4).replace(/@[^/]*$/, "");
225
+ return { kind: "npm", value: pkgName, original };
226
+ }
227
+ if (
228
+ original.startsWith("git:") ||
229
+ original.startsWith("git@") ||
230
+ original.startsWith("https://") ||
231
+ original.startsWith("http://") ||
232
+ original.startsWith("ssh://") ||
233
+ original.startsWith("github:")
234
+ ) {
235
+ let url = original.replace(/^git:/, "");
236
+ url = url.replace(/^github:/, "github.com/");
237
+ url = url.replace(/^git@([^:]+):/, "$1/");
238
+ url = url.replace(/^(https?|ssh|git):\/\//, "");
239
+ url = url.replace(/^[^@]+@/, "");
240
+ url = url.replace(/\.git$/, "").replace(/@[^/]*$/, "");
241
+ return { kind: "git", value: url, original };
242
+ }
243
+ if (path.isAbsolute(original)) {
244
+ return { kind: "local-abs", value: original, original };
245
+ }
246
+ return { kind: "local-rel", value: original, original };
247
+ }
248
+
249
+ /**
250
+ * Compute the absolute install directory for a parsed entry. Mirrors
251
+ * pi-coding-agent's own arithmetic in `dist/core/package-manager.js`
252
+ * `getNpmInstallPath` / `getGitInstallPath` / `resolvePathFromBase`.
253
+ */
254
+ function computeInstallPath(
255
+ parsed: ParsedSource,
256
+ scope: "user" | "project",
257
+ ctx: ScopeContext,
258
+ settingsDir: string,
259
+ ): string | null {
260
+ switch (parsed.kind) {
261
+ case "npm": {
262
+ if (scope === "project") {
263
+ if (!ctx.cwd) return null;
264
+ return path.join(ctx.cwd, ".pi", "npm", "node_modules", parsed.value);
265
+ }
266
+ if (!ctx.npmRoot) return null;
267
+ return path.join(ctx.npmRoot, parsed.value);
268
+ }
269
+ case "git": {
270
+ if (scope === "project") {
271
+ if (!ctx.cwd) return null;
272
+ return path.join(ctx.cwd, ".pi", "git", parsed.value);
273
+ }
274
+ return path.join(ctx.agentDir, "git", parsed.value);
275
+ }
276
+ case "local-abs":
277
+ return parsed.value;
278
+ case "local-rel":
279
+ return path.resolve(settingsDir, parsed.value);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Read `<settingsPath>`'s `packages[]` array. Returns `[]` on missing
285
+ * file, parse error, or missing `packages` field. Never throws.
286
+ */
287
+ function readSettingsPackages(settingsPath: string): unknown[] {
288
+ try {
289
+ if (!fs.existsSync(settingsPath)) return [];
290
+ const raw = fs.readFileSync(settingsPath, "utf-8");
291
+ const parsed = JSON.parse(raw);
292
+ const pkgs = (parsed && typeof parsed === "object" ? (parsed as { packages?: unknown }).packages : undefined);
293
+ return Array.isArray(pkgs) ? pkgs : [];
294
+ } catch (err) {
295
+ console.warn(
296
+ `[pi-package-resolver] failed to read ${settingsPath}: ${(err as Error).message}`,
297
+ );
298
+ return [];
299
+ }
300
+ }
301
+
302
+ interface PackageJsonShape {
303
+ name?: string;
304
+ main?: string;
305
+ exports?: unknown;
306
+ pi?: { extensions?: unknown };
307
+ }
308
+
309
+ function readPackageJson(packageDir: string): PackageJsonShape | null {
310
+ const pkgJsonPath = path.join(packageDir, "package.json");
311
+ try {
312
+ if (!fs.existsSync(pkgJsonPath)) return null;
313
+ const raw = fs.readFileSync(pkgJsonPath, "utf-8");
314
+ return JSON.parse(raw) as PackageJsonShape;
315
+ } catch (err) {
316
+ console.warn(
317
+ `[pi-package-resolver] failed to parse ${pkgJsonPath}: ${(err as Error).message}`,
318
+ );
319
+ return null;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Entry-point resolution priority:
325
+ * 1. package.json#exports["."] (string, or `{import|default|node}`)
326
+ * 2. package.json#main
327
+ * 3. package.json#pi.extensions[0]
328
+ * 4. <packageDir>/index.js
329
+ * 5. <packageDir>/index.ts
330
+ *
331
+ * Each candidate is existence-checked before being returned. Returns
332
+ * `null` when no candidate resolves to an existing file.
333
+ */
334
+ function resolveEntryPath(packageDir: string, pkg: PackageJsonShape): string | null {
335
+ // 1. exports["."]
336
+ const exportsDot = extractExportsDot(pkg.exports);
337
+ if (typeof exportsDot === "string") {
338
+ const candidate = path.resolve(packageDir, exportsDot);
339
+ if (fs.existsSync(candidate)) return candidate;
340
+ }
341
+ // 2. main
342
+ if (typeof pkg.main === "string" && pkg.main.length > 0) {
343
+ const candidate = path.resolve(packageDir, pkg.main);
344
+ if (fs.existsSync(candidate)) return candidate;
345
+ }
346
+ // 3. pi.extensions[0]
347
+ if (pkg.pi && Array.isArray(pkg.pi.extensions) && pkg.pi.extensions.length > 0) {
348
+ const first = pkg.pi.extensions[0];
349
+ if (typeof first === "string") {
350
+ const candidate = path.resolve(packageDir, first);
351
+ if (fs.existsSync(candidate)) return candidate;
352
+ }
353
+ }
354
+ // 4. index.js
355
+ const idxJs = path.join(packageDir, "index.js");
356
+ if (fs.existsSync(idxJs)) return idxJs;
357
+ // 5. index.ts
358
+ const idxTs = path.join(packageDir, "index.ts");
359
+ if (fs.existsSync(idxTs)) return idxTs;
360
+
361
+ return null;
362
+ }
363
+
364
+ /**
365
+ * Extract the "." subpath from `package.json#exports`. Supports:
366
+ * - "./entry.js" (string form)
367
+ * - { ".": "./entry.js" } (path-only object)
368
+ * - { ".": { "import": "...", "default": "..." } } (conditional)
369
+ * Returns the first matching string from `import` → `default` → `node`.
370
+ * Returns `null` for unsupported shapes.
371
+ */
372
+ function extractExportsDot(exportsField: unknown): string | null {
373
+ if (typeof exportsField === "string") {
374
+ return exportsField; // bare exports applies to "." implicitly
375
+ }
376
+ if (!exportsField || typeof exportsField !== "object") return null;
377
+ const root = exportsField as Record<string, unknown>;
378
+ const dot = root["."];
379
+ if (typeof dot === "string") return dot;
380
+ if (dot && typeof dot === "object") {
381
+ const cond = dot as Record<string, unknown>;
382
+ for (const key of ["import", "default", "node"]) {
383
+ const v = cond[key];
384
+ if (typeof v === "string") return v;
385
+ }
386
+ }
387
+ return null;
388
+ }
@@ -4,6 +4,7 @@
4
4
  * with a single configurable resolver.
5
5
  */
6
6
  import { execSync, spawnSync, buildSafeArgv } from "./exec.js";
7
+ import { ensureWindowsSystemPath } from "./ensure-windows-path.js";
7
8
  import { existsSync, realpathSync } from "node:fs";
8
9
  import { createRequire } from "node:module";
9
10
  import { pathToFileURL } from "node:url";
@@ -442,8 +443,29 @@ export class ToolResolver {
442
443
  /**
443
444
  * Build a spawn environment with managed bin, node bin, extra dirs,
444
445
  * and common user bin dirs prepended to PATH.
446
+ *
447
+ * On Windows, additionally guarantees canonical system directories
448
+ * (System32, Wbem, PowerShell, WindowsApps) are present via
449
+ * `ensureWindowsSystemPath`. See change:
450
+ * fix-windows-path-system32-missing.
445
451
  */
446
- buildSpawnEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
452
+ buildSpawnEnv(
453
+ base: NodeJS.ProcessEnv = process.env,
454
+ opts: { platform?: NodeJS.Platform; exists?: (p: string) => boolean } = {},
455
+ ): NodeJS.ProcessEnv {
456
+ // Strip Electron-specific vars so spawned child processes (pi sessions,
457
+ // npm installs) don't accidentally run as Electron-node mode.
458
+ const ELECTRON_VARS_TO_STRIP = new Set([
459
+ "ELECTRON_RUN_AS_NODE",
460
+ "ELECTRON_DEFAULT_ERROR_MODE",
461
+ "ELECTRON_ENABLE_STACK_DUMPING",
462
+ ]);
463
+ const strippedBase: NodeJS.ProcessEnv = {};
464
+ for (const [k, v] of Object.entries(base)) {
465
+ if (!ELECTRON_VARS_TO_STRIP.has(k)) strippedBase[k] = v;
466
+ }
467
+ base = strippedBase;
468
+
447
469
  const currentPath = base.PATH || "";
448
470
  const parts: string[] = [];
449
471
 
@@ -474,8 +496,10 @@ export class ToolResolver {
474
496
  }
475
497
  }
476
498
 
477
- if (parts.length === 0) return base;
478
- return { ...base, PATH: `${parts.join(path.delimiter)}${path.delimiter}${currentPath}` };
499
+ const out = parts.length === 0
500
+ ? base
501
+ : { ...base, PATH: `${parts.join(path.delimiter)}${path.delimiter}${currentPath}` };
502
+ return ensureWindowsSystemPath(out, opts);
479
503
  }
480
504
  }
481
505
 
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ensureWindowsSystemPath — restore canonical Windows system directories
3
+ * on `env.PATH` when they're absent.
4
+ *
5
+ * Electron / GUI-launched processes can inherit a stripped PATH that
6
+ * lacks `C:\Windows\System32`. Without System32 on PATH, every spawn
7
+ * of `where.exe`, `powershell.exe`, `tasklist.exe`, `taskkill.exe`,
8
+ * `wmic.exe` fails with ENOENT and cascades into Tools-panel red rows,
9
+ * empty process-scanner output, and broken bridge spawns.
10
+ *
11
+ * This helper is idempotent: calling it twice on the same env returns
12
+ * an env identical to a single call. On non-Windows hosts it is a
13
+ * no-op.
14
+ *
15
+ * See change: fix-windows-path-system32-missing.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import path from "node:path";
19
+
20
+ export interface EnsureWindowsSystemPathOpts {
21
+ /** Override `process.platform` for tests. */
22
+ platform?: NodeJS.Platform;
23
+ /** Override `fs.existsSync` for tests. */
24
+ exists?: (p: string) => boolean;
25
+ }
26
+
27
+ /**
28
+ * Prepend canonical Windows system directories to `env.PATH` when:
29
+ * - the host is Windows (or `opts.platform === "win32"`),
30
+ * - the directory physically exists on disk (via `opts.exists`),
31
+ * - the directory is not already substring-present in PATH
32
+ * (case-insensitive — Windows PATH semantics).
33
+ *
34
+ * Returns the input env unchanged on non-Windows hosts (no-op).
35
+ *
36
+ * Idempotence invariant:
37
+ * ensureWindowsSystemPath(ensureWindowsSystemPath(env)) deep-equals
38
+ * ensureWindowsSystemPath(env).
39
+ */
40
+ export function ensureWindowsSystemPath(
41
+ env: NodeJS.ProcessEnv,
42
+ opts: EnsureWindowsSystemPathOpts = {},
43
+ ): NodeJS.ProcessEnv {
44
+ const platform = opts.platform ?? process.platform;
45
+ if (platform !== "win32") return env;
46
+
47
+ const exists = opts.exists ?? existsSync;
48
+
49
+ const systemRoot = env.SYSTEMROOT || env.SystemRoot || env.systemroot || "C:\\Windows";
50
+ const localAppData = env.LOCALAPPDATA || env.LocalAppData || env.localappdata || "";
51
+
52
+ // Use win32 path semantics regardless of host OS — these paths only
53
+ // matter on Windows, and tests on POSIX hosts must still produce
54
+ // backslash-separated candidates.
55
+ const join = path.win32.join;
56
+
57
+ const candidates: string[] = [
58
+ join(systemRoot, "System32"),
59
+ systemRoot,
60
+ join(systemRoot, "System32", "Wbem"),
61
+ join(systemRoot, "System32", "WindowsPowerShell", "v1.0"),
62
+ join(systemRoot, "System32", "OpenSSH"),
63
+ ];
64
+ if (localAppData) {
65
+ candidates.push(join(localAppData, "Microsoft", "WindowsApps"));
66
+ }
67
+
68
+ const currentPath = env.PATH ?? "";
69
+ const currentLower = currentPath.toLowerCase();
70
+
71
+ // Track lower-cased additions so duplicates within the candidate
72
+ // list itself don't slip through (defensive — current list has none).
73
+ const addedLower = new Set<string>();
74
+ const toAdd: string[] = [];
75
+ for (const dir of candidates) {
76
+ if (!exists(dir)) continue;
77
+ const dirLower = dir.toLowerCase();
78
+ if (currentLower.includes(dirLower)) continue;
79
+ if (addedLower.has(dirLower)) continue;
80
+ addedLower.add(dirLower);
81
+ toAdd.push(dir);
82
+ }
83
+
84
+ if (toAdd.length === 0) return env;
85
+
86
+ // Windows PATH delimiter is `;`. Hard-code it: host may be POSIX
87
+ // during tests (`path.delimiter === ":"`) but we're building a
88
+ // Windows-targeted PATH.
89
+ const DELIM = ";";
90
+ const newPath = currentPath
91
+ ? `${toAdd.join(DELIM)}${DELIM}${currentPath}`
92
+ : toAdd.join(DELIM);
93
+
94
+ return { ...env, PATH: newPath };
95
+ }
@@ -21,11 +21,13 @@
21
21
  */
22
22
  import {
23
23
  execSync as nodeExecSync,
24
+ execFileSync as nodeExecFileSync,
24
25
  exec as nodeExec,
25
26
  execFile as nodeExecFile,
26
27
  spawnSync as nodeSpawnSync,
27
28
  spawn as nodeSpawn,
28
29
  type ExecSyncOptions,
30
+ type ExecFileSyncOptions,
29
31
  type ExecOptions,
30
32
  type ExecFileOptions,
31
33
  type SpawnSyncOptions,
@@ -127,6 +129,25 @@ export function execSync(
127
129
  return nodeExecSync(command, withHide(options));
128
130
  }
129
131
 
132
+ /** Wrapped `execFileSync`. Always `windowsHide: true` unless overridden. */
133
+ export function execFileSync(
134
+ file: string,
135
+ args: readonly string[] | undefined,
136
+ options: ExecFileSyncOptions & { encoding: BufferEncoding },
137
+ ): string;
138
+ export function execFileSync(
139
+ file: string,
140
+ args?: readonly string[],
141
+ options?: ExecFileSyncOptions,
142
+ ): Buffer | string;
143
+ export function execFileSync(
144
+ file: string,
145
+ args?: readonly string[],
146
+ options?: ExecFileSyncOptions,
147
+ ): Buffer | string {
148
+ return nodeExecFileSync(file, args ?? [], withHide(options));
149
+ }
150
+
130
151
  /** Wrapped `spawnSync`. Always `windowsHide: true` unless overridden. */
131
152
  export function spawnSync<T extends string | Buffer = Buffer>(
132
153
  command: string,
@@ -211,6 +232,7 @@ export const execFileAsync = promisify(execFile) as unknown as (
211
232
 
212
233
  export type {
213
234
  ExecSyncOptions,
235
+ ExecFileSyncOptions,
214
236
  ExecOptions,
215
237
  ExecFileOptions,
216
238
  SpawnSyncOptions,