@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1

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 (181) hide show
  1. package/AGENTS.md +339 -190
  2. package/README.md +50 -7
  3. package/docs/architecture.md +238 -23
  4. package/package.json +23 -13
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
  13. package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
  14. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  15. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  16. package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
  17. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  19. package/packages/extension/src/ask-user-tool.ts +1 -1
  20. package/packages/extension/src/bridge-context.ts +1 -1
  21. package/packages/extension/src/bridge.ts +169 -4
  22. package/packages/extension/src/command-handler.ts +65 -2
  23. package/packages/extension/src/flow-event-wiring.ts +1 -1
  24. package/packages/extension/src/markdown-image-inliner.ts +268 -0
  25. package/packages/extension/src/multiselect-list.ts +1 -1
  26. package/packages/extension/src/pi-env.d.ts +16 -9
  27. package/packages/extension/src/prompt-expander.ts +50 -2
  28. package/packages/extension/src/provider-register.ts +132 -8
  29. package/packages/extension/src/retry-tracker.ts +123 -0
  30. package/packages/extension/src/server-launcher.ts +18 -1
  31. package/packages/extension/src/session-sync.ts +15 -1
  32. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  33. package/packages/server/package.json +6 -6
  34. package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
  35. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
  36. package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
  37. package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
  38. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  39. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  40. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  41. package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
  42. package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
  43. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  44. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  45. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  46. package/packages/server/src/__tests__/directory-service.test.ts +1 -1
  47. package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
  48. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
  49. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  50. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  51. package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
  52. package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
  53. package/packages/server/src/__tests__/health-shape.test.ts +43 -0
  54. package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
  55. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  56. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
  57. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  58. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  59. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  60. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  61. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  62. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  63. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
  64. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  65. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  66. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  67. package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
  68. package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
  69. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  70. package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
  71. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
  72. package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
  74. package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
  75. package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
  76. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  77. package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
  78. package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
  79. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
  80. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  81. package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
  82. package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
  83. package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
  84. package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
  85. package/packages/server/src/bootstrap-install-from-list.ts +232 -0
  86. package/packages/server/src/bootstrap-state.ts +18 -0
  87. package/packages/server/src/browser-gateway.ts +70 -24
  88. package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
  89. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  90. package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
  91. package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
  92. package/packages/server/src/changelog-fs.ts +167 -0
  93. package/packages/server/src/changelog-parser.ts +321 -0
  94. package/packages/server/src/changelog-remote.ts +134 -0
  95. package/packages/server/src/cli.ts +23 -2
  96. package/packages/server/src/directory-service.ts +31 -0
  97. package/packages/server/src/event-wiring.ts +105 -5
  98. package/packages/server/src/headless-pid-registry.ts +54 -5
  99. package/packages/server/src/home-lock.d.ts +124 -0
  100. package/packages/server/src/home-lock.js +330 -0
  101. package/packages/server/src/home-lock.js.map +1 -0
  102. package/packages/server/src/idle-timer.ts +15 -1
  103. package/packages/server/src/pending-client-correlations.ts +73 -0
  104. package/packages/server/src/pending-fork-registry.ts +24 -12
  105. package/packages/server/src/pi-core-checker.ts +77 -17
  106. package/packages/server/src/pi-core-updater.ts +81 -15
  107. package/packages/server/src/pi-dev-version-check.ts +145 -0
  108. package/packages/server/src/pi-gateway.ts +10 -0
  109. package/packages/server/src/pi-version-skew.ts +12 -4
  110. package/packages/server/src/process-manager.ts +115 -21
  111. package/packages/server/src/provider-auth-handlers.ts +9 -0
  112. package/packages/server/src/provider-auth-storage.ts +83 -51
  113. package/packages/server/src/provider-catalogue-cache.ts +47 -0
  114. package/packages/server/src/routes/doctor-routes.ts +140 -0
  115. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  116. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  117. package/packages/server/src/routes/provider-auth-routes.ts +14 -1
  118. package/packages/server/src/routes/provider-routes.ts +4 -4
  119. package/packages/server/src/routes/system-routes.ts +38 -1
  120. package/packages/server/src/server.ts +85 -66
  121. package/packages/server/src/session-api.ts +54 -3
  122. package/packages/server/src/session-bootstrap.ts +27 -12
  123. package/packages/server/src/session-discovery.ts +11 -4
  124. package/packages/server/src/session-file-reader.ts +1 -1
  125. package/packages/server/src/session-scanner.ts +4 -2
  126. package/packages/server/src/spawn-failure-log.ts +130 -0
  127. package/packages/server/src/spawn-preflight.ts +82 -0
  128. package/packages/server/src/spawn-register-watchdog.ts +291 -0
  129. package/packages/server/src/spawn-token.ts +20 -0
  130. package/packages/server/src/terminal-manager.ts +12 -1
  131. package/packages/shared/package.json +1 -1
  132. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
  133. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  134. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  135. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  136. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  137. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  138. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
  139. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  140. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
  141. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
  142. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  143. package/packages/shared/src/__tests__/config.test.ts +48 -0
  144. package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
  145. package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
  146. package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
  147. package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
  148. package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
  149. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
  150. package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
  151. package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
  152. package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
  153. package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
  154. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
  155. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
  156. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  157. package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
  158. package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
  159. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  160. package/packages/shared/src/bootstrap-install.ts +197 -3
  161. package/packages/shared/src/browser-protocol.ts +155 -1
  162. package/packages/shared/src/changelog-types.ts +111 -0
  163. package/packages/shared/src/config.ts +15 -0
  164. package/packages/shared/src/dashboard-starter.ts +33 -0
  165. package/packages/shared/src/doctor-core.ts +821 -0
  166. package/packages/shared/src/index.ts +9 -0
  167. package/packages/shared/src/installable-list.ts +152 -0
  168. package/packages/shared/src/launch-source-flag.ts +14 -0
  169. package/packages/shared/src/launch-source-types.ts +18 -0
  170. package/packages/shared/src/openspec-activity-detector.ts +25 -7
  171. package/packages/shared/src/platform/detached-spawn.ts +13 -2
  172. package/packages/shared/src/platform/managed-node-path.ts +77 -0
  173. package/packages/shared/src/platform/node-spawn.ts +29 -21
  174. package/packages/shared/src/protocol.ts +54 -2
  175. package/packages/shared/src/resolve-jiti.ts +62 -9
  176. package/packages/shared/src/rest-api.ts +4 -0
  177. package/packages/shared/src/skill-block-parser.ts +115 -0
  178. package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
  179. package/packages/shared/src/tool-registry/definitions.ts +33 -8
  180. package/packages/shared/src/tool-registry/strategies.ts +42 -0
  181. package/packages/shared/src/types.ts +64 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Skill block parser & builder.
3
+ *
4
+ * Pi's `_expandSkillCommand` (in `@earendil-works/pi-coding-agent`) wraps skill
5
+ * expansions in a `<skill name="..." location="...">…</skill>\n\nargs` envelope.
6
+ * The dashboard's bridge expander (`packages/extension/src/prompt-expander.ts`)
7
+ * aligns to the same byte format. This module is the single source of truth for
8
+ * both producing and recovering that envelope.
9
+ *
10
+ * See change: render-skill-invocations-collapsibly.
11
+ */
12
+
13
+ export interface SkillBlock {
14
+ /** Bare skill name (no `skill:` prefix), e.g. `"openspec-explore"`. */
15
+ name: string;
16
+ /** Absolute path to `SKILL.md`. */
17
+ location: string;
18
+ /**
19
+ * Skill body with the `References are relative to <baseDir>.\n\n` preamble
20
+ * stripped — what users see in the card. The preamble is bridge-internal
21
+ * orientation for the LLM and is not interesting to users.
22
+ *
23
+ * Pi's own `parseSkillBlock` returns the unstripped form (calls it `content`).
24
+ * We strip here so the renderer doesn't have to. If the preamble shape ever
25
+ * changes upstream and stripping fails, `body` falls back to the captured
26
+ * content verbatim.
27
+ */
28
+ body: string;
29
+ /** User text after the skill name. `undefined` when no args were typed. */
30
+ args: string | undefined;
31
+ /** Slash-command form: `"/skill:" + name + (args ? " " + args : "")`. */
32
+ condensed: string;
33
+ }
34
+
35
+ /**
36
+ * Anchored, non-greedy match of a wrapped skill block.
37
+ *
38
+ * Why anchored: prevents a literal `<skill>` substring elsewhere in user text
39
+ * from matching. Why non-greedy + optional trailing args at end-of-string:
40
+ * forces the regex engine to extend the body to the last valid
41
+ * `\n</skill>(\n\nargs)?$` boundary, so SKILL.md bodies that document the
42
+ * `<skill>` tag (e.g. include the literal text in code samples) do not
43
+ * terminate the match prematurely.
44
+ */
45
+ const SKILL_BLOCK_RE =
46
+ /^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/;
47
+
48
+ /**
49
+ * Parse a skill block from message text. Returns `null` when the input is not
50
+ * a well-formed skill envelope.
51
+ */
52
+ /**
53
+ * Strip the `References are relative to <baseDir>.\n\n` preamble from a captured
54
+ * `<skill>` content block. Returns the stripped body, or the input unchanged if
55
+ * the preamble shape doesn't match.
56
+ */
57
+ function stripReferencesPreamble(content: string): string {
58
+ const m = content.match(/^References are relative to [^\n]+\.\n\n([\s\S]*)$/);
59
+ return m ? m[1] : content;
60
+ }
61
+
62
+ export function parseSkillBlock(text: string): SkillBlock | null {
63
+ const m = text.match(SKILL_BLOCK_RE);
64
+ if (!m) return null;
65
+ const name = m[1];
66
+ const location = m[2];
67
+ const body = stripReferencesPreamble(m[3]);
68
+ const args = m[4];
69
+ const condensed = `/skill:${name}${args ? " " + args : ""}`;
70
+ return { name, location, body, args, condensed };
71
+ }
72
+
73
+ export interface BuildSkillBlockArgs {
74
+ /** Bare skill name (no `skill:` prefix). */
75
+ name: string;
76
+ /** Absolute path to `SKILL.md`. */
77
+ filePath: string;
78
+ /** Skill base directory — `dirname(filePath)`. */
79
+ baseDir: string;
80
+ /** Skill body, frontmatter already stripped. Should be `.trim()`-ed. */
81
+ body: string;
82
+ /** Optional user-typed args appended after the closing tag. */
83
+ userArgs?: string;
84
+ }
85
+
86
+ /**
87
+ * Build a skill block in the exact byte format pi's `_expandSkillCommand`
88
+ * produces. The output is byte-identical to pi's output for the same inputs;
89
+ * `parseSkillBlock(buildSkillBlock(x))` round-trips for `name`, `body`, `args`.
90
+ */
91
+ export function buildSkillBlock(input: BuildSkillBlockArgs): string {
92
+ const wrapper =
93
+ `<skill name="${input.name}" location="${input.filePath}">\n` +
94
+ `References are relative to ${input.baseDir}.\n\n` +
95
+ `${input.body}\n` +
96
+ `</skill>`;
97
+ return input.userArgs ? `${wrapper}\n\n${input.userArgs}` : wrapper;
98
+ }
99
+
100
+ /**
101
+ * Condense a user-message content string for `firstMessage` / display purposes.
102
+ *
103
+ * If `text` parses as a `<skill>` envelope, returns the slash-command form
104
+ * (`/skill:name args`) truncated to `maxLen` chars. Otherwise returns
105
+ * `text.slice(0, maxLen)`. Used by session-scanner / session-discovery so the
106
+ * 200-char `firstMessage` shows the recognisable slash form instead of the
107
+ * front of an opaque wrapper.
108
+ *
109
+ * See change: render-skill-invocations-collapsibly.
110
+ */
111
+ export function condenseForFirstMessage(text: string, maxLen: number): string {
112
+ const block = parseSkillBlock(text);
113
+ if (block) return block.condensed.slice(0, maxLen);
114
+ return text.slice(0, maxLen);
115
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Chain-order tests for the managed-Node runtime strategy.
3
+ *
4
+ * After change `embed-managed-node-runtime`, the `node` and `npm`
5
+ * strategy chains gain a `managedRuntimeStrategy` between
6
+ * `overrideStrategy` and the existing PATH/where lookup. These tests
7
+ * pin the precedence:
8
+ *
9
+ * 1. override (tool-overrides.json) — wins
10
+ * 2. managed runtime (<managedDir>/node/) — preferred over PATH
11
+ * 3. where / PATH — fallback
12
+ *
13
+ * `exists` is injected so no real filesystem is touched.
14
+ *
15
+ * See change: embed-managed-node-runtime (task 3.3).
16
+ */
17
+ import os from "node:os";
18
+ import path from "node:path";
19
+ import { describe, expect, it } from "vitest";
20
+ import {
21
+ ToolRegistry,
22
+ registerDefaultTools,
23
+ OverridesStore,
24
+ } from "../index.js";
25
+
26
+ function freshRegistry(opts: {
27
+ exists?: (p: string) => boolean;
28
+ which?: (name: string) => string | null;
29
+ overrides?: Record<string, string>;
30
+ platform?: NodeJS.Platform;
31
+ }) {
32
+ const store = new OverridesStore({
33
+ filePath: path.join(os.tmpdir(), `mr-test-${Math.random()}.json`),
34
+ warn: () => {},
35
+ });
36
+ for (const [k, v] of Object.entries(opts.overrides ?? {})) store.set(k, v);
37
+
38
+ const r = new ToolRegistry({
39
+ overrides: store,
40
+ platform: opts.platform ?? "linux",
41
+ });
42
+ registerDefaultTools(r, {
43
+ exists: opts.exists ?? (() => false),
44
+ which: opts.which ?? (() => null),
45
+ npmRootGlobal: () => "",
46
+ });
47
+ return r;
48
+ }
49
+
50
+ const HOME = os.homedir();
51
+ const MANAGED_NODE_UNIX = path.join(HOME, ".pi-dashboard", "node", "bin", "node");
52
+ const MANAGED_NPM_UNIX = path.join(HOME, ".pi-dashboard", "node", "bin", "npm");
53
+ const MANAGED_NODE_WIN = path.join(HOME, ".pi-dashboard", "node", "node.exe");
54
+ const MANAGED_NPM_WIN = path.join(HOME, ".pi-dashboard", "node", "npm.cmd");
55
+
56
+ describe("node: managed-runtime strategy precedence", () => {
57
+ it("managed runtime present → returned over PATH (Unix)", () => {
58
+ const r = freshRegistry({
59
+ platform: "linux",
60
+ exists: (p) => p === MANAGED_NODE_UNIX,
61
+ which: () => "/usr/bin/node",
62
+ });
63
+ const res = r.resolve("node");
64
+ expect(res.ok).toBe(true);
65
+ expect(res.path).toBe(MANAGED_NODE_UNIX);
66
+ expect(res.source).toBe("managed");
67
+ });
68
+
69
+ it("managed runtime present → returned over PATH (Windows)", () => {
70
+ const r = freshRegistry({
71
+ platform: "win32",
72
+ exists: (p) => p === MANAGED_NODE_WIN,
73
+ which: () => "C:\\node\\node.exe",
74
+ });
75
+ const res = r.resolve("node");
76
+ expect(res.ok).toBe(true);
77
+ expect(res.path).toBe(MANAGED_NODE_WIN);
78
+ expect(res.source).toBe("managed");
79
+ });
80
+
81
+ it("override wins over managed runtime", () => {
82
+ const custom = "/opt/custom/node";
83
+ const r = freshRegistry({
84
+ platform: "linux",
85
+ overrides: { node: custom },
86
+ exists: (p) => p === custom || p === MANAGED_NODE_UNIX,
87
+ });
88
+ const res = r.resolve("node");
89
+ expect(res.ok).toBe(true);
90
+ expect(res.path).toBe(custom);
91
+ expect(res.source).toBe("override");
92
+ });
93
+
94
+ it("both absent → falls through to PATH lookup", () => {
95
+ const r = freshRegistry({
96
+ platform: "linux",
97
+ exists: () => false,
98
+ which: (name) => (name === "node" ? "/usr/bin/node" : null),
99
+ });
100
+ const res = r.resolve("node");
101
+ expect(res.ok).toBe(true);
102
+ expect(res.path).toBe("/usr/bin/node");
103
+ expect(res.source).toBe("system");
104
+ });
105
+
106
+ it("nothing present → ok:false with diagnostic trail", () => {
107
+ const r = freshRegistry({ platform: "linux", exists: () => false, which: () => null });
108
+ const res = r.resolve("node");
109
+ expect(res.ok).toBe(false);
110
+ const trailNames = res.tried.map((t) => t.strategy);
111
+ expect(trailNames).toContain("override");
112
+ expect(trailNames).toContain("managed");
113
+ expect(trailNames).toContain("where");
114
+ });
115
+ });
116
+
117
+ describe("npm: managed-runtime strategy precedence", () => {
118
+ it("managed npm present → returned over PATH (Unix)", () => {
119
+ const r = freshRegistry({
120
+ platform: "linux",
121
+ exists: (p) => p === MANAGED_NPM_UNIX,
122
+ which: () => "/usr/bin/npm",
123
+ });
124
+ const res = r.resolve("npm");
125
+ expect(res.ok).toBe(true);
126
+ expect(res.path).toBe(MANAGED_NPM_UNIX);
127
+ expect(res.source).toBe("managed");
128
+ });
129
+
130
+ it("managed npm present → returned over PATH (Windows)", () => {
131
+ const r = freshRegistry({
132
+ platform: "win32",
133
+ exists: (p) => p === MANAGED_NPM_WIN,
134
+ which: () => "C:\\node\\npm.cmd",
135
+ });
136
+ const res = r.resolve("npm");
137
+ expect(res.ok).toBe(true);
138
+ expect(res.path).toBe(MANAGED_NPM_WIN);
139
+ expect(res.source).toBe("managed");
140
+ });
141
+
142
+ it("override wins over managed npm", () => {
143
+ const custom = "/opt/custom/npm";
144
+ const r = freshRegistry({
145
+ platform: "linux",
146
+ overrides: { npm: custom },
147
+ exists: (p) => p === custom || p === MANAGED_NPM_UNIX,
148
+ });
149
+ const res = r.resolve("npm");
150
+ expect(res.ok).toBe(true);
151
+ expect(res.path).toBe(custom);
152
+ expect(res.source).toBe("override");
153
+ });
154
+
155
+ it("npm: managed absent + PATH present → falls through (Unix)", () => {
156
+ const r = freshRegistry({
157
+ platform: "linux",
158
+ exists: () => false,
159
+ which: (name) => (name === "npm" ? "/usr/bin/npm" : null),
160
+ });
161
+ const res = r.resolve("npm");
162
+ expect(res.ok).toBe(true);
163
+ expect(res.path).toBe("/usr/bin/npm");
164
+ expect(res.source).toBe("system");
165
+ });
166
+ });
@@ -18,6 +18,7 @@ import {
18
18
  bareImportStrategy,
19
19
  managedBinStrategy,
20
20
  managedModuleStrategy,
21
+ managedRuntimeStrategy,
21
22
  npmGlobalStrategy,
22
23
  overrideStrategy,
23
24
  whereStrategy,
@@ -39,21 +40,27 @@ function classify(strategyName: string): Source {
39
40
  // ── Binary definitions ──────────────────────────────────────────────────────
40
41
 
41
42
  function binaryDef(binaryName: string, deps?: StrategyDeps): ToolDefinition {
43
+ // The `node` binary gets the managed-Node runtime strategy prepended
44
+ // (after override) so the persistent <managedDir>/node/ install wins
45
+ // over PATH lookup. See change: embed-managed-node-runtime.
46
+ const isNode = binaryName === "node";
47
+ const strategies = [
48
+ overrideStrategy(binaryName, deps),
49
+ ...(isNode ? [managedRuntimeStrategy("node", deps)] : []),
50
+ managedBinStrategy(binaryName, deps),
51
+ whereStrategy(binaryName, deps),
52
+ ];
42
53
  return {
43
54
  name: binaryName,
44
55
  kind: "binary",
45
- strategies: [
46
- overrideStrategy(binaryName, deps),
47
- managedBinStrategy(binaryName, deps),
48
- whereStrategy(binaryName, deps),
49
- ],
56
+ strategies,
50
57
  classify,
51
58
  };
52
59
  }
53
60
 
54
61
  // ── Module definitions ──────────────────────────────────────────────────────
55
62
 
56
- /** Sibling probe for an aliased package name (pi: `@mariozechner/*` + `@oh-my-pi/*`). */
63
+ /** Sibling probe for an aliased package name (pi: `@earendil-works/*` + `@mariozechner/*`). */
57
64
  function moduleDefWithAliases(
58
65
  canonicalName: string,
59
66
  pkgNames: readonly string[],
@@ -181,7 +188,7 @@ const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, re
181
188
  * On Unix, the chain finds `pi` on PATH; argv = [pi].
182
189
  */
183
190
  function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
184
- const piPkgAliases = ["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"];
191
+ const piPkgAliases = ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"];
185
192
  const cliEntry = path.join("dist", "cli.js");
186
193
 
187
194
  const winStrategies = [
@@ -283,14 +290,20 @@ function npmExecutorDef(deps?: StrategyDeps): ToolDefinition {
283
290
  },
284
291
  };
285
292
 
293
+ // Managed-Node runtime: prefer <managedDir>/node/{npm.cmd,bin/npm}
294
+ // when the runtime is installed. See change: embed-managed-node-runtime.
295
+ const managedNpm = managedRuntimeStrategy("npm", deps);
296
+
286
297
  const winStrategies = [
287
298
  overrideStrategy("npm", deps),
299
+ managedNpm,
288
300
  npmCliBesideNodeStrategy,
289
301
  whereStrategy("npm", deps),
290
302
  ];
291
303
 
292
304
  const unixStrategies = [
293
305
  overrideStrategy("npm", deps),
306
+ managedNpm,
294
307
  whereStrategy("npm", deps),
295
308
  ];
296
309
 
@@ -400,7 +413,19 @@ export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps
400
413
  registry.register(
401
414
  moduleDefWithAliases(
402
415
  "pi-coding-agent",
403
- ["@mariozechner/pi-coding-agent", "@oh-my-pi/pi-coding-agent"],
416
+ ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"],
417
+ path.join("dist", "index.js"),
418
+ deps,
419
+ ),
420
+ );
421
+
422
+ // pi-ai module — used by model-proxy to call upstream LLM providers.
423
+ // Aliases: @earendil-works/pi-ai (preferred) + @mariozechner/pi-ai (legacy fallback).
424
+ // See change: add-dashboard-model-proxy.
425
+ registry.register(
426
+ moduleDefWithAliases(
427
+ "pi-ai",
428
+ ["@earendil-works/pi-ai", "@mariozechner/pi-ai"],
404
429
  path.join("dist", "index.js"),
405
430
  deps,
406
431
  ),
@@ -13,6 +13,7 @@ import { createRequire } from "node:module";
13
13
  import path from "node:path";
14
14
  import { ToolResolver, isAppImageSelfHit } from "../platform/binary-lookup.js";
15
15
  import { getManagedBin, getManagedDir } from "../managed-paths.js";
16
+ import { getManagedNodeBinDir } from "../platform/managed-node-path.js";
16
17
  import * as npm from "../platform/npm.js";
17
18
  import type { Strategy, StrategyCtx, StrategyResult } from "./types.js";
18
19
 
@@ -86,6 +87,47 @@ export function overrideStrategy(toolName: string, deps?: StrategyDeps): Strateg
86
87
  };
87
88
  }
88
89
 
90
+ /**
91
+ * Managed Node runtime: `<managedDir>/node/{node.exe,npm.cmd,npx.cmd}`
92
+ * on Windows or `<managedDir>/node/bin/{node,npm,npx}` on Unix.
93
+ *
94
+ * Lets `ToolRegistry.resolve("node")` and `resolve("npm")` prefer the
95
+ * persistent runtime under `~/.pi-dashboard/node/` (installed by
96
+ * `installManagedNode`) over the system PATH lookup, while still
97
+ * deferring to `tool-overrides.json`.
98
+ *
99
+ * Returns `null` when the managed Node runtime is not present, so the
100
+ * standalone-CLI / no-Electron-resources case falls through cleanly to
101
+ * the existing `where`/PATH strategy.
102
+ *
103
+ * See change: embed-managed-node-runtime (spec: managed-node-runtime,
104
+ * Requirement: ToolRegistry resolves managed runtime first).
105
+ */
106
+ export function managedRuntimeStrategy(
107
+ toolName: "node" | "npm" | "npx",
108
+ deps?: StrategyDeps,
109
+ ): Strategy {
110
+ const { exists } = d(deps);
111
+ return {
112
+ name: "managed",
113
+ run(ctx): StrategyResult {
114
+ const dir = getManagedNodeBinDir(ctx.env, ctx.platform);
115
+ const isWin = ctx.platform === "win32";
116
+ const fileName =
117
+ toolName === "node"
118
+ ? isWin
119
+ ? "node.exe"
120
+ : "node"
121
+ : isWin
122
+ ? `${toolName}.cmd`
123
+ : toolName;
124
+ const candidate = path.join(dir, fileName);
125
+ if (exists(candidate)) return { ok: true, path: candidate };
126
+ return { ok: false, reason: `missing: ${candidate}` };
127
+ },
128
+ };
129
+ }
130
+
89
131
  /**
90
132
  * Managed install: `~/.pi-dashboard/node_modules/.bin/<name>(.cmd)` for
91
133
  * binaries, or any explicit relative path under `MANAGED_DIR` for
@@ -125,6 +125,15 @@ export interface DashboardSession {
125
125
  * the entry. See change: add-extension-ui-decorations.
126
126
  */
127
127
  uiDecorators?: Record<string, DecoratorDescriptor>;
128
+ /**
129
+ * Per-session image asset registry, keyed by content hash. Populated by
130
+ * `asset_register` events emitted by the bridge for local-file images
131
+ * referenced as `![](path)` in assistant markdown. Survives event-buffer
132
+ * eviction (lives on Session, not in the rolling event buffer) so
133
+ * `pi-asset:<hash>` references in older messages still resolve.
134
+ * See change: chat-markdown-local-images-and-math.
135
+ */
136
+ assets?: Record<string, { data: string; mimeType: string }>;
128
137
  }
129
138
 
130
139
  // ── Extension UI System (Phase 1: management-modal slot) ───────────
@@ -333,6 +342,41 @@ export interface ModelInfo {
333
342
  id: string;
334
343
  }
335
344
 
345
+ /**
346
+ * Provider catalogue entry pushed by the bridge to the server.
347
+ * Derived from pi's live `ModelRegistry` (see provider-register.ts in
348
+ * the bridge). The server caches the most recently received catalogue
349
+ * and uses it as the source for `GET /api/provider-auth/status`.
350
+ * See change: replace-hardcoded-provider-lists.
351
+ */
352
+ export interface ProviderInfo {
353
+ /** pi-ai provider id (e.g. "anthropic", "deepseek", "google-vertex"). */
354
+ id: string;
355
+ /** From `modelRegistry.getProviderDisplayName(id)`; falls back to id. */
356
+ displayName: string;
357
+ /** True iff `authStorage.getOAuthProviders()` includes this id. */
358
+ hasOAuth: boolean;
359
+ /** True iff a credential is stored in auth.json. */
360
+ configured: boolean;
361
+ /** Where the credential is sourced from, when configured. */
362
+ source?: "stored" | "environment" | "fallback" | "runtime";
363
+ /** First env var name pi-ai consults for this provider, when applicable. */
364
+ envVar?: string;
365
+ /** True when configured via ambient credential chain (AWS profile / GCP ADC). */
366
+ ambient?: boolean;
367
+ /** Expiry timestamp for OAuth credentials. */
368
+ expires?: number;
369
+ /**
370
+ * True when this provider was registered by the dashboard itself via
371
+ * `pi.registerProvider()` from `~/.pi/agent/providers.json` (a "custom"
372
+ * provider managed by the LLM Providers settings section). Consumers
373
+ * use this to suppress API-key auth rows for custom providers — their
374
+ * keys are managed elsewhere. OAuth rows are still emitted because a
375
+ * custom OAuth provider needs its login button.
376
+ */
377
+ custom?: boolean;
378
+ }
379
+
336
380
  /** Role assignment info (from pi-flows role-manager) */
337
381
  export interface RoleInfo {
338
382
  roles: Record<string, string>;
@@ -383,6 +427,19 @@ export function deriveChangeState(change: OpenSpecChange): ChangeState {
383
427
  export interface OpenSpecData {
384
428
  initialized: boolean;
385
429
  changes: OpenSpecChange[];
430
+ /**
431
+ * Cold-boot signaling: server has detected `openspec/changes/` for this
432
+ * cwd but the slow poll has not yet produced authoritative data.
433
+ *
434
+ * Optional for backwards compatibility — absence means `false`. Composes
435
+ * with `initialized` to encode three states:
436
+ * - { initialized: false, pending: false } → no openspec dir
437
+ * - { initialized: false, pending: true } → dir exists, polling
438
+ * - { initialized: true, pending: ? } → poll complete
439
+ *
440
+ * See change: fix-cold-boot-openspec-protocol.
441
+ */
442
+ pending?: boolean;
386
443
  }
387
444
 
388
445
  /** OpenSpec workflow phase detected from tool calls */
@@ -592,4 +649,11 @@ export interface ApiResponse<T = unknown> {
592
649
  success: boolean;
593
650
  data?: T;
594
651
  error?: string;
652
+ /**
653
+ * Optional structured failure-classifier code paired with `error`.
654
+ * Lets clients render specific UI for known failure modes
655
+ * (e.g., `"FORK_EMPTY_SESSION"`).
656
+ * See change: fix-fork-empty-session-silent-timeout.
657
+ */
658
+ code?: string;
595
659
  }