@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,255 @@
1
+ /**
2
+ * REST API endpoint types.
3
+ */
4
+ import type {
5
+ DashboardSession,
6
+ DashboardEvent,
7
+ ApiResponse,
8
+ } from "./types.js";
9
+
10
+ // ── Sessions ────────────────────────────────────────────────────────
11
+
12
+ export interface ListSessionsQuery {
13
+ status?: "active" | "ended";
14
+ }
15
+
16
+ export type ListSessionsResponse = ApiResponse<DashboardSession[]>;
17
+
18
+ // ── Events ──────────────────────────────────────────────────────────
19
+
20
+ export type FetchEventContentResponse = ApiResponse<DashboardEvent>;
21
+
22
+ // ── Session Spawn ───────────────────────────────────────────────────
23
+
24
+ export interface SpawnSessionRequest {
25
+ cwd: string;
26
+ }
27
+
28
+ export type SpawnSessionResponse = ApiResponse<{ message: string }>;
29
+
30
+ // ── Aggregate Stats ─────────────────────────────────────────────────
31
+
32
+ export interface AggregateStats {
33
+ activeSessions: number;
34
+ totalTokensIn: number;
35
+ totalTokensOut: number;
36
+ totalCost: number;
37
+ }
38
+
39
+ export type AggregateStatsResponse = ApiResponse<AggregateStats>;
40
+
41
+ // ── File Read ───────────────────────────────────────────────────────
42
+
43
+ export interface FileContentResult {
44
+ type: "file";
45
+ content: string;
46
+ }
47
+
48
+ export interface DirectoryListResult {
49
+ type: "directory";
50
+ entries: string[];
51
+ }
52
+
53
+ export type FileReadResult = FileContentResult | DirectoryListResult;
54
+
55
+ export type FileReadResponse = ApiResponse<FileReadResult>;
56
+
57
+ // ── Browse ──────────────────────────────────────────────────────────
58
+
59
+ export interface BrowseEntry {
60
+ name: string;
61
+ path: string;
62
+ isGit: boolean;
63
+ isPi: boolean;
64
+ }
65
+
66
+ export interface BrowseResult {
67
+ entries: BrowseEntry[];
68
+ parent: string | null;
69
+ current: string;
70
+ }
71
+
72
+ export type BrowseResponse = ApiResponse<BrowseResult>;
73
+
74
+ // ── Tunnel Status ───────────────────────────────────────────────────
75
+
76
+ export type TunnelStatus =
77
+ | { status: "active"; url: string; serverOs: string }
78
+ | { status: "inactive"; serverOs: string }
79
+ | { status: "unavailable"; serverOs: string };
80
+
81
+ export type TunnelStatusResponse = ApiResponse<TunnelStatus>;
82
+
83
+ // ── Pi Resources ────────────────────────────────────────────────────
84
+
85
+ export interface PiResource {
86
+ name: string;
87
+ description?: string;
88
+ filePath: string;
89
+ type: "extension" | "skill" | "prompt";
90
+ }
91
+
92
+ export interface PiResourceScope {
93
+ extensions: PiResource[];
94
+ skills: PiResource[];
95
+ prompts: PiResource[];
96
+ }
97
+
98
+ export interface PiPackageInfo {
99
+ name: string;
100
+ description?: string;
101
+ source: string; // e.g. "npm:pi-web-access", "git:github.com/user/repo", "../relative"
102
+ resources: PiResourceScope;
103
+ /** Which scope this package was resolved from */
104
+ scope?: "local" | "global";
105
+ }
106
+
107
+ export interface PiResourcesResult {
108
+ local: PiResourceScope;
109
+ global: PiResourceScope;
110
+ packages: PiPackageInfo[];
111
+ }
112
+
113
+ export type PiResourcesResponse = ApiResponse<PiResourcesResult>;
114
+
115
+ // ── Git Operations ──────────────────────────────────────────────────
116
+
117
+ export interface GitBranchEntry {
118
+ name: string;
119
+ isRemote: boolean;
120
+ isCurrent: boolean;
121
+ }
122
+
123
+ export interface GitBranchesResult {
124
+ current: string;
125
+ detached: boolean;
126
+ branches: GitBranchEntry[];
127
+ }
128
+
129
+ export type GitBranchesResponse = ApiResponse<GitBranchesResult>;
130
+
131
+ export interface GitCheckoutRequest {
132
+ cwd: string;
133
+ branch: string;
134
+ stash?: boolean;
135
+ }
136
+
137
+ export type GitCheckoutResponse =
138
+ | ApiResponse<{ stashed?: boolean }>
139
+ | ApiResponse<never> & { success: false; dirty: true; files: string[] };
140
+
141
+ export interface GitInitRequest {
142
+ cwd: string;
143
+ }
144
+
145
+ export type GitInitResponse = ApiResponse<void>;
146
+
147
+ export interface GitStashPopResult {
148
+ conflicts: boolean;
149
+ }
150
+
151
+ export type GitStashPopResponse = ApiResponse<GitStashPopResult>;
152
+
153
+ // ── Provider Auth ─────────────────────────────────────────────────────────
154
+
155
+ export interface ProviderAuthInfo {
156
+ id: string;
157
+ name: string;
158
+ flowType: "auth_code" | "device_code";
159
+ }
160
+
161
+ export interface ProviderAuthStatus {
162
+ id: string;
163
+ name: string;
164
+ flowType: "auth_code" | "device_code" | "api_key";
165
+ authenticated: boolean;
166
+ expires?: number;
167
+ maskedKey?: string;
168
+ }
169
+
170
+ export interface AuthorizeResponse {
171
+ flowId: string;
172
+ authUrl: string;
173
+ }
174
+
175
+ export interface DeviceCodeResponse {
176
+ flowId: string;
177
+ userCode: string;
178
+ verificationUri: string;
179
+ expiresIn: number;
180
+ interval: number;
181
+ }
182
+
183
+ // ── Package Management ──────────────────────────────────────────────
184
+
185
+ /** A single result from the npm registry search. */
186
+ export interface NpmPackageResult {
187
+ name: string;
188
+ description?: string;
189
+ version: string;
190
+ keywords: string[];
191
+ date: string;
192
+ publisher?: { username: string; email?: string };
193
+ links?: { npm?: string; homepage?: string; repository?: string };
194
+ downloads?: { weekly: number; monthly: number };
195
+ /** Derived from keywords: extension, skill, theme, prompt */
196
+ types: string[];
197
+ }
198
+
199
+ export interface NpmSearchResponse {
200
+ packages: NpmPackageResult[];
201
+ total: number;
202
+ }
203
+
204
+ export type NpmSearchApiResponse = ApiResponse<NpmSearchResponse>;
205
+
206
+ export interface NpmReadmeResponse {
207
+ readme: string;
208
+ name: string;
209
+ version: string;
210
+ }
211
+
212
+ export type NpmReadmeApiResponse = ApiResponse<NpmReadmeResponse>;
213
+
214
+ /** An installed pi package as returned by the list endpoint. */
215
+ export interface InstalledPackage {
216
+ source: string;
217
+ scope: "user" | "project";
218
+ filtered: boolean;
219
+ installedPath?: string;
220
+ /** Set after check-updates: true if newer version available */
221
+ updateAvailable?: boolean;
222
+ }
223
+
224
+ export type InstalledPackagesResponse = ApiResponse<InstalledPackage[]>;
225
+
226
+ /** Request body for install / remove / update operations. */
227
+ export interface PackageOperationRequest {
228
+ source: string;
229
+ scope: "global" | "local";
230
+ cwd?: string;
231
+ }
232
+
233
+ /** Response returned immediately (202) when an operation starts. */
234
+ export interface PackageOperationResponse {
235
+ operationId: string;
236
+ }
237
+
238
+ export type PackageOperationApiResponse = ApiResponse<PackageOperationResponse>;
239
+
240
+ /** Result of check-updates. */
241
+ export interface PackageUpdateInfo {
242
+ source: string;
243
+ displayName: string;
244
+ type: "npm" | "git";
245
+ }
246
+
247
+ export type CheckUpdatesResponse = ApiResponse<PackageUpdateInfo[]>;
248
+
249
+ /** Detected network interface for trusted networks UI. */
250
+ export interface NetworkInterface {
251
+ name: string;
252
+ address: string;
253
+ netmask: string;
254
+ cidr: string;
255
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Server identity verification via HTTP health check.
3
+ * Replaces bare TCP port probes with identity-verified dashboard detection.
4
+ */
5
+
6
+ const HEALTH_TIMEOUT = 2000;
7
+
8
+ export interface DashboardStatus {
9
+ /** Whether the dashboard server is running on this port */
10
+ running: boolean;
11
+ /** PID of the running server (if detected) */
12
+ pid?: number;
13
+ /** Port is occupied by a non-dashboard service */
14
+ portConflict?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Check if a dashboard server is running on the given port by hitting GET /api/health.
19
+ * Returns identity-verified status instead of just "port is open".
20
+ */
21
+ export async function isDashboardRunning(port: number, host = "localhost"): Promise<DashboardStatus> {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT);
24
+
25
+ try {
26
+ const res = await fetch(`http://${host}:${port}/api/health`, {
27
+ signal: controller.signal,
28
+ });
29
+ clearTimeout(timer);
30
+
31
+ if (!res.ok) {
32
+ return { running: false, portConflict: true };
33
+ }
34
+
35
+ const data = await res.json() as Record<string, unknown>;
36
+ if (data && data.ok === true && typeof data.pid === "number") {
37
+ return { running: true, pid: data.pid };
38
+ }
39
+
40
+ // HTTP 200 but not our format — another service
41
+ return { running: false, portConflict: true };
42
+ } catch (err: unknown) {
43
+ clearTimeout(timer);
44
+ // Connection refused or timeout — nothing running
45
+ if (err instanceof Error && err.name === "AbortError") {
46
+ return { running: false };
47
+ }
48
+ // Could be ECONNREFUSED or other network error
49
+ return { running: false };
50
+ }
51
+ }
@@ -0,0 +1,86 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Session metadata stored as a sidecar `.meta.json` file
6
+ * next to the session's `.jsonl` file.
7
+ *
8
+ * Contains dashboard-owned per-session state and cached stats.
9
+ * All fields are optional — a minimal `{ source: "dashboard" }` is valid.
10
+ */
11
+ export interface SessionMeta {
12
+ // Dashboard-owned (user-set via UI)
13
+ source?: string;
14
+ name?: string;
15
+ attachedProposal?: string | null;
16
+ hidden?: boolean;
17
+
18
+ // Cached identity & state (from .jsonl header / bridge)
19
+ cwd?: string;
20
+ status?: string;
21
+ startedAt?: number;
22
+ endedAt?: number;
23
+ firstMessage?: string;
24
+
25
+ // Cached stats (extracted from .jsonl, avoids re-parsing)
26
+ model?: string;
27
+ thinkingLevel?: string;
28
+ tokensIn?: number;
29
+ tokensOut?: number;
30
+ cacheRead?: number;
31
+ cacheWrite?: number;
32
+ cost?: number;
33
+ contextTokens?: number;
34
+ contextWindow?: number;
35
+
36
+ // Cache freshness — compared against .jsonl mtime
37
+ cachedAt?: number;
38
+ }
39
+
40
+ /**
41
+ * Derive the `.meta.json` path from a `.jsonl` session file path.
42
+ */
43
+ export function metaPath(sessionFile: string): string {
44
+ const dir = path.dirname(sessionFile);
45
+ const base = path.basename(sessionFile, ".jsonl");
46
+ return path.join(dir, `${base}.meta.json`);
47
+ }
48
+
49
+ /**
50
+ * Read session metadata from the sidecar file.
51
+ * Returns undefined if the file doesn't exist or is invalid.
52
+ */
53
+ export function readSessionMeta(sessionFile: string): SessionMeta | undefined {
54
+ try {
55
+ const content = fs.readFileSync(metaPath(sessionFile), "utf-8");
56
+ return JSON.parse(content) as SessionMeta;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Write session metadata to the sidecar file.
64
+ * Creates parent directories if needed.
65
+ * Uses atomic write (write-to-tmp + rename) to prevent corruption.
66
+ */
67
+ export function writeSessionMeta(sessionFile: string, meta: SessionMeta): void {
68
+ const p = metaPath(sessionFile);
69
+ const dir = path.dirname(p);
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ const tmpPath = p + ".tmp";
72
+ fs.writeFileSync(tmpPath, JSON.stringify(meta, null, 2) + "\n");
73
+ fs.renameSync(tmpPath, p);
74
+ }
75
+
76
+ /**
77
+ * Merge new fields into an existing `.meta.json` sidecar.
78
+ * Reads the existing file, merges with the provided partial,
79
+ * and writes atomically. Fields in `partial` overwrite existing ones.
80
+ * Preserves any unknown fields already in the file.
81
+ */
82
+ export function mergeSessionMeta(sessionFile: string, partial: Partial<SessionMeta>): void {
83
+ const existing = readSessionMeta(sessionFile) ?? {};
84
+ const merged = { ...existing, ...partial };
85
+ writeSessionMeta(sessionFile, merged);
86
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * State replay — synthesizes dashboard events from pi session entries
3
+ * so the browser can rebuild the chat view after a reconnect or DB reset.
4
+ */
5
+ import type { EventForwardMessage } from "./protocol.js";
6
+
7
+ /**
8
+ * Convert pi session entries (from ctx.sessionManager.getBranch())
9
+ * into dashboard event_forward messages that the event reducer can process.
10
+ *
11
+ * Only generates the minimal events needed to rebuild the chat view:
12
+ * - message_start for user messages
13
+ * - message_update + message_end for assistant messages
14
+ * - tool_execution_start / tool_execution_end for tool calls
15
+ * - model_select for model changes
16
+ */
17
+ export function replayEntriesAsEvents(
18
+ sessionId: string,
19
+ entries: any[],
20
+ ): EventForwardMessage[] {
21
+ const messages: EventForwardMessage[] = [];
22
+ const openToolCalls = new Set<string>(); // track tool calls without results
23
+
24
+ let currentModel = "";
25
+
26
+ for (const entry of entries) {
27
+ if (!entry || !entry.type) continue;
28
+ const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
29
+
30
+ if (entry.type === "model_change") {
31
+ currentModel = entry.modelId ?? "";
32
+ }
33
+
34
+ if (entry.type === "message" && entry.message) {
35
+ const msg = entry.message;
36
+
37
+ if (msg.role === "user") {
38
+ messages.push(makeEvent(sessionId, "message_start", ts, { message: msg, entryId: entry.id }));
39
+ }
40
+
41
+ if (msg.role === "assistant") {
42
+ const content = Array.isArray(msg.content) ? msg.content : [];
43
+ // Emit tool_execution_start for each tool call
44
+ for (const part of content) {
45
+ if (part.type === "toolCall") {
46
+ messages.push(makeEvent(sessionId, "tool_execution_start", ts, {
47
+ toolCallId: part.id,
48
+ toolName: part.name,
49
+ args: typeof part.arguments === "string"
50
+ ? tryParseJson(part.arguments)
51
+ : part.arguments,
52
+ }));
53
+ openToolCalls.add(part.id);
54
+ }
55
+ }
56
+ // Emit message_update (sets streamingText) then message_end (finalizes)
57
+ messages.push(makeEvent(sessionId, "message_update", ts, { message: msg }));
58
+ messages.push(makeEvent(sessionId, "message_end", ts, { message: msg, entryId: entry.id }));
59
+
60
+ // Emit stats_update if usage data is present
61
+ const usage = msg.usage as Record<string, unknown> | undefined;
62
+ if (usage) {
63
+ const cost = usage.cost as Record<string, number> | undefined;
64
+ const totalTokens = usage.totalTokens as number | undefined;
65
+ const statsData: Record<string, unknown> = {
66
+ tokensIn: (usage.input as number) ?? 0,
67
+ tokensOut: (usage.output as number) ?? 0,
68
+ cost: cost?.total ?? 0,
69
+ turnUsage: {
70
+ input: (usage.input as number) ?? 0,
71
+ output: (usage.output as number) ?? 0,
72
+ cacheRead: (usage.cacheRead as number) ?? 0,
73
+ cacheWrite: (usage.cacheWrite as number) ?? 0,
74
+ },
75
+ };
76
+ // Include context usage estimate from totalTokens
77
+ if (totalTokens && totalTokens > 0) {
78
+ statsData.contextUsage = {
79
+ tokens: totalTokens,
80
+ contextWindow: inferContextWindow(currentModel),
81
+ };
82
+ }
83
+ messages.push(makeEvent(sessionId, "stats_update", ts, statsData));
84
+ }
85
+ }
86
+
87
+ // Tool results: toolCallId and toolName are at the message level
88
+ // Structure: { role: "toolResult", toolCallId, toolName, content: [{type:"text",text:"..."}], isError }
89
+ if (msg.role === "toolResult" && msg.toolCallId) {
90
+ const resultText = Array.isArray(msg.content)
91
+ ? msg.content
92
+ .filter((c: any) => c.type === "text")
93
+ .map((c: any) => c.text)
94
+ .join("")
95
+ : typeof msg.content === "string" ? msg.content : "";
96
+ // Extract image content blocks if present
97
+ const imageBlocks = Array.isArray(msg.content)
98
+ ? msg.content.filter((c: any) => c.type === "image" && c.data && c.mimeType)
99
+ : [];
100
+ const eventData: Record<string, unknown> = {
101
+ toolCallId: msg.toolCallId,
102
+ toolName: msg.toolName ?? "unknown",
103
+ result: resultText,
104
+ isError: msg.isError ?? false,
105
+ };
106
+ if (imageBlocks.length > 0) {
107
+ eventData.images = imageBlocks.map((c: any) => ({ data: c.data, mimeType: c.mimeType }));
108
+ }
109
+ // Include tool details (e.g. AgentDetails from pi-subagents) if present
110
+ if (msg.details && typeof msg.details === "object") {
111
+ eventData.details = msg.details;
112
+ }
113
+ messages.push(makeEvent(sessionId, "tool_execution_end", ts, eventData));
114
+ openToolCalls.delete(msg.toolCallId);
115
+ }
116
+ }
117
+
118
+ if (entry.type === "model_change") {
119
+ messages.push(makeEvent(sessionId, "model_select", ts, {
120
+ type: "model_select",
121
+ model: { provider: entry.provider, id: entry.modelId },
122
+ }));
123
+ }
124
+ }
125
+
126
+ // Close any orphaned tool calls (agent killed mid-execution)
127
+ for (const toolCallId of openToolCalls) {
128
+ const startEvent = messages.find(
129
+ (m) => m.event.eventType === "tool_execution_start" && (m.event.data as any).toolCallId === toolCallId,
130
+ );
131
+ const ts = startEvent ? startEvent.event.timestamp : Date.now();
132
+ messages.push(makeEvent(sessionId, "tool_execution_end", ts, {
133
+ toolCallId,
134
+ toolName: (startEvent?.event.data as any)?.toolName ?? "unknown",
135
+ result: "",
136
+ isError: false,
137
+ }));
138
+ }
139
+
140
+ return messages;
141
+ }
142
+
143
+ function makeEvent(
144
+ sessionId: string,
145
+ eventType: string,
146
+ timestamp: number,
147
+ data: Record<string, unknown>,
148
+ ): EventForwardMessage {
149
+ return {
150
+ type: "event_forward",
151
+ sessionId,
152
+ event: {
153
+ eventType,
154
+ timestamp,
155
+ data: { type: eventType, ...data },
156
+ },
157
+ };
158
+ }
159
+
160
+ function tryParseJson(s: string): Record<string, unknown> {
161
+ try { return JSON.parse(s); } catch { return {}; }
162
+ }
163
+
164
+ /** Infer context window size from model ID */
165
+ function inferContextWindow(modelId: string): number {
166
+ const id = modelId.toLowerCase();
167
+ if (id.includes("claude") && (id.includes("opus") || id.includes("sonnet") || id.includes("haiku"))) return 200_000;
168
+ if (id.includes("gpt-4o")) return 128_000;
169
+ if (id.includes("gpt-4")) return 128_000;
170
+ if (id.includes("o1") || id.includes("o3") || id.includes("o4")) return 200_000;
171
+ if (id.includes("gemini")) return 1_000_000;
172
+ if (id.includes("deepseek")) return 128_000;
173
+ return 200_000; // safe default
174
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Extract token stats from a turn_end event.
3
+ * Reads usage from event.message.usage (the pi SDK's TurnEndEvent shape).
4
+ */
5
+
6
+ export interface StatsData {
7
+ tokensIn: number;
8
+ tokensOut: number;
9
+ cost: number;
10
+ turnUsage: {
11
+ input: number;
12
+ output: number;
13
+ cacheRead: number;
14
+ cacheWrite: number;
15
+ };
16
+ contextUsage?: {
17
+ tokens: number | null;
18
+ contextWindow: number;
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Extract stats from a turn_end event and context usage.
24
+ * Returns null if the event has no usage data.
25
+ */
26
+ export function extractTurnStats(
27
+ event: Record<string, unknown>,
28
+ contextUsage?: { tokens: number | null; contextWindow: number },
29
+ ): StatsData | null {
30
+ const message = event.message as Record<string, unknown> | undefined;
31
+ const usage = message?.usage as Record<string, unknown> | undefined;
32
+
33
+ if (!usage) return null;
34
+
35
+ const cost = usage.cost as Record<string, number> | undefined;
36
+
37
+ const stats: StatsData = {
38
+ tokensIn: (usage.input as number) ?? 0,
39
+ tokensOut: (usage.output as number) ?? 0,
40
+ cost: cost?.total ?? 0,
41
+ turnUsage: {
42
+ input: (usage.input as number) ?? 0,
43
+ output: (usage.output as number) ?? 0,
44
+ cacheRead: (usage.cacheRead as number) ?? 0,
45
+ cacheWrite: (usage.cacheWrite as number) ?? 0,
46
+ },
47
+ };
48
+
49
+ if (contextUsage) {
50
+ stats.contextUsage = contextUsage;
51
+ }
52
+
53
+ return stats;
54
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared types for the terminal emulator feature.
3
+ */
4
+
5
+ export interface TerminalSession {
6
+ id: string;
7
+ cwd: string;
8
+ shell: string;
9
+ status: "active" | "ended";
10
+ title?: string;
11
+ manuallyRenamed?: boolean;
12
+ createdAt: number;
13
+ }
14
+
15
+ /** Control messages sent as text frames on the terminal WebSocket. */
16
+ export type TerminalControlMessage =
17
+ | { type: "resize"; cols: number; rows: number }
18
+ | { type: "title"; title: string };