@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,140 @@
1
+ /**
2
+ * GET /api/doctor — server-side diagnostic endpoint.
3
+ *
4
+ * Calls `runSharedChecks(...)` with server-appropriate `deps`, post-stamps
5
+ * `section` + `suggestion`, returns `{ checks, summary, generatedAt }`.
6
+ *
7
+ * Auth-gated identically to `/api/config`; unauthenticated requests yield
8
+ * the same status code as `/api/config` (the auth plugin's onRequest hook
9
+ * intercepts before this handler runs).
10
+ *
11
+ * On a thrown error from `runSharedChecks`, returns a 200 with a single
12
+ * fallback `error` row rather than a 500 — the web client always has
13
+ * something to render. See change: doctor-rich-output (tasks 4.1–4.5).
14
+ */
15
+ import type { FastifyInstance } from "fastify";
16
+ import path from "node:path";
17
+ import os from "node:os";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import {
20
+ runSharedChecks,
21
+ stampSectionsAndSuggestions,
22
+ safeExec,
23
+ type DoctorCheck,
24
+ type DoctorReport,
25
+ type SharedChecksDeps,
26
+ } from "@blackbelt-technology/pi-dashboard-shared/doctor-core.js";
27
+
28
+ function getManagedDir(): string {
29
+ return process.env.MANAGED_DIR || path.join(os.homedir(), ".pi-dashboard");
30
+ }
31
+
32
+ function detectSystemNode(): { found: boolean; path?: string } {
33
+ const cmd = process.platform === "win32" ? "where node" : "which node"; // platform-branch-ok: localised PATH-lookup primitive
34
+ const r = safeExec(cmd, { timeoutMs: 3000 });
35
+ if (!r.ok) return { found: false };
36
+ const first = r.stdout.trim().split("\n")[0];
37
+ return first ? { found: true, path: first } : { found: false };
38
+ }
39
+
40
+ function detectOnPath(name: string): { found: boolean; path?: string; source?: string } {
41
+ const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`; // platform-branch-ok: localised PATH-lookup primitive
42
+ const r = safeExec(cmd, { timeoutMs: 3000 });
43
+ if (!r.ok) return { found: false };
44
+ const first = r.stdout.trim().split("\n")[0];
45
+ return first ? { found: true, path: first, source: "system" } : { found: false };
46
+ }
47
+
48
+ function isApiKeyConfigured(): boolean {
49
+ try {
50
+ const settings = path.join(os.homedir(), ".pi", "agent", "settings.json");
51
+ if (!existsSync(settings)) return false;
52
+ const data = JSON.parse(readFileSync(settings, "utf-8"));
53
+ if (data?.anthropicApiKey || data?.openaiApiKey || data?.apiKey) return true;
54
+ if (data?.providers && typeof data.providers === "object") {
55
+ for (const v of Object.values(data.providers as Record<string, unknown>)) {
56
+ if (v && typeof v === "object" && "apiKey" in (v as object)) return true;
57
+ }
58
+ }
59
+ return false;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function buildDefaultDeps(): SharedChecksDeps {
66
+ return {
67
+ managedDir: getManagedDir(),
68
+ detectSystemNode,
69
+ detectPi: () => detectOnPath("pi"),
70
+ detectOpenSpec: () => detectOnPath("openspec"),
71
+ isApiKeyConfigured,
72
+ probeServer: async () => {
73
+ const r = safeExec("curl -sf http://localhost:8000/api/health", { timeoutMs: 3000 });
74
+ if (!r.ok || !r.stdout.trim()) return { running: false };
75
+ try {
76
+ const h = JSON.parse(r.stdout);
77
+ return {
78
+ running: true,
79
+ version: typeof h.version === "string" ? h.version : undefined,
80
+ mode: typeof h.mode === "string" ? h.mode : undefined,
81
+ starter: typeof h.starter === "string" ? h.starter : null,
82
+ installable:
83
+ h.installable && typeof h.installable === "object"
84
+ ? {
85
+ total: h.installable.total ?? 0,
86
+ installed: h.installable.installed ?? 0,
87
+ failed: Array.isArray(h.installable.failed) ? h.installable.failed : [],
88
+ }
89
+ : null,
90
+ };
91
+ } catch {
92
+ return { running: true };
93
+ }
94
+ },
95
+ };
96
+ }
97
+
98
+ function summarize(checks: DoctorCheck[]): DoctorReport["summary"] {
99
+ return {
100
+ ok: checks.filter((c) => c.status === "ok").length,
101
+ warnings: checks.filter((c) => c.status === "warning").length,
102
+ errors: checks.filter((c) => c.status === "error").length,
103
+ };
104
+ }
105
+
106
+ export interface DoctorRouteDeps {
107
+ /** Override for tests — substitutes a different `runSharedChecks` deps shape (or throws to exercise fault tolerance). */
108
+ buildDeps?: () => SharedChecksDeps;
109
+ }
110
+
111
+ export function registerDoctorRoutes(fastify: FastifyInstance, deps: DoctorRouteDeps = {}): void {
112
+ fastify.get("/api/doctor", async (_request, _reply): Promise<DoctorReport> => {
113
+ try {
114
+ const sharedDeps = deps.buildDeps ? deps.buildDeps() : buildDefaultDeps();
115
+ const checks = await runSharedChecks(sharedDeps);
116
+ stampSectionsAndSuggestions(checks);
117
+ return {
118
+ checks,
119
+ summary: summarize(checks),
120
+ generatedAt: Date.now(),
121
+ };
122
+ } catch (err) {
123
+ const e = err instanceof Error ? err : new Error(String(err));
124
+ const fallback: DoctorCheck = {
125
+ name: "Doctor failed to produce a report",
126
+ section: "diagnostics",
127
+ status: "error",
128
+ message: "Unexpected internal failure",
129
+ detail: `${e.message}\n${(e.stack || "").split("\n").slice(0, 4).join("\n")}`,
130
+ suggestion:
131
+ "Check `~/.pi-dashboard/doctor.log` on the server, then file an issue with the captured error.",
132
+ };
133
+ return {
134
+ checks: [fallback],
135
+ summary: { ok: 0, warnings: 0, errors: 1 },
136
+ generatedAt: Date.now(),
137
+ };
138
+ }
139
+ });
140
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * REST route for `GET /api/pi-core/changelog`.
3
+ *
4
+ * Returns the parsed `CHANGELOG.md` for a core package filtered to
5
+ * a `(from, to]` half-open version range, plus a derived `hasBreaking`
6
+ * flag and a public GitHub URL for the full changelog.
7
+ *
8
+ * Gated identically to other `/api/pi-core/*` routes via the shared
9
+ * bootstrap-status gate.
10
+ *
11
+ * See change: pi-update-whats-new-panel.
12
+ */
13
+ import type { FastifyInstance } from "fastify";
14
+ import type {
15
+ ChangelogResponse,
16
+ } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
17
+ import { CORE_PACKAGE_NAMES } from "../pi-core-checker.js";
18
+ import { parseVersion, compareVersions } from "../pi-version-skew.js";
19
+ import {
20
+ findChangelogPath,
21
+ readPackageJson,
22
+ deriveChangelogUrl,
23
+ } from "../changelog-fs.js";
24
+ import {
25
+ parseChangelog,
26
+ readAndParseChangelog,
27
+ getCachedRemoteChangelog,
28
+ setRemoteChangelog,
29
+ refreshRemoteChangelogTtl,
30
+ } from "../changelog-parser.js";
31
+ import {
32
+ deriveChangelogRawUrl,
33
+ fetchRemoteChangelog,
34
+ } from "../changelog-remote.js";
35
+ import type { ChangelogRelease } from "@blackbelt-technology/pi-dashboard-shared/changelog-types.js";
36
+ import type { BootstrapStateStore } from "../bootstrap-state.js";
37
+
38
+ export interface PiChangelogRouteDeps {
39
+ /** When provided, route returns 503 unless bootstrap status is "ready". */
40
+ bootstrapState?: BootstrapStateStore;
41
+ }
42
+
43
+ interface QueryShape {
44
+ pkg?: string;
45
+ from?: string;
46
+ to?: string;
47
+ }
48
+
49
+ export function registerPiChangelogRoutes(
50
+ fastify: FastifyInstance,
51
+ deps: PiChangelogRouteDeps,
52
+ ): void {
53
+ const { bootstrapState } = deps;
54
+
55
+ const bootstrapGate = async (
56
+ _req: unknown,
57
+ reply: { code: (n: number) => { send: (body: unknown) => unknown } },
58
+ ): Promise<unknown> => {
59
+ if (!bootstrapState) return undefined;
60
+ const status = bootstrapState.get().status;
61
+ if (status === "ready") return undefined;
62
+ return reply.code(503).send({
63
+ success: false,
64
+ error: `pi not yet installed (bootstrap status: ${status})`,
65
+ bootstrap: status,
66
+ });
67
+ };
68
+
69
+ fastify.get<{ Querystring: QueryShape }>(
70
+ "/api/pi-core/changelog",
71
+ { preHandler: bootstrapGate as any },
72
+ async (request, reply) => {
73
+ const pkg = (request.query.pkg ?? "").trim();
74
+ const from = (request.query.from ?? "").trim();
75
+ const to = (request.query.to ?? "").trim();
76
+
77
+ // Validate `pkg` against the core whitelist BEFORE touching
78
+ // the filesystem — prevents arbitrary path reads via crafted
79
+ // input.
80
+ if (!pkg || !CORE_PACKAGE_NAMES.includes(pkg)) {
81
+ return reply.code(400).send({
82
+ success: false,
83
+ error: `pkg must be one of: ${CORE_PACKAGE_NAMES.join(", ")}`,
84
+ });
85
+ }
86
+
87
+ // Validate version range using the existing parseVersion helper
88
+ // from pi-version-skew. Both endpoints are required.
89
+ if (!from || !to) {
90
+ return reply.code(400).send({
91
+ success: false,
92
+ error: "from and to query params are required",
93
+ });
94
+ }
95
+ if (!parseVersion(from) || !parseVersion(to)) {
96
+ return reply.code(400).send({
97
+ success: false,
98
+ error: "from and to must be parseable semver versions",
99
+ });
100
+ }
101
+
102
+ const located = findChangelogPath(pkg);
103
+
104
+ // Spec: package not installed / CHANGELOG missing → 200 with empty body.
105
+ if (!located) {
106
+ const body: ChangelogResponse = {
107
+ pkg,
108
+ from,
109
+ to,
110
+ releases: [],
111
+ hasBreaking: false,
112
+ changelogUrl: null,
113
+ parsedAt: new Date().toISOString(),
114
+ };
115
+ return body;
116
+ }
117
+
118
+ // Read package.json once for both URLs (raw for the parser,
119
+ // human for the dialog footer link).
120
+ const pkgJson = readPackageJson(located.packageDir);
121
+ const rawUrl = pkgJson ? deriveChangelogRawUrl(pkgJson.repository) : null;
122
+ const changelogUrl = pkgJson ? deriveChangelogUrl(pkgJson.repository) : null;
123
+
124
+ // Try remote first — the upstream CHANGELOG describes versions
125
+ // newer than the locally-installed tarball. Fall back to local
126
+ // on failure / offline / non-GitHub repo. See change:
127
+ // read-changelog-from-github.
128
+ let allReleases: ChangelogRelease[] | undefined;
129
+ let usedRemote = false;
130
+
131
+ if (rawUrl) {
132
+ const cached = getCachedRemoteChangelog(pkg);
133
+ if (cached && !cached.expired) {
134
+ // Within TTL — reuse cached result, no fetch.
135
+ allReleases = cached.releases;
136
+ usedRemote = true;
137
+ } else {
138
+ const fetchResult = await fetchRemoteChangelog(rawUrl, {
139
+ etag: cached?.etag ?? null,
140
+ });
141
+ if (fetchResult?.status === "ok") {
142
+ const parsed = parseChangelog(fetchResult.text);
143
+ setRemoteChangelog(pkg, parsed, fetchResult.etag);
144
+ allReleases = parsed;
145
+ usedRemote = true;
146
+ } else if (fetchResult?.status === "not-modified" && cached) {
147
+ refreshRemoteChangelogTtl(pkg);
148
+ allReleases = cached.releases;
149
+ usedRemote = true;
150
+ }
151
+ // null result (network error / offline / non-2xx): fall through
152
+ // to local read below.
153
+ }
154
+ }
155
+
156
+ if (!usedRemote) {
157
+ try {
158
+ allReleases = readAndParseChangelog(pkg, located.changelogPath);
159
+ } catch (err: any) {
160
+ request.log.warn(
161
+ { err: err?.message },
162
+ "[pi-changelog-routes] local read/parse failed; returning empty",
163
+ );
164
+ allReleases = [];
165
+ }
166
+ }
167
+
168
+ // Filter to (from, to]. Unparseable release versions are
169
+ // dropped — conservative.
170
+ const filtered = (allReleases ?? []).filter((r) => {
171
+ const rv = parseVersion(r.version);
172
+ if (!rv) return false;
173
+ return compareVersions(r.version, from) > 0 &&
174
+ compareVersions(r.version, to) <= 0;
175
+ });
176
+
177
+ const hasBreaking = filtered.some((r) => r.breaking.length > 0);
178
+
179
+ const body: ChangelogResponse = {
180
+ pkg,
181
+ from,
182
+ to,
183
+ releases: filtered,
184
+ hasBreaking,
185
+ changelogUrl,
186
+ parsedAt: new Date().toISOString(),
187
+ };
188
+ return body;
189
+ },
190
+ );
191
+ }
192
+
193
+ // Re-export for tests that need to bypass the cache.
194
+ export { parseChangelog };
@@ -5,7 +5,7 @@
5
5
  * POST /api/pi-core/update { packages?: string[] }
6
6
  *
7
7
  * Complements /api/packages/* (extension management): this endpoint covers
8
- * globally-installed pi CLI packages like @mariozechner/pi-coding-agent,
8
+ * globally-installed pi CLI packages like @earendil-works/pi-coding-agent,
9
9
  * pi-dashboard itself, pi-model-proxy, etc.
10
10
  */
11
11
  import type { FastifyInstance } from "fastify";
@@ -18,6 +18,7 @@ import {
18
18
  resolveAuthJsonKey,
19
19
  type ApiKeyCredential,
20
20
  } from "../provider-auth-storage.js";
21
+ import { getLatestCatalogue } from "../provider-catalogue-cache.js";
21
22
  import { startCallbackServer } from "../oauth-callback-server.js";
22
23
  import type { PiGateway } from "../pi-gateway.js";
23
24
  import type { BrowserGateway } from "../browser-gateway.js";
@@ -87,8 +88,12 @@ export function registerProviderAuthRoutes(
87
88
  const { piGateway, browserGateway } = deps;
88
89
 
89
90
  function notifyBridges() {
91
+ // Tell every bridge to reload auth.json + refresh its model registry.
92
+ // Each bridge will then push a fresh per-session models_list (and
93
+ // providers_list); browsers pick those up via the existing per-session
94
+ // broadcast and update modelsMap / catalogue cache without needing a
95
+ // global wipe. See change: simplify-model-selection-channels.
90
96
  piGateway.broadcast({ type: "credentials_updated" });
91
- browserGateway.broadcastToAll({ type: "models_refreshed" });
92
97
  }
93
98
 
94
99
  // List OAuth providers
@@ -98,6 +103,14 @@ export function registerProviderAuthRoutes(
98
103
 
99
104
  // Full status (OAuth + API key)
100
105
  fastify.get("/api/provider-auth/status", async () => {
106
+ // Cold-cache nudge: if no bridge has pushed a catalogue yet, ask
107
+ // every connected pi to send one. Best-effort, doesn't block this
108
+ // response. See change: replace-hardcoded-provider-lists.
109
+ if (getLatestCatalogue().length === 0) {
110
+ for (const sid of piGateway.getConnectedSessionIds()) {
111
+ piGateway.sendToSession(sid, { type: "request_providers", sessionId: sid });
112
+ }
113
+ }
101
114
  return getAuthStatus();
102
115
  });
103
116
 
@@ -98,13 +98,13 @@ export function registerProviderRoutes(fastify: FastifyInstance, deps: { network
98
98
  mkdirSync(dir, { recursive: true });
99
99
  writeFileSync(CONFIG_PATH, JSON.stringify(fileData, null, 2) + "\n", "utf-8");
100
100
 
101
- // Broadcast credentials_updated so all sessions refresh their model registries
101
+ // Broadcast credentials_updated so each bridge re-reads providers.json
102
+ // and pushes a fresh per-session models_list. Browsers receive those
103
+ // pushes via the existing per-session broadcast — no global wipe.
104
+ // See change: simplify-model-selection-channels.
102
105
  if (piGateway) {
103
106
  piGateway.broadcast({ type: "credentials_updated" });
104
107
  }
105
- if (deps.browserGateway) {
106
- deps.browserGateway.broadcastToAll({ type: "models_refreshed" });
107
- }
108
108
 
109
109
  return { success: true };
110
110
  },
@@ -19,8 +19,10 @@ import { spawn } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.j
19
19
  import path from "node:path";
20
20
  import os from "node:os";
21
21
  import { localhostGuard, netmaskToCidrBits, networkAddress } from "../localhost-guard.js";
22
+ import { readSpawnFailures } from "../spawn-failure-log.js";
22
23
  import { getPluginStatusStore } from "@blackbelt-technology/dashboard-plugin-runtime/server";
23
24
  import type { NetworkInterface } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
25
+ import type { BootstrapStateStore } from "../bootstrap-state.js";
24
26
 
25
27
  export function registerSystemRoutes(
26
28
  fastify: FastifyInstance,
@@ -33,9 +35,10 @@ export function registerSystemRoutes(
33
35
  version?: string;
34
36
  directoryService?: DirectoryService;
35
37
  piGateway?: PiGateway;
38
+ bootstrapState?: BootstrapStateStore;
36
39
  },
37
40
  ) {
38
- const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway } = deps;
41
+ const { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version, directoryService, piGateway, bootstrapState } = deps;
39
42
 
40
43
  // Quiesce windows for the bridge `server_restarting` broadcast. See change
41
44
  // `fix-restart-bridge-auto-start-race`. Bridges that receive this message
@@ -189,6 +192,8 @@ export function registerSystemRoutes(
189
192
  return {
190
193
  ok: true,
191
194
  pid: process.pid,
195
+ starter: bootstrapState?.get().starter ?? "Standalone",
196
+ installable: bootstrapState?.get().installable,
192
197
  version: version ?? "unknown",
193
198
  uptime: Math.floor((Date.now() - serverStartTime) / 1000),
194
199
  mode: config.dev ? "dev" : "production",
@@ -223,6 +228,26 @@ export function registerSystemRoutes(
223
228
  },
224
229
  );
225
230
 
231
+ // Re-extract endpoint — Electron-only; 403 for Bridge/Standalone, 202 for Electron.
232
+ // See change: simplify-electron-bootstrap-derived-state (task 6.4).
233
+ fastify.post(
234
+ "/api/electron/reextract",
235
+ { preHandler: networkGuard },
236
+ async (_request, reply) => {
237
+ const starter = bootstrapState?.get().starter ?? "Standalone";
238
+ if (starter !== "Electron") {
239
+ reply.status(403);
240
+ return {
241
+ error: "reextract_not_allowed",
242
+ message: `Re-extract is only available when the server was started by Electron (current starter: ${starter})`,
243
+ starter,
244
+ };
245
+ }
246
+ reply.status(202);
247
+ return { ok: true, message: "Re-extraction scheduled. Electron will restart the server." };
248
+ },
249
+ );
250
+
226
251
  // Restart endpoint — flush state, spawn new server, then exit
227
252
  fastify.post<{ Body: { dev?: boolean } }>(
228
253
  "/api/restart",
@@ -269,6 +294,18 @@ export function registerSystemRoutes(
269
294
  );
270
295
 
271
296
  // Network interfaces for trusted networks UI (localhost-only for security)
297
+ // GET /api/spawn-failures — rolling log of failed spawn attempts. See change: spawn-failure-diagnostics.
298
+ fastify.get<{ Querystring: { limit?: string } }>(
299
+ "/api/spawn-failures",
300
+ async (request) => {
301
+ const rawLimit = request.query.limit;
302
+ const parsed = rawLimit !== undefined ? parseInt(rawLimit, 10) : NaN;
303
+ const limit = Number.isNaN(parsed) ? 50 : parsed;
304
+ const entries = readSpawnFailures(limit);
305
+ return { entries };
306
+ },
307
+ );
308
+
272
309
  fastify.get(
273
310
  "/api/network-interfaces",
274
311
  { preHandler: localhostGuard },