@blackbelt-technology/pi-agent-dashboard 0.2.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 (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * REST routes for browser-based pi provider authentication.
3
+ */
4
+ import { exec } from "node:child_process";
5
+ import type { FastifyInstance } from "fastify";
6
+ import {
7
+ getProviderHandler,
8
+ generatePKCE,
9
+ generateState,
10
+ type AuthCodeHandler,
11
+ type DeviceCodeHandler,
12
+ type PKCEPair,
13
+ } from "../provider-auth-handlers.js";
14
+ import {
15
+ writeCredential,
16
+ removeCredential,
17
+ getAuthStatus,
18
+ getOAuthProvidersMeta,
19
+ resolveAuthJsonKey,
20
+ type ApiKeyCredential,
21
+ } from "../provider-auth-storage.js";
22
+ import { startCallbackServer } from "../oauth-callback-server.js";
23
+ import type { PiGateway } from "../pi-gateway.js";
24
+
25
+ // ── In-memory flow store (short-lived PKCE + device code state) ──────────────
26
+
27
+ interface AuthCodeFlow {
28
+ providerId: string;
29
+ pkce: PKCEPair;
30
+ state: string;
31
+ redirectUri: string;
32
+ createdAt: number;
33
+ }
34
+
35
+ interface DeviceCodeFlow {
36
+ providerId: string;
37
+ deviceCode: string;
38
+ interval: number;
39
+ expiresIn: number;
40
+ extra?: Record<string, unknown>;
41
+ status: "pending" | "complete" | "error" | "expired";
42
+ error?: string;
43
+ createdAt: number;
44
+ }
45
+
46
+ const authCodeFlows = new Map<string, AuthCodeFlow>();
47
+ const deviceCodeFlows = new Map<string, DeviceCodeFlow>();
48
+
49
+ // Expire flows after 10 minutes
50
+ const FLOW_TTL_MS = 10 * 60 * 1000;
51
+
52
+ function pruneFlows() {
53
+ const now = Date.now();
54
+ for (const [id, f] of authCodeFlows) {
55
+ if (now - f.createdAt > FLOW_TTL_MS) authCodeFlows.delete(id);
56
+ }
57
+ for (const [id, f] of deviceCodeFlows) {
58
+ if (now - f.createdAt > FLOW_TTL_MS) deviceCodeFlows.delete(id);
59
+ }
60
+ }
61
+
62
+ function makeFlowId(): string {
63
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
64
+ }
65
+
66
+ // ── Helpers ──────────────────────────────────────────────────────────────────
67
+
68
+ /** Open a URL in the system's default browser */
69
+ function openInBrowser(url: string): void {
70
+ const cmd = process.platform === "darwin"
71
+ ? `open ${JSON.stringify(url)}`
72
+ : process.platform === "win32"
73
+ ? `start "" ${JSON.stringify(url)}`
74
+ : `xdg-open ${JSON.stringify(url)}`;
75
+ exec(cmd, (err) => {
76
+ if (err) console.error("[provider-auth] Failed to open browser:", err.message);
77
+ });
78
+ }
79
+
80
+ // ── Route registration ───────────────────────────────────────────────────────
81
+
82
+ export function registerProviderAuthRoutes(
83
+ fastify: FastifyInstance,
84
+ deps: { piGateway: PiGateway },
85
+ ) {
86
+ const { piGateway } = deps;
87
+
88
+ function notifyBridges() {
89
+ piGateway.broadcast({ type: "credentials_updated" });
90
+ }
91
+
92
+ // List OAuth providers
93
+ fastify.get("/api/provider-auth/providers", async () => {
94
+ return getOAuthProvidersMeta();
95
+ });
96
+
97
+ // Full status (OAuth + API key)
98
+ fastify.get("/api/provider-auth/status", async () => {
99
+ return getAuthStatus();
100
+ });
101
+
102
+ // Start auth-code flow — opens system browser, starts temp callback server
103
+ fastify.post<{ Body: { provider: string } }>("/api/provider-auth/authorize", async (request, reply) => {
104
+ pruneFlows();
105
+ const { provider } = request.body ?? {};
106
+ const handler = getProviderHandler(provider);
107
+ if (!handler || handler.flowType !== "auth_code") {
108
+ return reply.code(400).send({ error: `Unknown auth-code provider: ${provider}` });
109
+ }
110
+ const h = handler as AuthCodeHandler;
111
+ const pkce = await generatePKCE();
112
+ const state = generateState();
113
+ const redirectUri = `http://localhost:${h.callbackPort}${h.callbackPath}`;
114
+ const authUrl = h.buildAuthUrl(redirectUri, state, pkce);
115
+ const flowId = makeFlowId();
116
+ authCodeFlows.set(flowId, { providerId: provider, pkce, state, redirectUri, createdAt: Date.now() });
117
+
118
+ // Start temp callback server to receive the OAuth redirect
119
+ try {
120
+ await startCallbackServer({
121
+ providerId: provider,
122
+ port: h.callbackPort,
123
+ path: h.callbackPath,
124
+ onCode: async (code, cbState) => {
125
+ const flow = Array.from(authCodeFlows.values()).find(
126
+ (f) => f.providerId === provider && f.state === (cbState || state),
127
+ );
128
+ if (!flow) throw new Error("Unknown or expired flow");
129
+ // Remove the flow
130
+ for (const [id, f] of authCodeFlows) {
131
+ if (f === flow) { authCodeFlows.delete(id); break; }
132
+ }
133
+ const credential = await h.exchangeCode(code, flow.redirectUri, flow.pkce, flow.state);
134
+ writeCredential(flow.providerId, credential);
135
+ notifyBridges();
136
+ },
137
+ });
138
+ } catch (err: any) {
139
+ return reply.code(500).send({ error: err.message });
140
+ }
141
+
142
+ // Open the auth URL in the system browser
143
+ openInBrowser(authUrl);
144
+
145
+ return { flowId, authUrl };
146
+ });
147
+
148
+ // Start device-code flow
149
+ fastify.post<{ Body: { provider: string; enterpriseDomain?: string } }>(
150
+ "/api/provider-auth/device-code",
151
+ async (request, reply) => {
152
+ pruneFlows();
153
+ const { provider, enterpriseDomain } = request.body ?? {};
154
+ const handler = getProviderHandler(provider);
155
+ if (!handler || handler.flowType !== "device_code") {
156
+ return reply.code(400).send({ error: `Unknown device-code provider: ${provider}` });
157
+ }
158
+ const h = handler as DeviceCodeHandler;
159
+ try {
160
+ const dc = await h.requestDeviceCode(enterpriseDomain);
161
+ const flowId = makeFlowId();
162
+ deviceCodeFlows.set(flowId, {
163
+ providerId: provider,
164
+ deviceCode: dc.deviceCode,
165
+ interval: dc.interval,
166
+ expiresIn: dc.expiresIn,
167
+ extra: dc.extra,
168
+ status: "pending",
169
+ createdAt: Date.now(),
170
+ });
171
+ // Start polling in background
172
+ pollDeviceCode(flowId, h).catch(() => {});
173
+ return {
174
+ flowId,
175
+ userCode: dc.userCode,
176
+ verificationUri: dc.verificationUri,
177
+ expiresIn: dc.expiresIn,
178
+ interval: dc.interval,
179
+ };
180
+ } catch (err: any) {
181
+ return reply.code(400).send({ error: err.message });
182
+ }
183
+ },
184
+ );
185
+
186
+ // Poll device-code status
187
+ fastify.get<{ Params: { flowId: string } }>(
188
+ "/api/provider-auth/device-status/:flowId",
189
+ async (request, reply) => {
190
+ const flow = deviceCodeFlows.get(request.params.flowId);
191
+ if (!flow) return reply.code(404).send({ error: "Unknown flow" });
192
+ return { status: flow.status, error: flow.error };
193
+ },
194
+ );
195
+
196
+ // Save API key
197
+ fastify.put<{ Body: { provider: string; key: string } }>(
198
+ "/api/provider-auth/api-key",
199
+ async (request, reply) => {
200
+ const { provider, key } = request.body ?? {};
201
+ if (!provider || !key) return reply.code(400).send({ error: "provider and key required" });
202
+ try {
203
+ // Resolve the authJsonKey for API key providers (e.g., "anthropic-api" → "anthropic")
204
+ const authJsonKey = resolveAuthJsonKey(provider);
205
+ const credential: ApiKeyCredential = { type: "api_key", key };
206
+ writeCredential(authJsonKey, credential);
207
+ notifyBridges();
208
+ return { ok: true };
209
+ } catch (err: any) {
210
+ request.log.error(err, "Failed to save API key");
211
+ return reply.code(500).send({ error: err.message || "Failed to save API key" });
212
+ }
213
+ },
214
+ );
215
+
216
+ // Remove credential
217
+ fastify.delete<{ Params: { provider: string } }>(
218
+ "/api/provider-auth/:provider",
219
+ async (request) => {
220
+ const authJsonKey = resolveAuthJsonKey(request.params.provider);
221
+ removeCredential(authJsonKey);
222
+ notifyBridges();
223
+ return { ok: true };
224
+ },
225
+ );
226
+
227
+ // ── Device-code background poller ──────────────────────────────────────────
228
+
229
+ async function pollDeviceCode(flowId: string, handler: DeviceCodeHandler) {
230
+ const flow = deviceCodeFlows.get(flowId);
231
+ if (!flow) return;
232
+ try {
233
+ const credential = await handler.pollForToken(
234
+ flow.deviceCode, flow.interval, flow.expiresIn, flow.extra,
235
+ );
236
+ writeCredential(flow.providerId, credential);
237
+ notifyBridges();
238
+ flow.status = "complete";
239
+ } catch (err: any) {
240
+ flow.status = err.message?.includes("expired") ? "expired" : "error";
241
+ flow.error = err.message;
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Provider REST API routes: read/write custom LLM providers (~/.pi/agent/providers.json).
3
+ */
4
+ import type { FastifyInstance } from "fastify";
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join, dirname } from "node:path";
8
+ import type { NetworkGuard } from "./route-deps.js";
9
+
10
+ const REDACTED = "***";
11
+ const CONFIG_PATH = join(homedir(), ".pi", "agent", "providers.json");
12
+
13
+ interface ProviderEntry {
14
+ baseUrl: string;
15
+ apiKey: string;
16
+ api?: string;
17
+ }
18
+
19
+ function readProvidersRaw(): Record<string, ProviderEntry> {
20
+ if (!existsSync(CONFIG_PATH)) return {};
21
+ try {
22
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
23
+ return raw.providers ?? {};
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+
29
+ function redactProviders(
30
+ providers: Record<string, ProviderEntry>,
31
+ ): Record<string, ProviderEntry> {
32
+ const redacted: Record<string, ProviderEntry> = {};
33
+ for (const [name, entry] of Object.entries(providers)) {
34
+ redacted[name] = {
35
+ ...entry,
36
+ apiKey:
37
+ entry.apiKey && entry.apiKey.startsWith("$")
38
+ ? entry.apiKey
39
+ : entry.apiKey
40
+ ? REDACTED
41
+ : "",
42
+ };
43
+ }
44
+ return redacted;
45
+ }
46
+
47
+ export function registerProviderRoutes(fastify: FastifyInstance, deps: { networkGuard: NetworkGuard }): void {
48
+ const { networkGuard } = deps;
49
+ fastify.get(
50
+ "/api/providers",
51
+ { preHandler: networkGuard },
52
+ async () => {
53
+ const providers = readProvidersRaw();
54
+ return { success: true, providers: redactProviders(providers) };
55
+ },
56
+ );
57
+
58
+ fastify.put(
59
+ "/api/providers",
60
+ { preHandler: networkGuard },
61
+ async (request, reply) => {
62
+ const body = request.body as Record<string, any> | null;
63
+ if (!body || typeof body !== "object" || !body.providers || typeof body.providers !== "object") {
64
+ return reply.code(400).send({ success: false, error: "Invalid body" });
65
+ }
66
+
67
+ const incoming = body.providers as Record<string, ProviderEntry>;
68
+ const existing = readProvidersRaw();
69
+
70
+ // Merge: preserve redacted apiKey values from existing file
71
+ const merged: Record<string, ProviderEntry> = {};
72
+ for (const [name, entry] of Object.entries(incoming)) {
73
+ merged[name] = {
74
+ baseUrl: entry.baseUrl,
75
+ apiKey:
76
+ entry.apiKey === REDACTED && existing[name]
77
+ ? existing[name].apiKey
78
+ : entry.apiKey,
79
+ api: entry.api,
80
+ };
81
+ }
82
+
83
+ // Read raw file to preserve any non-providers fields
84
+ let fileData: Record<string, any> = {};
85
+ if (existsSync(CONFIG_PATH)) {
86
+ try {
87
+ fileData = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
88
+ } catch {
89
+ // start fresh
90
+ }
91
+ }
92
+ fileData.providers = merged;
93
+
94
+ const dir = dirname(CONFIG_PATH);
95
+ mkdirSync(dir, { recursive: true });
96
+ writeFileSync(CONFIG_PATH, JSON.stringify(fileData, null, 2) + "\n", "utf-8");
97
+
98
+ return { success: true };
99
+ },
100
+ );
101
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared dependency types for route modules.
3
+ * Each route module receives only the deps it needs.
4
+ */
5
+ import type { SessionManager } from "../memory-session-manager.js";
6
+ import type { EventStore } from "../memory-event-store.js";
7
+ import type { PreferencesStore } from "../preferences-store.js";
8
+ import type { MetaPersistence } from "../meta-persistence.js";
9
+ import type { DirectoryService } from "../directory-service.js";
10
+ import type { ServerConfig } from "../server.js";
11
+ import type { FastifyRequest, FastifyReply } from "fastify";
12
+
13
+ export type NetworkGuard = (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
14
+
15
+ export interface RouteDeps {
16
+ sessionManager: SessionManager;
17
+ eventStore: EventStore;
18
+ preferencesStore: PreferencesStore;
19
+ metaPersistence: MetaPersistence;
20
+ directoryService: DirectoryService;
21
+ config: ServerConfig;
22
+ networkGuard: NetworkGuard;
23
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Session-related REST API routes.
3
+ */
4
+ import { readFile } from "node:fs/promises";
5
+ import { resolve, relative, isAbsolute } from "node:path";
6
+ import type { FastifyInstance } from "fastify";
7
+ import type { SessionManager } from "../memory-session-manager.js";
8
+ import type { EventStore } from "../memory-event-store.js";
9
+ import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
10
+ import type { NetworkGuard } from "./route-deps.js";
11
+ import { extractFileChanges, enrichWithGitDiff } from "../session-diff.js";
12
+
13
+ export function registerSessionRoutes(
14
+ fastify: FastifyInstance,
15
+ deps: {
16
+ sessionManager: SessionManager;
17
+ eventStore: EventStore;
18
+ networkGuard: NetworkGuard;
19
+ },
20
+ ) {
21
+ const { sessionManager, eventStore, networkGuard } = deps;
22
+
23
+ fastify.get("/api/sessions", async () => {
24
+ const sessions = sessionManager.listAll();
25
+ return { success: true, data: sessions } satisfies ApiResponse;
26
+ });
27
+
28
+ fastify.get<{ Params: { sessionId: string; seq: string } }>(
29
+ "/api/events/:sessionId/:seq",
30
+ async (request) => {
31
+ const { sessionId, seq } = request.params;
32
+ const event = eventStore.getEvent(sessionId, parseInt(seq, 10));
33
+ if (!event) {
34
+ return { success: false, error: "Event not found" } satisfies ApiResponse;
35
+ }
36
+ return { success: true, data: event } satisfies ApiResponse;
37
+ },
38
+ );
39
+
40
+ // Session file diff endpoint (localhost-only)
41
+ fastify.get<{ Querystring: { sessionId?: string } }>(
42
+ "/api/session-diff",
43
+ { preHandler: networkGuard },
44
+ async (request) => {
45
+ const { sessionId } = request.query;
46
+ if (!sessionId) {
47
+ return { success: false, error: "sessionId required" } satisfies ApiResponse;
48
+ }
49
+ const session = sessionManager.get(sessionId);
50
+ if (!session) {
51
+ return { success: false, error: "session not found" } satisfies ApiResponse;
52
+ }
53
+ const events = eventStore.getEvents(sessionId, 0).map((e) => e.event);
54
+ const files = extractFileChanges(events, session.cwd);
55
+ const { enrichedFiles, isGitRepo: isGit } = enrichWithGitDiff(session.cwd, files);
56
+ return { success: true, data: { files: enrichedFiles, isGitRepo: isGit } } satisfies ApiResponse;
57
+ },
58
+ );
59
+
60
+ // Read a file within a session's cwd (localhost-only)
61
+ fastify.get<{ Querystring: { sessionId?: string; path?: string } }>(
62
+ "/api/session-file",
63
+ { preHandler: networkGuard },
64
+ async (request, reply) => {
65
+ const { sessionId, path: filePath } = request.query;
66
+ if (!sessionId || !filePath) {
67
+ reply.code(400);
68
+ return { success: false, error: "sessionId and path required" } satisfies ApiResponse;
69
+ }
70
+ const session = sessionManager.get(sessionId);
71
+ if (!session) {
72
+ reply.code(404);
73
+ return { success: false, error: "session not found" } satisfies ApiResponse;
74
+ }
75
+ // Resolve and ensure path is within cwd
76
+ const absPath = isAbsolute(filePath) ? filePath : resolve(session.cwd, filePath);
77
+ const rel = relative(session.cwd, absPath);
78
+ if (rel.startsWith("..") || isAbsolute(rel)) {
79
+ reply.code(403);
80
+ return { success: false, error: "path outside session directory" } satisfies ApiResponse;
81
+ }
82
+ try {
83
+ const content = await readFile(absPath, "utf-8");
84
+ return { success: true, data: { content } } satisfies ApiResponse;
85
+ } catch {
86
+ reply.code(404);
87
+ return { success: false, error: "file not found" } satisfies ApiResponse;
88
+ }
89
+ },
90
+ );
91
+ }