@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.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 (238) hide show
  1. package/AGENTS.md +64 -8
  2. package/README.md +308 -101
  3. package/docs/architecture.md +515 -16
  4. package/package.json +14 -7
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
  7. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
  8. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
  9. package/packages/extension/src/__tests__/git-info.test.ts +67 -55
  10. package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
  11. package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
  12. package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
  13. package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
  14. package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
  15. package/packages/extension/src/ask-user-tool.ts +289 -20
  16. package/packages/extension/src/bridge.ts +107 -6
  17. package/packages/extension/src/command-handler.ts +34 -39
  18. package/packages/extension/src/dev-build.ts +1 -1
  19. package/packages/extension/src/git-info.ts +9 -19
  20. package/packages/extension/src/pi-env.d.ts +1 -0
  21. package/packages/extension/src/process-scanner.ts +72 -38
  22. package/packages/extension/src/prompt-expander.ts +25 -4
  23. package/packages/extension/src/provider-register.ts +304 -16
  24. package/packages/extension/src/server-auto-start.ts +27 -1
  25. package/packages/extension/src/server-launcher.ts +71 -27
  26. package/packages/server/package.json +17 -2
  27. package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
  28. package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
  29. package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
  30. package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
  31. package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
  32. package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
  33. package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
  34. package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
  35. package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
  36. package/packages/server/src/__tests__/config-api.test.ts +68 -0
  37. package/packages/server/src/__tests__/cors.test.ts +34 -2
  38. package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
  39. package/packages/server/src/__tests__/directory-service.test.ts +234 -8
  40. package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
  41. package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
  42. package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
  43. package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
  44. package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
  45. package/packages/server/src/__tests__/extension-register.test.ts +3 -1
  46. package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
  47. package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
  48. package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
  49. package/packages/server/src/__tests__/git-operations.test.ts +9 -7
  50. package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
  51. package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
  52. package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
  53. package/packages/server/src/__tests__/home-lock.test.ts +308 -0
  54. package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
  55. package/packages/server/src/__tests__/node-guard.test.ts +85 -0
  56. package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
  57. package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
  58. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
  59. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
  60. package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
  61. package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
  62. package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
  63. package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
  64. package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
  65. package/packages/server/src/__tests__/process-manager.test.ts +45 -18
  66. package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
  67. package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
  68. package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
  69. package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
  70. package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
  71. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
  72. package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
  73. package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
  74. package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
  75. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
  76. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
  77. package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
  78. package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
  79. package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
  80. package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
  81. package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
  82. package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
  83. package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
  84. package/packages/server/src/__tests__/tunnel.test.ts +103 -6
  85. package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
  86. package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
  87. package/packages/server/src/bootstrap-queue.ts +130 -0
  88. package/packages/server/src/bootstrap-state.ts +131 -0
  89. package/packages/server/src/browse.ts +108 -9
  90. package/packages/server/src/browser-gateway.ts +16 -3
  91. package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
  92. package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
  93. package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
  94. package/packages/server/src/cli.ts +256 -32
  95. package/packages/server/src/config-api.ts +16 -0
  96. package/packages/server/src/directory-service.ts +270 -39
  97. package/packages/server/src/editor-detection.ts +12 -9
  98. package/packages/server/src/editor-manager.ts +39 -5
  99. package/packages/server/src/editor-pid-registry.ts +199 -0
  100. package/packages/server/src/editor-registry.ts +22 -25
  101. package/packages/server/src/fix-pty-permissions.ts +44 -0
  102. package/packages/server/src/git-operations.ts +1 -1
  103. package/packages/server/src/headless-pid-registry.ts +16 -20
  104. package/packages/server/src/home-lock-release.ts +72 -0
  105. package/packages/server/src/home-lock.ts +389 -0
  106. package/packages/server/src/node-guard.ts +52 -0
  107. package/packages/server/src/npm-search-proxy.ts +71 -0
  108. package/packages/server/src/openspec-tasks.ts +158 -0
  109. package/packages/server/src/package-manager-wrapper.ts +225 -34
  110. package/packages/server/src/pi-core-checker.ts +290 -0
  111. package/packages/server/src/pi-core-updater.ts +172 -0
  112. package/packages/server/src/pi-gateway.ts +7 -0
  113. package/packages/server/src/pi-resource-scanner.ts +5 -8
  114. package/packages/server/src/pi-version-skew.ts +196 -0
  115. package/packages/server/src/preferences-store.ts +17 -3
  116. package/packages/server/src/process-manager.ts +403 -222
  117. package/packages/server/src/provider-probe.ts +234 -0
  118. package/packages/server/src/restart-helper.ts +130 -0
  119. package/packages/server/src/routes/bootstrap-routes.ts +88 -0
  120. package/packages/server/src/routes/file-routes.ts +30 -3
  121. package/packages/server/src/routes/openspec-routes.ts +107 -1
  122. package/packages/server/src/routes/pi-core-routes.ts +140 -0
  123. package/packages/server/src/routes/provider-auth-routes.ts +12 -10
  124. package/packages/server/src/routes/provider-routes.ts +55 -2
  125. package/packages/server/src/routes/recommended-routes.ts +225 -0
  126. package/packages/server/src/routes/system-routes.ts +30 -34
  127. package/packages/server/src/routes/tool-routes.ts +153 -0
  128. package/packages/server/src/server-pid.ts +5 -9
  129. package/packages/server/src/server.ts +363 -26
  130. package/packages/server/src/session-api.ts +77 -8
  131. package/packages/server/src/session-bootstrap.ts +17 -3
  132. package/packages/server/src/session-diff.ts +21 -21
  133. package/packages/server/src/terminal-manager.ts +65 -20
  134. package/packages/server/src/test-env-guard.ts +26 -0
  135. package/packages/server/src/test-support/test-server.ts +63 -0
  136. package/packages/server/src/tunnel.ts +172 -34
  137. package/packages/shared/package.json +10 -3
  138. package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
  139. package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
  140. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
  141. package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
  142. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
  143. package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
  144. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
  145. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
  146. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
  147. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
  148. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
  149. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
  150. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
  151. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
  152. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
  153. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
  154. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
  155. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
  156. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
  157. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
  158. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
  159. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
  160. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
  161. package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
  162. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
  163. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
  164. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
  165. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
  166. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
  167. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
  168. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
  169. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
  170. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
  172. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
  173. package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
  174. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
  175. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
  176. package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
  177. package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
  178. package/packages/shared/src/__tests__/config.test.ts +59 -3
  179. package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
  180. package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
  181. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
  182. package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
  183. package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
  184. package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
  185. package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
  186. package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
  187. package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
  188. package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
  189. package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
  190. package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
  191. package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
  192. package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
  193. package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
  194. package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
  195. package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
  196. package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
  197. package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
  198. package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
  199. package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
  200. package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
  201. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
  202. package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
  203. package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
  204. package/packages/shared/src/bootstrap-install.ts +212 -0
  205. package/packages/shared/src/bridge-register.ts +87 -20
  206. package/packages/shared/src/browser-protocol.ts +93 -1
  207. package/packages/shared/src/config.ts +87 -15
  208. package/packages/shared/src/managed-paths.ts +31 -4
  209. package/packages/shared/src/openspec-poller.ts +71 -49
  210. package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
  211. package/packages/shared/src/platform/commands.ts +100 -0
  212. package/packages/shared/src/platform/detached-spawn.ts +305 -0
  213. package/packages/shared/src/platform/exec.ts +220 -0
  214. package/packages/shared/src/platform/git.ts +155 -0
  215. package/packages/shared/src/platform/index.ts +15 -0
  216. package/packages/shared/src/platform/npm.ts +162 -0
  217. package/packages/shared/src/platform/openspec.ts +91 -0
  218. package/packages/shared/src/platform/paths.ts +276 -0
  219. package/packages/shared/src/platform/process-identify.ts +126 -0
  220. package/packages/shared/src/platform/process-scan.ts +94 -0
  221. package/packages/shared/src/platform/process.ts +168 -0
  222. package/packages/shared/src/platform/runner.ts +369 -0
  223. package/packages/shared/src/platform/shell.ts +44 -0
  224. package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
  225. package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
  226. package/packages/shared/src/recommended-extensions.ts +196 -0
  227. package/packages/shared/src/resolve-jiti.ts +62 -3
  228. package/packages/shared/src/rest-api.ts +97 -0
  229. package/packages/shared/src/semaphore.ts +83 -0
  230. package/packages/shared/src/source-matching.ts +126 -0
  231. package/packages/shared/src/test-support/setup-home.ts +74 -0
  232. package/packages/shared/src/tool-registry/definitions.ts +342 -0
  233. package/packages/shared/src/tool-registry/index.ts +56 -0
  234. package/packages/shared/src/tool-registry/overrides.ts +118 -0
  235. package/packages/shared/src/tool-registry/registry.ts +262 -0
  236. package/packages/shared/src/tool-registry/strategies.ts +198 -0
  237. package/packages/shared/src/tool-registry/types.ts +180 -0
  238. package/packages/shared/src/types.ts +7 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Recommended pi extensions for pi-agent-dashboard.
3
+ *
4
+ * The dashboard has custom UI and wiring for a small set of pi extensions
5
+ * it was built to work with. This manifest enumerates them so the dashboard
6
+ * can surface installation status, offer one-click installs in the Packages
7
+ * tab, walk users through setup in the first-launch wizard, and warn when
8
+ * a `required` entry is missing.
9
+ *
10
+ * This list is intentionally curated (not auto-discovered from npm). Each
11
+ * entry lives and dies by explicit PR review — the dashboard team owns the
12
+ * decision of which extensions are promoted.
13
+ *
14
+ * Descriptions in `fallbackDescription` are shipped inline. At runtime the
15
+ * dashboard server optionally enriches them with live descriptions fetched
16
+ * from the npm registry or GitHub (see `/api/packages/recommended`).
17
+ */
18
+
19
+ /** Relative importance of a recommended extension. */
20
+ export type RecommendedExtensionStatus =
21
+ | "required" // dashboard features or provider paths break without it
22
+ | "strongly-suggested" // dashboard has UI that depends on this
23
+ | "optional"; // nice-to-have
24
+
25
+ /** Static manifest entry. Enriched at runtime via the recommended route. */
26
+ export interface RecommendedExtension {
27
+ /** Stable kebab-case identifier. Used for skip/persist state and IPC. */
28
+ id: string;
29
+
30
+ /**
31
+ * pi install source. Any form parseable by pi's DefaultPackageManager:
32
+ * - `npm:<name>`
33
+ * - `git:<host>/<path>`
34
+ * - `git@<host>:<path>.git`
35
+ * - `https://<host>/<path>.git`
36
+ * - local path
37
+ */
38
+ source: string;
39
+
40
+ /** Human-readable package name for the UI. */
41
+ displayName: string;
42
+
43
+ /**
44
+ * Fallback description. Used when npm/GitHub is unreachable. Kept
45
+ * short (one or two sentences).
46
+ */
47
+ fallbackDescription: string;
48
+
49
+ /** Relative importance. */
50
+ status: RecommendedExtensionStatus;
51
+
52
+ /** Which dashboard features light up when this is installed. */
53
+ unlocks: string[];
54
+
55
+ /** Tool names this extension registers (for diagnostics / UI hinting). */
56
+ toolsRegistered?: string[];
57
+
58
+ /**
59
+ * True when the extension self-wires into pi / dashboard without
60
+ * additional configuration — installing it is sufficient for it to
61
+ * start working.
62
+ */
63
+ autowired?: boolean;
64
+ }
65
+
66
+ /** Enriched manifest entry returned by GET /api/packages/recommended. */
67
+ export interface EnrichedRecommendedExtension extends RecommendedExtension {
68
+ /** Live description (falls back to `fallbackDescription` on fetch failure). */
69
+ description: string;
70
+ /** Current upstream version, if available. */
71
+ version?: string;
72
+ /**
73
+ * Install status by scope. `null` means not present on disk in any scope.
74
+ */
75
+ installed: { scope: "global" | "local" | null };
76
+ /** True iff the source is currently listed in `~/.pi/agent/settings.json` `packages[]`. */
77
+ activeInPi: boolean;
78
+ /** True iff a newer version is available upstream. */
79
+ updateAvailable: boolean;
80
+ }
81
+
82
+ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
83
+ {
84
+ id: "pi-anthropic-messages",
85
+ source: "https://github.com/BlackBeltTechnology/pi-anthropic-messages.git",
86
+ displayName: "pi-anthropic-messages",
87
+ fallbackDescription:
88
+ "Protocol bridge that makes pi's custom tools work with any " +
89
+ "anthropic-messages endpoint for Claude models (direct Anthropic " +
90
+ "OAuth/API key, 9Router cc/claude-*, pi-model-proxy, any Claude " +
91
+ "Code-flavored proxy). Required whenever a provider has " +
92
+ 'api: "anthropic-messages" with a Claude model — without it, ' +
93
+ "tool calls fall back to Claude Code's built-in bash_ide sandbox.",
94
+ status: "required",
95
+ unlocks: ["Tool calls on Anthropic OAuth / 9Router cc/* / proxy providers"],
96
+ autowired: true,
97
+ },
98
+ {
99
+ id: "tintinweb-pi-subagents",
100
+ source: "npm:@tintinweb/pi-subagents",
101
+ displayName: "@tintinweb/pi-subagents",
102
+ fallbackDescription:
103
+ "Claude Code-style autonomous sub-agents for pi. Registers " +
104
+ "the Agent tool and its companions. The dashboard has custom " +
105
+ "card UI for it.",
106
+ status: "strongly-suggested",
107
+ unlocks: [
108
+ "Agent tool card UI",
109
+ "Subagent activity badge",
110
+ "get_subagent_result / steer_subagent renderers",
111
+ ],
112
+ toolsRegistered: ["Agent", "get_subagent_result", "steer_subagent"],
113
+ autowired: true,
114
+ },
115
+ {
116
+ id: "pi-flows",
117
+ source: "https://github.com/BlackBeltTechnology/pi-flows.git",
118
+ displayName: "pi-flows",
119
+ fallbackDescription:
120
+ "Flow engine, dashboard, and orchestration extensions for pi. " +
121
+ "Powers the dashboard's Flow view, role aliases, and multi-agent " +
122
+ "orchestration tools.",
123
+ status: "strongly-suggested",
124
+ unlocks: [
125
+ "Flow dashboard",
126
+ "Role aliases (@planning, @coding, …)",
127
+ "subagent / flow_write / flow_results / agent_write / ask_user / skill_read / finish tools",
128
+ ],
129
+ toolsRegistered: [
130
+ "subagent",
131
+ "agent_catalog",
132
+ "agent_write",
133
+ "flow_write",
134
+ "flow_results",
135
+ "skill_read",
136
+ "ask_user",
137
+ "finish",
138
+ ],
139
+ autowired: true,
140
+ },
141
+ {
142
+ id: "pi-web-access",
143
+ source: "npm:pi-web-access",
144
+ displayName: "pi-web-access",
145
+ fallbackDescription:
146
+ "Web search, URL fetching, GitHub repo cloning, PDF extraction, " +
147
+ "and YouTube / local video analysis for pi.",
148
+ status: "strongly-suggested",
149
+ unlocks: ["web_search", "code_search", "fetch_content", "get_search_content"],
150
+ toolsRegistered: [
151
+ "web_search",
152
+ "code_search",
153
+ "fetch_content",
154
+ "get_search_content",
155
+ ],
156
+ },
157
+ {
158
+ id: "pi-agent-browser",
159
+ source: "npm:pi-agent-browser",
160
+ displayName: "pi-agent-browser",
161
+ fallbackDescription:
162
+ "Browser automation (open, snapshot, click, fill, screenshot) " +
163
+ "via the agent-browser CLI.",
164
+ status: "optional",
165
+ unlocks: ["browser tool (open, snapshot, click, screenshot)"],
166
+ toolsRegistered: ["browser"],
167
+ },
168
+ ];
169
+
170
+ /**
171
+ * Ids of recommended extensions that ship inside the Electron installer
172
+ * as a pre-bundled source tree. See
173
+ * `packages/electron/scripts/bundle-recommended-extensions.sh` and
174
+ * `installBundledExtensions()` in `dependency-installer.ts`. Every id
175
+ * MUST also appear in `RECOMMENDED_EXTENSIONS` and MUST have a git-based
176
+ * `source` (enforced by a test).
177
+ *
178
+ * Kept deliberately short — only first-party, source-only, native-dep-free
179
+ * extensions belong here.
180
+ */
181
+ export const BUNDLED_EXTENSION_IDS: readonly string[] = [
182
+ "pi-anthropic-messages",
183
+ "pi-flows",
184
+ ];
185
+
186
+ /** Retrieve a recommended entry by id, or `undefined`. */
187
+ export function getRecommendedExtension(id: string): RecommendedExtension | undefined {
188
+ return RECOMMENDED_EXTENSIONS.find((e) => e.id === id);
189
+ }
190
+
191
+ /** Retrieve all entries with the given status. */
192
+ export function getRecommendedByStatus(
193
+ status: RecommendedExtensionStatus,
194
+ ): readonly RecommendedExtension[] {
195
+ return RECOMMENDED_EXTENSIONS.filter((e) => e.status === status);
196
+ }
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import { createRequire } from "node:module";
11
- import { realpathSync } from "node:fs";
11
+ import { existsSync, realpathSync } from "node:fs";
12
12
  import path from "node:path";
13
+ import { pathToFileURL } from "node:url";
13
14
 
14
15
  const JITI_PACKAGES = [
15
16
  "@mariozechner/jiti",
@@ -17,8 +18,38 @@ const JITI_PACKAGES = [
17
18
  ];
18
19
 
19
20
  /**
20
- * Returns the absolute path to jiti's register hook (lib/jiti-register.mjs).
21
+ * Pure helper: given a jiti package.json path, return the file:// URL of
22
+ * its register hook. Exported for testing — no I/O.
23
+ *
24
+ * Returns a file:// URL (not a raw path) because Node >= 20 on Windows
25
+ * rejects raw absolute paths with a drive letter for --import (parses
26
+ * "C:" / "B:" as a URL scheme → ERR_UNSUPPORTED_ESM_URL_SCHEME). file://
27
+ * URLs are accepted on every OS.
28
+ * See change: fix-windows-server-parity.
29
+ */
30
+ export function buildJitiRegisterUrl(pkgJsonPath: string): string {
31
+ // Detect Windows-style input (drive letter + backslash) regardless of
32
+ // host OS, so unit tests can exercise the Windows path contract on macOS/Linux.
33
+ // Production behaviour is unchanged because the host-OS `path`/`pathToFileURL`
34
+ // match the input style automatically.
35
+ const isWindowsStyle = /^[A-Za-z]:[\\/]/.test(pkgJsonPath);
36
+ if (isWindowsStyle) {
37
+ // Manually build file:///C:/path/lib/jiti-register.mjs — pathToFileURL on
38
+ // POSIX hosts URL-encodes backslashes rather than treating them as
39
+ // separators. Do the join with path.win32 and format the URL ourselves.
40
+ const registerPath = path.win32.join(path.win32.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
41
+ return `file:///${registerPath.replace(/\\/g, "/")}`;
42
+ }
43
+ const registerPath = path.join(path.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
44
+ return pathToFileURL(registerPath).href;
45
+ }
46
+
47
+ /**
48
+ * Returns jiti's register hook as a file:// URL suitable for `node --import`.
21
49
  * Uses process.argv[1] (pi's entry point) to anchor module resolution.
50
+ *
51
+ * The return value is ALWAYS a file:// URL (never a raw path). See
52
+ * buildJitiRegisterUrl for the URL contract rationale.
22
53
  */
23
54
  export function resolveJitiImport(): string {
24
55
  const anchor = process.argv[1];
@@ -30,7 +61,7 @@ export function resolveJitiImport(): string {
30
61
  for (const jiti of JITI_PACKAGES) {
31
62
  try {
32
63
  const pkgJson = req.resolve(`${jiti}/package.json`);
33
- return path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
64
+ return buildJitiRegisterUrl(pkgJson);
34
65
  } catch { /* next */ }
35
66
  }
36
67
  } catch { /* fall through */ }
@@ -41,3 +72,31 @@ export function resolveJitiImport(): string {
41
72
  "Is @mariozechner/pi-coding-agent or @oh-my-pi/pi-coding-agent installed?"
42
73
  );
43
74
  }
75
+
76
+ /**
77
+ * Resolve jiti's register hook from an arbitrary anchor path (e.g. a
78
+ * pi-coding-agent package.json in a managed install, or a pi binary on
79
+ * the system PATH). Returns a file:// URL or null if jiti cannot be
80
+ * resolved from the anchor.
81
+ *
82
+ * This is the Electron/managed-install variant of `resolveJitiImport`
83
+ * — the difference is the caller supplies the anchor explicitly
84
+ * instead of using `process.argv[1]`. Consolidates what used to be a
85
+ * duplicate `resolveJitiFromAnchor` in
86
+ * `packages/electron/src/lib/server-lifecycle.ts`.
87
+ * See change: consolidate-platform-handlers.
88
+ */
89
+ export function resolveJitiFromAnchor(anchorPath: string): string | null {
90
+ if (!existsSync(anchorPath)) return null;
91
+ try {
92
+ const req = createRequire(anchorPath);
93
+ for (const jiti of JITI_PACKAGES) {
94
+ try {
95
+ const pkgJson = req.resolve(`${jiti}/package.json`);
96
+ const registerPath = path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
97
+ if (existsSync(registerPath)) return pathToFileURL(registerPath).href;
98
+ } catch { /* next */ }
99
+ }
100
+ } catch { /* ignore */ }
101
+ return null;
102
+ }
@@ -7,6 +7,11 @@ import type {
7
7
  ApiResponse,
8
8
  } from "./types.js";
9
9
 
10
+ export type { ApiResponse } from "./types.js";
11
+ import type { EnrichedRecommendedExtension } from "./recommended-extensions.js";
12
+
13
+ export type { EnrichedRecommendedExtension } from "./recommended-extensions.js";
14
+
10
15
  // ── Sessions ────────────────────────────────────────────────────────
11
16
 
12
17
  export interface ListSessionsQuery {
@@ -63,14 +68,44 @@ export interface BrowseEntry {
63
68
  isPi: boolean;
64
69
  }
65
70
 
71
+ /**
72
+ * Response shape for `GET /api/browse?path=<dir>&q=<query>`.
73
+ *
74
+ * The optional `q` query parameter, when present and non-empty, causes the
75
+ * server to filter entries by case-insensitive substring on `name` and rank
76
+ * them (exact → prefix → word-boundary → substring) before the 200-entry cap.
77
+ * When omitted or whitespace-only, entries are sorted alphabetically.
78
+ */
66
79
  export interface BrowseResult {
67
80
  entries: BrowseEntry[];
68
81
  parent: string | null;
69
82
  current: string;
83
+ /**
84
+ * The server's `process.platform` — lets the client use OS-correct path
85
+ * handling (separator, case-sensitivity, drive-letter rules) without
86
+ * having to sniff `navigator.userAgent`. Optional for backward
87
+ * compatibility; consumers fall back to inferring from the `current`
88
+ * path shape when absent.
89
+ *
90
+ * See change: platform-path-normalization.
91
+ */
92
+ platform?: NodeJS.Platform;
70
93
  }
71
94
 
72
95
  export type BrowseResponse = ApiResponse<BrowseResult>;
73
96
 
97
+ /** Request body for `POST /api/browse/mkdir`. */
98
+ export interface MkdirRequest {
99
+ parent: string;
100
+ name: string;
101
+ }
102
+
103
+ export interface MkdirResult {
104
+ path: string;
105
+ }
106
+
107
+ export type MkdirResponse = ApiResponse<MkdirResult>;
108
+
74
109
  // ── Tunnel Status ───────────────────────────────────────────────────
75
110
 
76
111
  export type TunnelStatus =
@@ -246,6 +281,46 @@ export interface PackageUpdateInfo {
246
281
 
247
282
  export type CheckUpdatesResponse = ApiResponse<PackageUpdateInfo[]>;
248
283
 
284
+ // ── Pi core version check ────────────────────────────────────
285
+
286
+ /** A core pi ecosystem CLI package (not managed by pi's PackageManager). */
287
+ export interface PiCorePackage {
288
+ name: string;
289
+ displayName: string;
290
+ currentVersion: string;
291
+ latestVersion: string | null;
292
+ updateAvailable: boolean;
293
+ installSource: "global" | "managed";
294
+ }
295
+
296
+ export interface PiCoreStatus {
297
+ packages: PiCorePackage[];
298
+ updatesAvailable: number;
299
+ lastChecked: string;
300
+ }
301
+
302
+ export type PiCoreVersionsResponse = ApiResponse<PiCoreStatus>;
303
+
304
+ /** Request body for POST /api/pi-core/update. Empty packages = update all. */
305
+ export interface PiCoreUpdateRequest {
306
+ packages?: string[];
307
+ }
308
+
309
+ /** Result of a single package update. */
310
+ export interface PiCoreUpdateResult {
311
+ name: string;
312
+ success: boolean;
313
+ error?: string;
314
+ }
315
+
316
+ /** Response from POST /api/pi-core/update (completes synchronously). */
317
+ export interface PiCoreUpdateResponse {
318
+ results: PiCoreUpdateResult[];
319
+ sessionsReloaded: number;
320
+ }
321
+
322
+ export type PiCoreUpdateApiResponse = ApiResponse<PiCoreUpdateResponse>;
323
+
249
324
  // ── Known Servers ─────────────────────────────────────────────
250
325
 
251
326
  import type { KnownServer } from "./config.js";
@@ -281,3 +356,25 @@ export interface NetworkInterface {
281
356
  netmask: string;
282
357
  cidr: string;
283
358
  }
359
+
360
+ // ── Recommended extensions ───────────────────────────
361
+
362
+ export type ListRecommendedExtensionsResponse = ApiResponse<{
363
+ recommended: EnrichedRecommendedExtension[];
364
+ }>;
365
+
366
+ // ── Tool registry ────────────────────
367
+
368
+ import type { Resolution } from "./tool-registry/types.js";
369
+ export type { Resolution, Source, TriedEntry } from "./tool-registry/types.js";
370
+
371
+ export type ListToolsResponse = ApiResponse<{ tools: Resolution[] }>;
372
+ export type GetToolResponse = ApiResponse<Resolution>;
373
+
374
+ export interface RescanToolsRequest {
375
+ name?: string;
376
+ }
377
+
378
+ export interface SetToolOverrideRequest {
379
+ path: string;
380
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tiny FIFO semaphore for throttling concurrent async operations.
3
+ *
4
+ * Used by the server's openspec polling scheduler to cap how many
5
+ * `openspec` CLI spawns may be running at once. Rolled in-repo instead
6
+ * of pulling `p-limit` because we need `setMax()` for live reconfig
7
+ * (when the user edits `openspec.maxConcurrentSpawns` in settings).
8
+ *
9
+ * Contract:
10
+ * - `run(fn)` runs `fn` through the gate. At most `max` tasks are
11
+ * in-flight; excess tasks queue FIFO.
12
+ * - `setMax(n)` resizes. Growing drains the queue up to the new cap
13
+ * on the next microtask. Shrinking does not interrupt in-flight
14
+ * tasks; it only affects newly queued ones.
15
+ * - `size()` = active + queued.
16
+ * - If the task throws/rejects, the slot is released and queued
17
+ * tasks proceed.
18
+ */
19
+ export interface Semaphore {
20
+ run<T>(fn: () => Promise<T>): Promise<T>;
21
+ setMax(n: number): void;
22
+ size(): number;
23
+ }
24
+
25
+ export function createSemaphore(max: number): Semaphore {
26
+ if (!Number.isFinite(max) || max < 1) {
27
+ throw new Error(`Semaphore max must be a positive integer, got ${max}`);
28
+ }
29
+ let limit = Math.floor(max);
30
+ let active = 0;
31
+ const queue: Array<() => void> = [];
32
+
33
+ function drain() {
34
+ while (active < limit && queue.length > 0) {
35
+ const next = queue.shift()!;
36
+ active++;
37
+ next();
38
+ }
39
+ }
40
+
41
+ function release() {
42
+ active--;
43
+ // Schedule drain on microtask so `run()` callers see a stable state first.
44
+ queueMicrotask(drain);
45
+ }
46
+
47
+ return {
48
+ run<T>(fn: () => Promise<T>): Promise<T> {
49
+ return new Promise<T>((resolve, reject) => {
50
+ const start = () => {
51
+ let settled = false;
52
+ try {
53
+ Promise.resolve()
54
+ .then(fn)
55
+ .then(
56
+ (value) => { if (!settled) { settled = true; release(); resolve(value); } },
57
+ (err) => { if (!settled) { settled = true; release(); reject(err); } },
58
+ );
59
+ } catch (err) {
60
+ if (!settled) { settled = true; release(); reject(err); }
61
+ }
62
+ };
63
+ if (active < limit) {
64
+ active++;
65
+ start();
66
+ } else {
67
+ queue.push(start);
68
+ }
69
+ });
70
+ },
71
+ setMax(n: number): void {
72
+ if (!Number.isFinite(n) || n < 1) {
73
+ throw new Error(`Semaphore max must be a positive integer, got ${n}`);
74
+ }
75
+ limit = Math.floor(n);
76
+ // Drain synchronously so callers that do `setMax(n); await tick` see queued tasks started.
77
+ drain();
78
+ },
79
+ size(): number {
80
+ return active + queue.length;
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,126 @@
1
+ // ---------------------------------------------------------------------------
2
+ // source-matching — canonical "two source strings refer to the same package"
3
+ // predicate, shared between the server's /api/packages/recommended route
4
+ // and the Electron wizard's bootstrap enricher.
5
+ //
6
+ // Pure string logic, no fs / no pi SDK dependency. Safe to import from any
7
+ // package (shared, server, client, electron).
8
+ //
9
+ // Input sources take one of these forms:
10
+ //
11
+ // npm:<name>[@<version>]
12
+ // e.g. "npm:pi-web-access", "npm:@tintinweb/pi-subagents@0.5.2"
13
+ //
14
+ // git@<host>:<owner>/<repo>[.git]
15
+ // e.g. "git@github.com:BlackBeltTechnology/pi-flows.git"
16
+ //
17
+ // https://<host>/<owner>/<repo>[.git][#ref]
18
+ // e.g. "https://github.com/BlackBeltTechnology/pi-flows.git"
19
+ //
20
+ // git:<host>/<owner>/<repo>[#ref]
21
+ // e.g. "git:github.com/BlackBeltTechnology/pi-flows#main"
22
+ //
23
+ // any other string (absolute path, relative path, unrecognized URL)
24
+ // → parsed as kind:"raw" with the literal preserved
25
+ //
26
+ // Matching rules:
27
+ // - Same kind: exact comparison of the semantically-meaningful parts.
28
+ // - Cross-kind (git ↔ raw): the raw source's basename (last path
29
+ // segment, stripped of trailing slash and trailing .git) must equal
30
+ // the git repo name, case-insensitive. This handles the common case
31
+ // where a user registered the package via `pi install -l <path>`
32
+ // instead of by URL and the basename is the repo name.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export type SourceKey =
36
+ | { kind: "npm"; name: string }
37
+ | { kind: "git"; host: string; owner: string; repo: string }
38
+ | { kind: "raw"; source: string };
39
+
40
+ export function parseSourceKey(source: string): SourceKey {
41
+ const trimmed = source.trim();
42
+
43
+ if (trimmed.startsWith("npm:")) {
44
+ const spec = trimmed.slice(4).trim();
45
+ // Strip a trailing @version but preserve the scope @ in @scope/name.
46
+ // If spec starts with @, the SECOND @ (if any) delimits version.
47
+ let name = spec;
48
+ if (spec.startsWith("@")) {
49
+ const idx = spec.indexOf("@", 1);
50
+ if (idx > 0) name = spec.slice(0, idx);
51
+ } else {
52
+ const idx = spec.indexOf("@");
53
+ if (idx > 0) name = spec.slice(0, idx);
54
+ }
55
+ return { kind: "npm", name };
56
+ }
57
+
58
+ const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/.]+)(?:\.git)?$/);
59
+ if (sshMatch) {
60
+ return { kind: "git", host: sshMatch[1], owner: sshMatch[2], repo: sshMatch[3] };
61
+ }
62
+
63
+ const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/#.]+)(?:\.git)?(?:#.+)?$/);
64
+ if (httpsMatch) {
65
+ return {
66
+ kind: "git",
67
+ host: httpsMatch[1],
68
+ owner: httpsMatch[2],
69
+ repo: httpsMatch[3],
70
+ };
71
+ }
72
+
73
+ const gitPrefixMatch = trimmed.match(/^git:([^/]+)\/([^/]+)\/([^/#]+?)(?:\.git)?(?:#.+)?$/);
74
+ if (gitPrefixMatch) {
75
+ return {
76
+ kind: "git",
77
+ host: gitPrefixMatch[1],
78
+ owner: gitPrefixMatch[2],
79
+ repo: gitPrefixMatch[3],
80
+ };
81
+ }
82
+
83
+ return { kind: "raw", source: trimmed };
84
+ }
85
+
86
+ /**
87
+ * Extract the basename (last path segment, .git-stripped) from a raw
88
+ * source string. Returns lowercase or null.
89
+ */
90
+ function localPathBasename(src: string): string | null {
91
+ const stripped = src.replace(/\/+$/, "").replace(/\.git$/, "");
92
+ const segments = stripped.split(/[\\/]/);
93
+ const tail = segments[segments.length - 1];
94
+ return tail ? tail.toLowerCase() : null;
95
+ }
96
+
97
+ /**
98
+ * True iff two source strings refer to the same package. See module
99
+ * header for the full matching rules and rationale.
100
+ */
101
+ export function sourcesMatch(a: string, b: string): boolean {
102
+ const ka = parseSourceKey(a);
103
+ const kb = parseSourceKey(b);
104
+
105
+ if (ka.kind === kb.kind) {
106
+ if (ka.kind === "npm" && kb.kind === "npm") return ka.name === kb.name;
107
+ if (ka.kind === "git" && kb.kind === "git") {
108
+ return (
109
+ ka.host.toLowerCase() === kb.host.toLowerCase() &&
110
+ ka.owner.toLowerCase() === kb.owner.toLowerCase() &&
111
+ ka.repo.toLowerCase() === kb.repo.toLowerCase()
112
+ );
113
+ }
114
+ if (ka.kind === "raw" && kb.kind === "raw") return ka.source === kb.source;
115
+ }
116
+
117
+ // Cross-kind: git ↔ raw (local path). Match on repo basename.
118
+ const gitKey = ka.kind === "git" ? ka : kb.kind === "git" ? kb : null;
119
+ const rawKey = ka.kind === "raw" ? ka : kb.kind === "raw" ? kb : null;
120
+ if (gitKey && rawKey) {
121
+ const basename = localPathBasename(rawKey.source);
122
+ if (basename && basename === gitKey.repo.toLowerCase()) return true;
123
+ }
124
+
125
+ return false;
126
+ }