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

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 (129) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +30 -0
  3. package/docs/architecture.md +129 -1
  4. package/package.json +6 -6
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +10 -8
  8. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  9. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  10. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  11. package/packages/extension/src/bridge-context.ts +67 -3
  12. package/packages/extension/src/bridge.ts +20 -8
  13. package/packages/extension/src/command-handler.ts +36 -13
  14. package/packages/extension/src/prompt-expander.ts +74 -63
  15. package/packages/extension/src/server-launcher.ts +31 -70
  16. package/packages/extension/src/slash-dispatch.ts +123 -0
  17. package/packages/server/bin/pi-dashboard.mjs +84 -0
  18. package/packages/server/package.json +6 -5
  19. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  20. package/packages/server/src/__tests__/cli-parse.test.ts +12 -18
  21. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  22. package/packages/server/src/__tests__/directory-service.test.ts +1 -1
  23. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  24. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  25. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  26. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  27. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  28. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  29. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  30. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  31. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  32. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  33. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  34. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  35. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  36. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  37. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  38. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  39. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  40. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  41. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  42. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  43. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  44. package/packages/server/src/auth-plugin.ts +3 -0
  45. package/packages/server/src/bootstrap-state.ts +10 -0
  46. package/packages/server/src/browser-gateway.ts +15 -7
  47. package/packages/server/src/browser-handlers/session-action-handler.ts +30 -4
  48. package/packages/server/src/cli.ts +61 -81
  49. package/packages/server/src/config-api.ts +14 -2
  50. package/packages/server/src/directory-service.ts +106 -4
  51. package/packages/server/src/event-wiring.ts +31 -1
  52. package/packages/server/src/headless-pid-registry.ts +299 -41
  53. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  54. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  55. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  56. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  57. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  58. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  59. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  60. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  61. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  62. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  63. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  64. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  65. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  66. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  67. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  68. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  69. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  70. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  71. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  72. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  73. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  74. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  75. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  76. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  77. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  78. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  79. package/packages/server/src/model-proxy/request-log.ts +53 -0
  80. package/packages/server/src/model-proxy/streamer.ts +59 -0
  81. package/packages/server/src/openspec-group-store.ts +490 -0
  82. package/packages/server/src/process-manager.ts +128 -0
  83. package/packages/server/src/provider-auth-storage.ts +29 -47
  84. package/packages/server/src/restart-helper.ts +17 -16
  85. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  86. package/packages/server/src/routes/jj-routes.ts +3 -0
  87. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  88. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  89. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  90. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  91. package/packages/server/src/routes/provider-auth-routes.ts +3 -0
  92. package/packages/server/src/routes/provider-routes.ts +24 -1
  93. package/packages/server/src/routes/system-routes.ts +44 -2
  94. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  95. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  96. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  97. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  98. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  99. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  100. package/packages/server/src/server.ts +178 -2
  101. package/packages/server/src/session-api.ts +9 -1
  102. package/packages/server/src/tunnel-watchdog.ts +230 -0
  103. package/packages/server/src/tunnel.ts +5 -1
  104. package/packages/shared/package.json +1 -1
  105. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  106. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  107. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  108. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  109. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  110. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  111. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  112. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  113. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  114. package/packages/shared/src/bootstrap-install.ts +1 -1
  115. package/packages/shared/src/browser-protocol.ts +27 -0
  116. package/packages/shared/src/config.ts +172 -2
  117. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  118. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  119. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  120. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  121. package/packages/shared/src/platform/node-spawn.ts +42 -5
  122. package/packages/shared/src/protocol.ts +19 -1
  123. package/packages/shared/src/recommended-extensions.ts +18 -0
  124. package/packages/shared/src/rest-api.ts +219 -1
  125. package/packages/shared/src/server-launcher.ts +277 -0
  126. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  127. package/packages/shared/src/types.ts +55 -0
  128. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -184
  129. package/packages/shared/src/resolve-jiti.ts +0 -155
package/AGENTS.md CHANGED
@@ -227,8 +227,12 @@ This section lists only the **architectural backbone** — the files agents touc
227
227
  | `src/shared/types.ts` | Data models (Session, Workspace, Event) |
228
228
  | `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`) |
229
229
  | `src/shared/semaphore.ts` | Tiny FIFO semaphore (`createSemaphore(max)`) |
230
- | `src/extension/bridge.ts` | Main bridge extension entry; PromptBus patch site, sync/tracker/flow composition |
231
- | `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules |
230
+ | `src/extension/bridge.ts` | Main bridge extension entry; PromptBus patch site, sync/tracker/flow composition; sessionPrompt routes through slash-dispatch (extension-cmd dispatch + stopgap) before template fallback. See change: fix-extension-slash-commands-in-dashboard. |
231
+ | `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules; hosts `isExtensionSlashCommand`, `hasDispatchCommand`, `DASHBOARD_NATIVE_COMMANDS` (= {"roles"}). See change: fix-extension-slash-commands-in-dashboard. |
232
+ | `src/extension/slash-dispatch.ts` | Shared `tryDispatchExtensionCommand` helper: routing-step 9 (three-way decision: pi.dispatchCommand 0.71+ → server-routed via RPC keeper UDS when headless → stopgap). See change: fix-extension-slash-commands-in-dashboard, add-rpc-stdin-dispatch-with-keeper-sidecar. |
233
+ | `packages/server/src/rpc-keeper/dispatch-router.ts` | Handles `dispatch_extension_command` extension→server message. Forwards pi RPC line via `headlessPidRegistry.writeRpc`; emits optimistic `command_feedback {completed}` or {error}. See change: add-rpc-stdin-dispatch-with-keeper-sidecar. |
234
+ | `packages/server/src/rpc-keeper/keeper-manager.ts` | Server-side helper: `spawnKeeperFor`, `writeRpc(sessionId)`, `writeRpcToSockPath`, `killKeeper`, `discoverExistingKeepers`. Singleton via `getKeeperManager()` in process-manager.ts. See change: add-rpc-stdin-dispatch-with-keeper-sidecar. |
235
+ | `packages/server/src/rpc-keeper/keeper.cjs` | CJS-pure RPC keeper sidecar; spawns pi as child, owns stdin pipe, listens on per-session UDS / named pipe; outlives dashboard server. See change: add-rpc-stdin-dispatch-with-keeper-sidecar. |
232
236
  | `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling |
233
237
  | `src/extension/model-tracker.ts` | Model/thinking-level/git/name change detection |
234
238
  | `src/extension/flow-event-wiring.ts` | Flow event listener registration (flow:* → event_forward) |
@@ -237,7 +241,7 @@ This section lists only the **architectural backbone** — the files agents touc
237
241
  | `src/shared/server-identity.ts` | Identity-verified health check (`isDashboardRunning`) |
238
242
  | `src/shared/mdns-discovery.ts` | mDNS advertise/discover/browse for `_pi-dashboard._tcp` |
239
243
  | `src/extension/server-launcher.ts` | Auto-start server as detached process; logs to `~/.pi/dashboard/server.log` |
240
- | `src/extension/command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands |
244
+ | `src/extension/command-handler.ts` | Command routing: `!`/`!!` bash, `/compact`, slash commands; slash else-arm now gates through extension-dispatch helper before `sendUserMessage`. See change: fix-extension-slash-commands-in-dashboard. |
241
245
  | `src/extension/prompt-expander.ts` | Slash command → prompt template expansion |
242
246
  | `src/extension/dev-build.ts` | Dev build-on-reload helper (client build + server shutdown) |
243
247
  | `src/extension/server-auto-start.ts` | mDNS-first → health check → auto-start with concurrent launch detection |
@@ -390,6 +394,22 @@ This section lists only the **architectural backbone** — the files agents touc
390
394
  | `src/server/provider-auth-storage.ts` | Read/write `~/.pi/agent/auth.json` with lockfile for pi provider credentials |
391
395
  | `src/server/routes/provider-auth-routes.ts` | REST routes: provider OAuth authorize/exchange/callback, device-code, API key CRUD |
392
396
  | `src/server/routes/provider-routes.ts` | REST routes: custom LLM provider CRUD + connection probe |
397
+ | `src/server/model-proxy/auth-gate.ts` | Fastify `onRequest` hook for `/v1/*`: uniform proxy-key auth, backoff, scope check |
398
+ | `src/server/model-proxy/api-key-store.ts` | Pure helpers: `hashKey`, `verifyKey`, `generateKey`, `findApiKey`, `recordKeyUsage`, `keyHasScope` |
399
+ | `src/server/model-proxy/concurrency.ts` | `ConcurrencyTracker`: server-wide + per-key + per-provider caps; throws `ConcurrencyError` |
400
+ | `src/server/model-proxy/failed-auth-backoff.ts` | Per-IP exponential backoff (10ms→10s cap) for failed proxy-key auth |
401
+ | `src/server/model-proxy/internal-registry.ts` | Server-resident model registry: reads auth/providers/models.json + pi-ai built-ins |
402
+ | `src/server/model-proxy/internal-auth-storage.ts` | Wraps `provider-auth-storage.ts`; handles OAuth-refresh-on-expiry via pi-ai per-provider helpers |
403
+ | `src/server/model-proxy/registry-singleton.ts` | Lazy singleton: `getModelRegistry()`, `refreshModelRegistry()`, `getModelProxyStatus()` |
404
+ | `src/server/model-proxy/recursion-guard.ts` | `isSelfPointing(baseUrl, origins)`: detects dashboard-pointing custom provider baseUrls |
405
+ | `src/server/model-proxy/request-log.ts` | Append-mode JSONL request log at `~/.pi/dashboard/model-proxy.jsonl`; 50MB rotation |
406
+ | `src/server/model-proxy/streamer.ts` | `streamCompletion(opts, streamSimple, registry?)`: resolves creds then streams via pi-ai |
407
+ | `src/server/model-proxy/convert/` | Lifted MIT converters: OpenAI↔pi-ai, Anthropic↔pi-ai (format conversions, SSE output) |
408
+ | `src/server/routes/model-proxy-routes.ts` | Route handlers: `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/messages` |
409
+ | `src/server/routes/model-proxy-api-key-routes.ts` | REST CRUD for proxy API keys: list, create, revoke, purge (JWT-gated) |
410
+ | `src/server/routes/model-proxy-refresh-routes.ts` | `POST /api/model-proxy/refresh`: JWT-gated manual registry refresh |
411
+ | `src/client/components/ModelProxySection.tsx` | Settings section: proxy toggle, second-port, API key table + reveal-once banner |
412
+ | `src/client/lib/model-proxy-api.ts` | Client fetch helpers: `listApiKeys`, `createApiKey`, `revokeApiKey`, `deleteApiKey`, `refreshRegistry` |
393
413
  | `src/server/provider-probe.ts` | Pure per-API probe builders + I/O `probeProvider` (8s timeout, no apiKey echo) |
394
414
  | `src/extension/provider-register.ts` | Reads `providers.json`, calls `pi.registerProvider`, hot-reload on credentials change |
395
415
  | `src/client/lib/providers-api.ts` | Client fetch helper for `/api/providers/test` connection probe |
@@ -420,7 +440,7 @@ This section lists only the **architectural backbone** — the files agents touc
420
440
  | `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view |
421
441
  | `src/server/cli.ts` | CLI entry: start/stop/restart/status; `cmdRestart` delegates to `/api/restart` when up |
422
442
  | `src/server/restart-helper.ts` | Cross-platform `/api/restart` orchestrator (detached node-built-ins-only spawner) |
423
- | `src/shared/resolve-jiti.ts` | Resolves pi's jiti register hook as a `file://` URL |
443
+ | `packages/shared/src/server-launcher.ts` | `launchDashboardServer` single shared spawn primitive (jiti loader, argv, env, log header, readiness) used by Bridge / Standalone / Electron starters |
424
444
  | `src/shared/platform/paths.ts` | OS-aware path primitives (`normalizePath`, `samePath`, `parsePathInput`) |
425
445
  | `src/client/lib/session-grouping.ts` | Sessions grouped by directory; `resolveSessionGroupPath` (pin > jjState.workspaceRoot > cwd) |
426
446
  | `src/shared/platform/` | Unified cross-OS primitives barrel (exec/runner/git/openspec/npm/process/binary-lookup/...) |
@@ -482,7 +502,8 @@ This section lists only the **architectural backbone** — the files agents touc
482
502
  | `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
483
503
  | `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
484
504
  | `packages/electron/resources/icon.png` | Master 1024×1024 app icon |
485
- | `.github/workflows/publish.yml` | CI: build matrix × 6 (platform,arch); idempotent ordered npm publish; no-bash-on-Windows |
505
+ | `.github/workflows/publish.yml` | CI: build matrix × 6 (platform,arch); idempotent ordered npm publish; lockfile regen + verify in prepare; no-bash-on-Windows |
506
+ | `scripts/verify-lockfile-versions.mjs` | Sanity gate: asserts every cross-ref in `package-lock.json` is `^<root.version>`; runs after `npm install --package-lock-only` in `prepare` |
486
507
  | `packages/shared/src/__tests__/publish-workflow-contract.test.ts` | Repo-lint: pin electron job's `needs:` array and `fail-fast: false` |
487
508
  | `packages/shared/src/__tests__/no-bash-on-windows.test.ts` | Repo-lint: forbid `shell: bash` on steps reachable on Windows runners |
488
509
 
package/README.md CHANGED
@@ -62,6 +62,15 @@ On first launch a setup wizard walks you through mode selection (standalone vs.
62
62
 
63
63
  **Picking the right macOS DMG:** run `uname -m` in Terminal — `arm64` means Apple Silicon (M1/M2/M3/M4), `x86_64` means Intel. Or open  Apple menu → About This Mac and read the chip name. Download the matching DMG; if you grab the wrong one macOS will refuse to launch the app with a "cannot be opened" error.
64
64
 
65
+ **First-run unblocking (unsigned binaries):**
66
+
67
+ - **macOS** — the DMGs are not yet notarized. Either right-click `PI-Dashboard.app` → *Open* the first time, or clear all extended attributes from Terminal:
68
+ ```bash
69
+ xattr -cr /Applications/PI-Dashboard.app
70
+ ```
71
+ Use `-cr` (clear, recursive) rather than `-d com.apple.quarantine` — it's idempotent and won't print `No such xattr: com.apple.quarantine` when the attribute isn't there. That message is harmless; it just means quarantine was never set or already cleared.
72
+ - **Windows** — SmartScreen warns on first launch. Click *More info → Run anyway*, or right-click the downloaded `.exe` / `.zip` → *Properties* → tick *Unblock* → *OK* before running. For ZIPs, unblock the archive before extracting.
73
+
65
74
  > **Note:** A future release will rename the macOS DMGs to `PI-Dashboard-darwin-arm64-<ver>.dmg` and `PI-Dashboard-darwin-x64-<ver>.dmg` (previously a single `PI Dashboard.dmg` was produced and silently overwrote one arch on each release). Direct download links pointing at the unsuffixed filename will 404 from that release onward; please link to the [Releases page](https://github.com/BlackBeltTechnology/pi-agent-dashboard/releases) instead. See OpenSpec change `fix-darwin-dmg-arch-collision`.
66
75
 
67
76
  ### B — pi package (recommended for CLI users)
@@ -275,6 +284,27 @@ The file is deliberately separate from `config.json` so machine-specific paths d
275
284
 
276
285
  ---
277
286
 
287
+ ## Using the model proxy
288
+
289
+ The dashboard exposes an OpenAI-compatible HTTP proxy on the same port as the dashboard UI (`/v1/...`). Any LLM client that accepts a custom `base_url` can use it.
290
+
291
+ ```bash
292
+ # Example: point an OpenAI-compatible client at the dashboard
293
+ export OPENAI_BASE_URL=http://localhost:8000/v1
294
+ export OPENAI_API_KEY=pi-proxy-<your-proxy-key>
295
+ ```
296
+
297
+ **Setup:** open Settings → API Proxy in the dashboard UI, enable the proxy, and create an API key.
298
+
299
+ **Endpoints:**
300
+ - `GET /v1/models` — list available models (requires `models:list` scope or `all`)
301
+ - `POST /v1/chat/completions` — OpenAI chat completions, streaming + non-streaming
302
+ - `POST /v1/messages` — Anthropic messages, streaming + non-streaming
303
+
304
+ **Auth:** proxy API keys only (`pi-proxy-*` prefix). Dashboard JWT is never accepted on `/v1/*`.
305
+
306
+ For migration from `@blackbelt-technology/pi-model-proxy`, see [`docs/migration/from-pi-model-proxy.md`](docs/migration/from-pi-model-proxy.md).
307
+
278
308
  ## Usage
279
309
 
280
310
  ### Auto-start (default)
@@ -483,6 +483,24 @@ See changes: `unified-bootstrap-install`, `pi-zero-seventy-compat`, `warn-pi-ver
483
483
 
484
484
  Electron resources ship bundled Node under `resources/node/` (Windows: `node.exe` + `npm.cmd` + `npx.cmd` at root; Unix: `bin/node` + `bin/npm` + `bin/npx`). `installManagedNode` (`packages/shared/src/bootstrap-install.ts`) `fs.cp`-copies bundle into `<managedDir>/node/` (default `~/.pi-dashboard/node/`), writes `.version` marker for idempotency. `installStandalone` calls it BEFORE first `bootstrapInstall` so npm shims exist when registry install runs; Doctor calls it unconditionally on every launch as a self-repair step. ToolRegistry `node` + `npm` strategy chains prefer `<managedDir>/node/` ahead of system PATH; `prependManagedNodeToPath(env, managedDir)` (`packages/shared/src/platform/managed-node-path.ts`) injects same dir at HEAD of every spawned child's `PATH` (pi-session, pi-core-updater, headless RPC, server-launcher) so `npm.cmd`/`npx.cmd` resolve without `where npm` returning empty on Windows. Standalone CLI / dev / builds without `resources/node/`: bundled source absent, both helpers no-op, resolver falls through to system PATH. See change: embed-managed-node-runtime.
485
485
 
486
+ #### Legacy pi detection & cleanup
487
+
488
+ Pi renamed `@mariozechner/pi-coding-agent` → `@earendil-works/pi-coding-agent` at v0.74. Old scope ships only up to v0.73.x; new scope's `bin/pi` symlink collides with legacy install on `npm install -g` (EEXIST), producing silent "no spawning" failures when both exist.
489
+
490
+ `packages/server/src/legacy-pi-cleanup.ts` scans 3 locations: npm-global (`npm root -g` → `<root>/@mariozechner/pi-coding-agent`), npx-cache (`~/.npm/_npx/*/node_modules/@mariozechner/pi-coding-agent`, all hashed entries), managed (`~/.pi-dashboard/node_modules/@mariozechner/pi-coding-agent`). Detection cost: one `npm root -g` (~50ms) + `fs.statSync` per candidate. Read-only.
491
+
492
+ Startup scan: `server.ts` calls `detectLegacyPiInstalls()` once at boot, writes result to `bootstrapState.legacyPiInstalls`. Broadcast via `bootstrap_status_update` WS.
493
+
494
+ REST: `GET /api/bootstrap/legacy-pi` force-refreshes detection. `POST /api/bootstrap/legacy-pi/cleanup` removes all detected installs, re-scans, returns `{results, remaining}`. Both auth-gated.
495
+
496
+ UI: `BootstrapBanner` renders amber "Remove legacy pi (N)" sub-banner when `legacyPiInstalls.length > 0` AND `status === "ready"`. Takes precedence over upgrade-recommended hint (legacy install blocks `upgrade-pi`). Wired via `useBootstrapStatus().cleanupLegacyPi()`.
497
+
498
+ Cleanup actions: npm-global removed via `npm uninstall -g @mariozechner/pi-coding-agent --no-fund --no-audit` so bin symlinks unwound; npx-cache + managed via `fs.rmSync({recursive, force})`. Per-install try/catch — one failure does not abort siblings.
499
+
500
+ Idempotent: empty pre-scan short-circuits the POST without invoking npm; missing path under `fs.rmSync({force:true})` returns `removed: true`. Tests in `packages/server/src/__tests__/legacy-pi-cleanup.test.ts` (12 cases) cover pure helpers, fs-backed detection under HOME-tempdir isolation, and removal idempotency.
501
+
502
+ See change: legacy-pi-cleanup.
503
+
486
504
  ### Force Kill Escalation
487
505
  The Stop button supports two-click escalation for stuck sessions:
488
506
  1. **Click 1 (Abort)**: Sends `abort` → bridge → `ctx.abort()`. Button transitions to orange pulsing "Force Stop".
@@ -697,6 +715,12 @@ Fold-back is **a skill, not a server route**. The dashboard's `JjFoldBackDialog`
697
715
  7. Session cards display processes with elapsed time and a kill button (sends SIGTERM to process group)
698
716
 
699
717
  ### OpenSpec Polling (Server-Side)
718
+
719
+ **Master gate**: `DashboardConfig.openspec.enabled` (boolean, default `true`).
720
+ - `false` disables all polling. Hides OPENSPEC subcards across dashboard.
721
+ - Tuning fields below (`pollIntervalSeconds`, `maxConcurrentSpawns`, `changeDetection`, `jitterSeconds`) ignored at runtime when `false`. Values preserved.
722
+ - Runtime-reconfigurable via `PUT /api/config`. Disable transition clears per-cwd `OpenSpecData` cache + broadcasts cleared payload `{ initialized: false, pending: false, changes: [] }` per cwd so client predicate `openspecInitialized === false && pending === false` collapses subcards uniformly. Same broadcast shape covers "no openspec/ dir" + "openspec.enabled === false".
723
+
700
724
  1. Server's DirectoryService polls `openspec` CLI for each known directory (union of pinned dirs + session cwds) at a **configurable interval** (`DashboardConfig.openspec.pollIntervalSeconds`, default 30 s, range 5–3600 s).
701
725
  2. OpenSpec data is keyed by directory (cwd), not by session — one poll per directory regardless of session count.
702
726
  3. Changes are broadcast to all connected browsers via `openspec_update { cwd, data }`.
@@ -1012,6 +1036,24 @@ To disable: set `tunnel.enabled` to `false` in `~/.pi/dashboard/config.json` or
1012
1036
  The client can query `GET /api/tunnel-status` which returns `{ status: "active"|"inactive"|"unavailable", url?, serverOs }`.
1013
1037
  The client can connect/disconnect the tunnel via `POST /api/tunnel-connect` and `POST /api/tunnel-disconnect`.
1014
1038
 
1039
+ ### Tunnel watchdog
1040
+
1041
+ Long-lived `zrok share` goes stale on edge. Watchdog detects + recycles.
1042
+
1043
+ - Probe target: `GET ${publicUrl}/api/health` through public zrok URL (real edge round-trip, not localhost).
1044
+ - Cadence: every `intervalMs` (default 60000).
1045
+ - Failure classes: HTTP 5xx, network error, timeout (`probeTimeoutMs`, default 10000). 4xx + 2xx count as healthy reachability.
1046
+ - Threshold: `failureThreshold` consecutive failures (default 2) triggers recycle.
1047
+ - Recycle action: `deleteTunnel()` then `createTunnel(port, reservedToken)`. Reserved token preserved — URL stable.
1048
+ - Counter resets to 0 on success or after successful recycle.
1049
+ - Recycle-failure backoff: next retry delay ×2 each consecutive recycle failure, capped ×8 `intervalMs`. Resets to base on first successful probe.
1050
+ - Config: `tunnel.watchdog.{enabled, intervalMs, failureThreshold, probeTimeoutMs}` in `~/.pi/dashboard/config.json`. Defaults: enabled true, 60s, 2 failures, 10s timeout.
1051
+ - Status: `GET /api/tunnel-status` active variant carries `watchdog: {lastProbeAt, lastSuccessAt, lastFailureAt, lastFailureReason, consecutiveFailures, lastRecycleAt, recycleCount}`.
1052
+ - Lifecycle: `startTunnelWatchdog` called in `server.ts` after `createTunnel` succeeds at startup + in `/api/tunnel-connect` after on-demand connect. `stopTunnelWatchdog` called before `deleteTunnel` in graceful shutdown + `/api/tunnel-disconnect`.
1053
+ - Module: `packages/server/src/tunnel-watchdog.ts`. Tests: `packages/server/src/__tests__/tunnel-watchdog.test.ts`.
1054
+ - Settings UI: Settings → Tunnel exposes watchdog enable + intervalMs + failureThreshold + probeTimeoutMs.
1055
+ - Live reload: `PUT /api/config` with `partial.tunnel` stops + restarts watchdog against new config when tunnel active. No server restart required for watchdog tweaks.
1056
+ - `writeConfigPartial` deep-merges `tunnel.watchdog` so partial UI saves preserve unspecified fields.
1015
1057
 
1016
1058
 
1017
1059
  ### CORS
@@ -1179,6 +1221,42 @@ Every mechanism branch forwards `sessionFile` + `mode` via the shared `sessionFl
1179
1221
 
1180
1222
  On Windows, `spawnDetached` uses `detached: true` which (via libuv's `src/win/process.c`) emits `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` and critically does NOT call `AssignProcessToJobObject` on the parent's global Job Object. This excludes the child from the parent's `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` job, so pi sessions survive when the dashboard server exits — matching Unix PGID behavior. The `headlessPidRegistry` reconciles these survivors on server restart.
1181
1223
 
1224
+ ### RPC keeper sidecar
1225
+
1226
+ Introduced by change `add-rpc-stdin-dispatch-with-keeper-sidecar`. Experimental, gated by `useRpcKeeper` config flag (default `false`). Resolves typed extension slash commands (`/ctx-stats`, `/curator`, `/agents`, `/flows:*`) in headless dashboard sessions despite pi 0.74 `ExtensionAPI` exposing no `dispatchCommand`.
1227
+
1228
+ Per-session keeper process owns pi's stdin pipe. Server writes RPC lines to keeper via UDS (Unix) or named pipe (Windows). Keeper forwards verbatim to pi's stdin. Pi's `--mode rpc` reader runs `session.prompt(text, {expandPromptTemplates: true})` which dispatches slash commands.
1229
+
1230
+ Keeper outlives dashboard server restarts. Replaces Unix `tail -f /dev/null | pi` wrapper. Brings Windows to durability parity (today Windows loses pi on server death).
1231
+
1232
+ Three-process topology + dual-channel boundary:
1233
+
1234
+ ```mermaid
1235
+ flowchart LR
1236
+ S["dashboard server"]
1237
+ K["keeper.cjs<br/>(1 per session)"]
1238
+ P["pi --mode rpc"]
1239
+ B["bridge.ts<br/>(loaded inside pi)"]
1240
+
1241
+ S -->|"UDS /<sessionId>.rpc.sock<br/>(slash dispatch only)"| K
1242
+ K -->|"pi.stdin pipe<br/>(forward JSON lines)"| P
1243
+ P --- B
1244
+ B -->|"bridge WS<br/>(events, send_prompt non-slash, abort, model, etc.)"| S
1245
+ ```
1246
+
1247
+ UDS path: `~/.pi/dashboard/sessions/<sessionId>.rpc.sock`. Windows pipe: `\\.\pipe\pi-rpc-<sessionId>`. Keeper PID sidecar: `<sockPath>.pid`. Server scans on startup for orphan-cleanup + reattach.
1248
+
1249
+ Protocol: line-framed JSON, fire-and-forget. Server writes `{"type":"prompt","message":"/cmd","id":"<requestId>"}\n`. Keeper forwards raw line; no parsing, no response. Acknowledgement implicit (UDS write success).
1250
+
1251
+ Dual-channel boundary explicit:
1252
+ - **Bridge WS** owns: send_prompt non-slash, abort, model switch, thinking-level, compaction, rename, events, flow control.
1253
+ - **Server → keeper UDS** owns: extension slash dispatch only.
1254
+ - **headlessPidRegistry kill** owns: kill-by-pid for shutdown / force-kill / reload.
1255
+
1256
+ Bridge cannot reach `session.prompt` from inside pi 0.74. Server can (owns spawn + keeper). Routing slash dispatch through the channel that has the capability is correct given the constraint.
1257
+
1258
+ Lifecycle: pi exits → keeper exits 0, unlinks socket + pid sidecar. Keeper crashes → pi reads EOF on stdin → exits. Force-kill → server kills pi PID first, schedules 200 ms keeper-fallback SIGTERM. Tmux / Windows-Terminal sessions retain the existing `command_feedback {error}` stopgap (terminal owns pi's stdin, no UDS route).
1259
+
1182
1260
  ### Server Log Hygiene
1183
1261
 
1184
1262
  The daemon log at `~/.pi/dashboard/server.log` is opened in **append mode** (`"a"`) so crash output from prior start attempts survives subsequent retries — essential for diagnosing silent failures. Each attempt writes a timestamped header to distinguish runs:
@@ -1522,7 +1600,7 @@ Every external binary, module, and directory the dashboard depends on is resolve
1522
1600
  | Tool | Kind | Strategy chain |
1523
1601
  |---|---|---|
1524
1602
  | `pi` | binary | override → managed (`MANAGED_BIN/pi[.cmd]`) → where |
1525
- | `pi-coding-agent` | module | override → bare-import → managed (`MANAGED_DIR/node_modules/.../dist/index.js`) → npm-global; probes both `@mariozechner/*` and `@oh-my-pi/*` aliases |
1603
+ | `pi-coding-agent` | module | override → bare-import → managed (`MANAGED_DIR/node_modules/.../dist/index.js`) → npm-global; probes `@earendil-works/*` (primary) and `@mariozechner/*` (legacy) aliases |
1526
1604
  | `openspec`, `npm`, `node`, `tsx`, `git`, `zrok` | binary | override → managed → where |
1527
1605
  | `pi-dashboard` | module | override → managed → npm-global (presence of `package.json` is enough) |
1528
1606
  | `electron` | module | override → bare-import (`paths: ["packages/electron"]`) → managed; resolves the package directory containing `install.js`, hoist-aware. See change: register-build-time-tools |
@@ -1998,3 +2076,53 @@ Diagnostics MUST never crash the app. `doctor-core.ts` enforces this with three
1998
2076
  - **`assumedMandatory(label, fn, deps)`** — wraps "should-never-fail" ops (e.g. reading bundled-Node version, listing `~/.pi-dashboard/`). On throw it appends a structured entry to `<managedDir>/doctor.log` (1MB ring rotation) AND surfaces a row in the `Diagnostics` section so the user sees something went wrong instead of a silent gap. The log is opened from the toolbar (`Open doctor log`); the IPC handler returns `{exists:false}` when the file is absent so the renderer can show "no entries yet" instead of erroring.
1999
2077
 
2000
2078
  See change: `doctor-rich-output`.
2079
+
2080
+ ## Model Proxy
2081
+
2082
+ Dashboard-resident LLM proxy: `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/messages`.
2083
+
2084
+ ```mermaid
2085
+ sequenceDiagram
2086
+ participant C as External client<br/>(Honcho, LangChain, curl)
2087
+ participant D as Dashboard :8000/v1/*
2088
+ participant R as InternalRegistry
2089
+ participant P as Upstream provider<br/>(Anthropic, OpenAI, Google…)
2090
+
2091
+ C->>D: Authorization: Bearer pi-proxy-*
2092
+ D->>D: Auth gate: verify key, scope, backoff
2093
+ D->>R: getAvailable() / find(provider, model)
2094
+ R->>R: auth.json + providers.json + models.json
2095
+ D->>R: getApiKeyAndHeaders(model)
2096
+ R->>D: { apiKey, headers }
2097
+ D->>P: streamSimple(model, context, opts)
2098
+ P-->>D: SSE stream
2099
+ D-->>C: SSE stream (OpenAI or Anthropic shape)
2100
+ ```
2101
+
2102
+ ### API-key auth data flow
2103
+
2104
+ 1. Client sends `Authorization: Bearer pi-proxy-<48-char-base64url>`.
2105
+ 2. Auth gate (`model-proxy/auth-gate.ts`) prefix-checks `pi-proxy-`, looks up `sha256(token)` in `config.json#modelProxy.apiKeys[]`.
2106
+ 3. On hit: checks `revokedAt`, `expiresAt`, scope vs. route path.
2107
+ 4. On success: attaches `request.proxyApiKeyId`, resets per-IP backoff, debounced `lastUsedAt` write.
2108
+
2109
+ ### Refresh trigger map
2110
+
2111
+ | Trigger | Site |
2112
+ |---|---|
2113
+ | `PUT /api/providers` | `routes/provider-routes.ts` → `refreshModelRegistry()` |
2114
+ | OAuth callback completes | `routes/provider-auth-routes.ts` → `refreshModelRegistry()` |
2115
+ | Config save | `config-api.ts#writeConfigPartial` → `refreshModelRegistry()` |
2116
+ | Bridge `credentials_updated` | `event-wiring.ts` → `refreshModelRegistry()` |
2117
+ | `POST /api/model-proxy/refresh` | manual trigger (JWT-gated) |
2118
+
2119
+ ### auth.json write contract
2120
+
2121
+ Two writer processes for `~/.pi/agent/auth.json`:
2122
+
2123
+ - **Dashboard**: `provider-auth-storage.ts#writeCredential` (mkdir-based lock). Used by OAuth-flow completion routes AND `InternalAuthStorage` OAuth-refresh-on-expiry.
2124
+ - **Pi sessions**: `pi-coding-agent`'s `AuthStorage` (proper-lockfile). Runs in each connected pi session.
2125
+
2126
+ Last-writer-wins on overlapping provider keys; non-overlapping providers preserved by merge. Acceptable — both writers re-read before writing; churn only occurs during concurrent OAuth refreshes (rare in practice).
2127
+
2128
+ See change: `add-dashboard-model-proxy`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-agent-dashboard",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Web dashboard for monitoring and interacting with pi agent sessions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "main": "packages/server/src/cli.ts",
21
21
  "bin": {
22
- "pi-dashboard": "packages/server/src/cli.ts"
22
+ "pi-dashboard": "packages/server/bin/pi-dashboard.mjs"
23
23
  },
24
24
  "pi": {
25
25
  "extensions": [
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "files": [
33
33
  "packages/server/src/",
34
+ "packages/server/scripts/",
34
35
  "packages/server/package.json",
35
36
  "packages/server/tsconfig.json",
36
37
  "packages/shared/src/",
@@ -73,16 +74,15 @@
73
74
  "node": ">=22.12.0 <25"
74
75
  },
75
76
  "dependencies": {
76
- "@blackbelt-technology/pi-dashboard-extension": "^0.5.1",
77
- "@blackbelt-technology/pi-dashboard-server": "^0.5.1",
78
- "@blackbelt-technology/pi-dashboard-web": "^0.5.1"
77
+ "@blackbelt-technology/pi-dashboard-extension": "^0.5.2",
78
+ "@blackbelt-technology/pi-dashboard-server": "^0.5.2",
79
+ "@blackbelt-technology/pi-dashboard-web": "^0.5.2"
79
80
  },
80
81
  "optionalDependencies": {
81
82
  "appdmg": "^0.6.6"
82
83
  },
83
84
  "devDependencies": {
84
85
  "jsdom": "^29.0.2",
85
- "tsx": "^4.21.0",
86
86
  "typescript": "^5.7.0",
87
87
  "vitest": "^4.0.0"
88
88
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-extension",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Pi bridge extension for pi-dashboard",
5
5
  "type": "module",
6
6
  "repository": {
@@ -24,7 +24,7 @@
24
24
  ".pi/skills/pi-dashboard/"
25
25
  ],
26
26
  "dependencies": {
27
- "@blackbelt-technology/pi-dashboard-shared": "^0.5.1",
27
+ "@blackbelt-technology/pi-dashboard-shared": "^0.5.2",
28
28
  "ws": "^8.18.0"
29
29
  },
30
30
  "peerDependencies": {