context-mode 1.0.162 → 1.0.163

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +142 -28
  7. package/bin/statusline.mjs +24 -4
  8. package/build/adapters/antigravity/index.d.ts +1 -1
  9. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  10. package/build/adapters/antigravity-cli/index.js +341 -0
  11. package/build/adapters/claude-code/hooks.d.ts +1 -0
  12. package/build/adapters/claude-code/hooks.js +3 -0
  13. package/build/adapters/claude-code/index.js +24 -5
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +5 -1
  16. package/build/adapters/codex/hooks.js +5 -1
  17. package/build/adapters/codex/index.d.ts +9 -1
  18. package/build/adapters/codex/index.js +87 -5
  19. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  20. package/build/adapters/copilot-cli/hooks.js +64 -0
  21. package/build/adapters/copilot-cli/index.d.ts +48 -0
  22. package/build/adapters/copilot-cli/index.js +341 -0
  23. package/build/adapters/detect.d.ts +1 -1
  24. package/build/adapters/detect.js +71 -3
  25. package/build/adapters/openclaw/mcp-tools.js +1 -1
  26. package/build/adapters/opencode/index.js +31 -17
  27. package/build/adapters/opencode/zod3tov4.js +27 -6
  28. package/build/adapters/pi/extension.d.ts +2 -12
  29. package/build/adapters/pi/extension.js +114 -96
  30. package/build/adapters/types.d.ts +5 -4
  31. package/build/adapters/types.js +4 -3
  32. package/build/cache-heal.d.ts +48 -0
  33. package/build/cache-heal.js +150 -0
  34. package/build/cli.js +37 -97
  35. package/build/executor.d.ts +25 -0
  36. package/build/executor.js +143 -22
  37. package/build/opencode-plugin.js +5 -2
  38. package/build/routing-block.d.ts +8 -0
  39. package/build/routing-block.js +86 -0
  40. package/build/runtime.d.ts +0 -36
  41. package/build/runtime.js +107 -27
  42. package/build/search/flood-guard.d.ts +57 -0
  43. package/build/search/flood-guard.js +80 -0
  44. package/build/security.d.ts +8 -3
  45. package/build/security.js +155 -29
  46. package/build/server.d.ts +14 -0
  47. package/build/server.js +368 -350
  48. package/build/session/analytics.d.ts +1 -1
  49. package/build/session/analytics.js +5 -1
  50. package/build/session/db.js +23 -3
  51. package/build/session/extract.js +8 -0
  52. package/build/store.d.ts +1 -1
  53. package/build/store.js +139 -25
  54. package/build/tool-naming.d.ts +4 -0
  55. package/build/tool-naming.js +24 -0
  56. package/build/util/jsonc.d.ts +14 -0
  57. package/build/util/jsonc.js +104 -0
  58. package/cli.bundle.mjs +254 -252
  59. package/configs/antigravity/GEMINI.md +2 -2
  60. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  61. package/configs/antigravity-cli/hooks.json +37 -0
  62. package/configs/antigravity-cli/mcp_config.json +10 -0
  63. package/configs/antigravity-cli/plugin.json +14 -0
  64. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  65. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  66. package/configs/claude-code/CLAUDE.md +2 -2
  67. package/configs/codex/AGENTS.md +2 -2
  68. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  69. package/configs/copilot-cli/.mcp.json +12 -0
  70. package/configs/copilot-cli/README.md +47 -0
  71. package/configs/copilot-cli/hooks.json +41 -0
  72. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  73. package/configs/gemini-cli/GEMINI.md +2 -2
  74. package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
  75. package/configs/kilo/AGENTS.md +2 -2
  76. package/configs/kiro/KIRO.md +2 -2
  77. package/configs/omp/SYSTEM.md +2 -2
  78. package/configs/openclaw/AGENTS.md +2 -2
  79. package/configs/opencode/AGENTS.md +2 -2
  80. package/configs/qwen-code/QWEN.md +2 -2
  81. package/configs/vscode-copilot/copilot-instructions.md +2 -2
  82. package/configs/zed/AGENTS.md +2 -2
  83. package/hooks/antigravity-cli/payload.mjs +98 -0
  84. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  85. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  86. package/hooks/antigravity-cli/stop.mjs +58 -0
  87. package/hooks/codex/pretooluse.mjs +14 -4
  88. package/hooks/codex/stop.mjs +12 -4
  89. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  90. package/hooks/copilot-cli/precompact.mjs +66 -0
  91. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  92. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  93. package/hooks/copilot-cli/stop.mjs +59 -0
  94. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  95. package/hooks/core/codex-caps.mjs +112 -0
  96. package/hooks/core/formatters.mjs +158 -7
  97. package/hooks/core/mcp-ready.mjs +37 -8
  98. package/hooks/core/routing.mjs +94 -8
  99. package/hooks/core/tool-naming.mjs +3 -0
  100. package/hooks/hooks.json +12 -1
  101. package/hooks/pretooluse.mjs +6 -2
  102. package/hooks/routing-block.mjs +2 -2
  103. package/hooks/security.bundle.mjs +2 -1
  104. package/hooks/session-db.bundle.mjs +5 -5
  105. package/hooks/session-directive.mjs +88 -20
  106. package/hooks/session-extract.bundle.mjs +1 -1
  107. package/hooks/session-helpers.mjs +21 -0
  108. package/hooks/sessionstart.mjs +37 -5
  109. package/hooks/stop.mjs +49 -0
  110. package/openclaw.plugin.json +1 -1
  111. package/package.json +4 -10
  112. package/scripts/install-antigravity-cli-plugin.mjs +141 -0
  113. package/server.bundle.mjs +208 -203
  114. package/skills/ctx-insight/SKILL.md +12 -17
  115. package/build/util/db-lock.d.ts +0 -65
  116. package/build/util/db-lock.js +0 -166
  117. package/insight/index.html +0 -13
  118. package/insight/package.json +0 -55
  119. package/insight/server.mjs +0 -1265
  120. package/insight/src/components/analytics.tsx +0 -112
  121. package/insight/src/components/ui/badge.tsx +0 -52
  122. package/insight/src/components/ui/button.tsx +0 -58
  123. package/insight/src/components/ui/card.tsx +0 -103
  124. package/insight/src/components/ui/chart.tsx +0 -371
  125. package/insight/src/components/ui/collapsible.tsx +0 -19
  126. package/insight/src/components/ui/input.tsx +0 -20
  127. package/insight/src/components/ui/progress.tsx +0 -83
  128. package/insight/src/components/ui/scroll-area.tsx +0 -55
  129. package/insight/src/components/ui/separator.tsx +0 -23
  130. package/insight/src/components/ui/table.tsx +0 -114
  131. package/insight/src/components/ui/tabs.tsx +0 -82
  132. package/insight/src/components/ui/tooltip.tsx +0 -64
  133. package/insight/src/lib/api.ts +0 -144
  134. package/insight/src/lib/utils.ts +0 -6
  135. package/insight/src/main.tsx +0 -22
  136. package/insight/src/routeTree.gen.ts +0 -189
  137. package/insight/src/router.tsx +0 -19
  138. package/insight/src/routes/__root.tsx +0 -55
  139. package/insight/src/routes/enterprise.tsx +0 -316
  140. package/insight/src/routes/index.tsx +0 -1482
  141. package/insight/src/routes/knowledge.tsx +0 -221
  142. package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
  143. package/insight/src/routes/search.tsx +0 -97
  144. package/insight/src/routes/sessions.tsx +0 -179
  145. package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
  146. package/insight/src/styles.css +0 -104
  147. package/insight/tsconfig.json +0 -29
  148. package/insight/vite.config.ts +0 -19
@@ -1,1265 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * context-mode Insight — Local analytics dashboard.
4
- * Cross-platform: works with Bun (bun:sqlite) or Node.js (better-sqlite3).
5
- *
6
- * Usage:
7
- * bun insight/server.mjs # fast, uses bun:sqlite
8
- * node insight/server.mjs # fallback, uses better-sqlite3
9
- */
10
-
11
- import { readFileSync, readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
12
- import { join, dirname, extname, normalize } from "node:path";
13
- import { homedir } from "node:os";
14
- import { fileURLToPath } from "node:url";
15
- import { createServer as createHttpServer } from "node:http";
16
-
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
- const PORT = process.env.PORT || 4747;
19
-
20
- // ── Cross-platform SQLite ────────────────────────────────
21
- // Detect runtime: Bun has bun:sqlite built-in, Node needs better-sqlite3
22
-
23
- let Database;
24
- const isBun = typeof globalThis.Bun !== "undefined";
25
-
26
- if (isBun) {
27
- Database = (await import("bun:sqlite")).Database;
28
- } else {
29
- try {
30
- Database = (await import("better-sqlite3")).default;
31
- // Verify native addon loads correctly (catches arch mismatch: x86_64 vs arm64)
32
- const testDb = new Database(":memory:");
33
- testDb.close();
34
- } catch (err) {
35
- const msg = err instanceof Error ? err.message : String(err);
36
- console.error("\n Error: better-sqlite3 failed to load.");
37
- console.error(` ${msg}`);
38
- if (msg.includes("incompatible architecture") || msg.includes("dlopen")) {
39
- const cacheHint = process.env.INSIGHT_SESSION_DIR
40
- ? join(dirname(process.env.INSIGHT_SESSION_DIR), "insight-cache", "node_modules")
41
- : join("~", ".claude", "context-mode", "insight-cache", "node_modules");
42
- console.error(`\n Fix: rm -rf ${cacheHint} && context-mode insight`);
43
- } else {
44
- console.error(" Install it: npm install better-sqlite3");
45
- }
46
- process.exit(1);
47
- }
48
- }
49
-
50
- // ── Paths ────────────────────────────────────────────────
51
- const SESSION_DIR = process.env.INSIGHT_SESSION_DIR || join(homedir(), ".claude", "context-mode", "sessions");
52
- const CONTENT_DIR = process.env.INSIGHT_CONTENT_DIR || join(homedir(), ".claude", "context-mode", "content");
53
- const DIST_DIR = join(__dirname, "dist");
54
-
55
- // ── Response cache (5min TTL) ────────────────────────────
56
- // Prevents double DB open when dashboard loads /analytics + /category-analytics
57
- const _cache = new Map();
58
- const CACHE_TTL_MS = 5 * 60 * 1000;
59
- function cached(key, fn) {
60
- const entry = _cache.get(key);
61
- if (entry && Date.now() - entry.ts < CACHE_TTL_MS) return entry.data;
62
- try {
63
- const data = fn();
64
- _cache.set(key, { data, ts: Date.now() });
65
- return data;
66
- } catch (e) {
67
- // Cache the error for 30s to avoid repeated expensive failures
68
- _cache.set(key, { data: { error: String(e) }, ts: Date.now() - CACHE_TTL_MS + 30000 });
69
- return { error: "analytics computation failed" };
70
- }
71
- }
72
-
73
- // ── SQLite helpers ───────────────────────────────────────
74
-
75
- function openDB(path) {
76
- try {
77
- return isBun
78
- ? new Database(path, { readonly: true })
79
- : new Database(path, { readonly: true, fileMustExist: true });
80
- } catch { return null; }
81
- }
82
-
83
- function safeAll(db, sql, params = []) {
84
- try { return db.prepare(sql).all(...params); } catch { return []; }
85
- }
86
-
87
- function safeGet(db, sql, params = []) {
88
- try { return db.prepare(sql).get(...params); } catch { return null; }
89
- }
90
-
91
- function hasColumn(db, table, column) {
92
- try {
93
- const rows = db.prepare(`PRAGMA table_xinfo(${table})`).all();
94
- return rows.some(r => r.name === column);
95
- } catch {
96
- return false;
97
- }
98
- }
99
-
100
- const UNKNOWN_PROJECT_KEY = "__unknown__";
101
-
102
- function normalizeFsPath(path) {
103
- const norm = normalize(String(path || "")).replace(/\\/g, "/");
104
- if (norm.length <= 1) return norm;
105
- return norm.replace(/\/+$/, "");
106
- }
107
-
108
- function parseFileSearchPath(data) {
109
- const marker = " in ";
110
- const idx = String(data || "").lastIndexOf(marker);
111
- if (idx < 0) return null;
112
- const p = String(data || "").slice(idx + marker.length).trim();
113
- return p || null;
114
- }
115
-
116
- function isLikelyPath(value) {
117
- const v = String(value || "");
118
- return v.includes("/") || v.includes("\\") || v.startsWith(".") || /^[A-Za-z]:[\\/]/.test(v);
119
- }
120
-
121
- function legacyProjectAttribution(db) {
122
- const origins = new Map(
123
- safeAll(db, "SELECT session_id, project_dir FROM session_meta")
124
- .map((r) => [r.session_id, r.project_dir || UNKNOWN_PROJECT_KEY]),
125
- );
126
-
127
- const events = safeAll(db, `SELECT id, session_id, type, data FROM session_events ORDER BY id ASC`);
128
- const lastProjectBySession = new Map();
129
- const projectAgg = new Map();
130
- let unknownEvents = 0;
131
-
132
- function addProject(projectDir, sessionId) {
133
- const key = projectDir || UNKNOWN_PROJECT_KEY;
134
- const existing = projectAgg.get(key) || { project_dir: key, sessionsSet: new Set(), events: 0, compacts: 0, avg_confidence: 0, high_conf_events: 0 };
135
- existing.events += 1;
136
- existing.sessionsSet.add(sessionId);
137
- projectAgg.set(key, existing);
138
- }
139
-
140
- for (const ev of events) {
141
- const sessionId = ev.session_id;
142
- const origin = origins.get(sessionId) || UNKNOWN_PROJECT_KEY;
143
- const last = lastProjectBySession.get(sessionId) || "";
144
- let projectDir = "";
145
-
146
- if (ev.type === "cwd" && isLikelyPath(ev.data)) {
147
- projectDir = normalizeFsPath(ev.data);
148
- } else if (ev.type === "file_read" || ev.type === "file_write" || ev.type === "file_edit" || ev.type === "rule") {
149
- if (isLikelyPath(ev.data)) {
150
- const p = normalizeFsPath(ev.data);
151
- if (origin !== UNKNOWN_PROJECT_KEY && (p === origin || p.startsWith(`${origin}/`))) projectDir = origin;
152
- else projectDir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : p;
153
- }
154
- } else if (ev.type === "file_search") {
155
- const p = parseFileSearchPath(ev.data);
156
- if (p && isLikelyPath(p)) {
157
- const pp = normalizeFsPath(p);
158
- if (origin !== UNKNOWN_PROJECT_KEY && (pp === origin || pp.startsWith(`${origin}/`))) projectDir = origin;
159
- else projectDir = pp;
160
- }
161
- }
162
-
163
- if (!projectDir) {
164
- projectDir = last || origin || UNKNOWN_PROJECT_KEY;
165
- }
166
- if (!projectDir || projectDir === UNKNOWN_PROJECT_KEY) unknownEvents += 1;
167
-
168
- addProject(projectDir, sessionId);
169
- if (projectDir && projectDir !== UNKNOWN_PROJECT_KEY) {
170
- lastProjectBySession.set(sessionId, projectDir);
171
- }
172
- }
173
-
174
- const rows = [...projectAgg.values()].map((r) => ({
175
- project_dir: r.project_dir,
176
- sessions: r.sessionsSet.size,
177
- events: r.events,
178
- compacts: 0,
179
- avg_confidence: 0,
180
- high_conf_events: 0,
181
- })).sort((a, b) => b.events - a.events);
182
-
183
- return {
184
- projectRows: rows,
185
- total_events: events.length,
186
- unknown_events: unknownEvents,
187
- avg_confidence: 0,
188
- high_conf_events: 0,
189
- };
190
- }
191
-
192
- function listDBFiles(dir) {
193
- if (!existsSync(dir)) return [];
194
- return readdirSync(dir)
195
- .filter(f => f.endsWith(".db"))
196
- .map(f => ({ name: f, path: join(dir, f), size: statSync(join(dir, f)).size }));
197
- }
198
-
199
- function formatBytes(b) {
200
- if (b >= 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB`;
201
- if (b >= 1024) return `${(b / 1024).toFixed(1)} KB`;
202
- return `${b} B`;
203
- }
204
-
205
- function queryAllSessionDBs(fn) {
206
- const results = [];
207
- for (const f of listDBFiles(SESSION_DIR)) {
208
- const db = openDB(f.path);
209
- if (!db) continue;
210
- try { results.push(...fn(db)); } finally { db.close(); }
211
- }
212
- return results;
213
- }
214
-
215
- function queryAllContentDBs(fn) {
216
- const results = [];
217
- for (const f of listDBFiles(CONTENT_DIR)) {
218
- const db = openDB(f.path);
219
- if (!db) continue;
220
- try { results.push(...fn(db)); } finally { db.close(); }
221
- }
222
- return results;
223
- }
224
-
225
- function mergeByKey(arr, key, mergeFn) {
226
- const map = new Map();
227
- for (const item of arr) {
228
- const k = item[key];
229
- if (map.has(k)) map.set(k, mergeFn(map.get(k), item));
230
- else map.set(k, { ...item });
231
- }
232
- return [...map.values()];
233
- }
234
-
235
- // ── Input validation ────────────────────────────────────
236
- function isValidHash(hash) {
237
- return /^[a-f0-9_]+$/.test(hash);
238
- }
239
-
240
- // ── API Handlers ─────────────────────────────────────────
241
-
242
- function apiOverview() {
243
- const contentDBs = listDBFiles(CONTENT_DIR);
244
- const sessionDBs = listDBFiles(SESSION_DIR);
245
- let totalSources = 0, totalChunks = 0, totalContentSize = 0;
246
- let totalSessions = 0, totalEvents = 0, totalSessionSize = 0;
247
-
248
- for (const f of contentDBs) {
249
- totalContentSize += f.size;
250
- const db = openDB(f.path);
251
- if (!db) continue;
252
- try {
253
- totalSources += safeGet(db, "SELECT COUNT(*) as c FROM sources")?.c || 0;
254
- totalChunks += safeGet(db, "SELECT COUNT(*) as c FROM chunks")?.c || 0;
255
- } finally { db.close(); }
256
- }
257
- for (const f of sessionDBs) {
258
- totalSessionSize += f.size;
259
- const db = openDB(f.path);
260
- if (!db) continue;
261
- try {
262
- totalSessions += safeGet(db, "SELECT COUNT(*) as c FROM session_meta")?.c || 0;
263
- totalEvents += safeGet(db, "SELECT COUNT(*) as c FROM session_events")?.c || 0;
264
- } finally { db.close(); }
265
- }
266
- return {
267
- content: { databases: contentDBs.length, sources: totalSources, chunks: totalChunks,
268
- totalSize: formatBytes(totalContentSize), totalSizeBytes: totalContentSize },
269
- sessions: { databases: sessionDBs.length, sessions: totalSessions, events: totalEvents,
270
- totalSize: formatBytes(totalSessionSize), totalSizeBytes: totalSessionSize },
271
- };
272
- }
273
-
274
- function apiContentDBs() {
275
- return listDBFiles(CONTENT_DIR).map(f => {
276
- const db = openDB(f.path);
277
- if (!db) return { hash: f.name.replace(".db",""), size: formatBytes(f.size), sources: [], sourceCount: 0, chunkCount: 0 };
278
- try {
279
- const sources = safeAll(db, "SELECT id, label, chunk_count, code_chunk_count, indexed_at FROM sources ORDER BY indexed_at DESC");
280
- const chunkCount = safeGet(db, "SELECT COUNT(*) as c FROM chunks")?.c || 0;
281
- return {
282
- hash: f.name.replace(".db",""), size: formatBytes(f.size), sizeBytes: f.size,
283
- sourceCount: sources.length, chunkCount,
284
- sources: sources.map(s => ({ id: s.id, label: s.label, chunks: s.chunk_count, codeChunks: s.code_chunk_count, indexedAt: s.indexed_at })),
285
- };
286
- } finally { db.close(); }
287
- });
288
- }
289
-
290
- function apiSourceChunks(dbHash, sourceId) {
291
- const db = openDB(join(CONTENT_DIR, `${dbHash}.db`));
292
- if (!db) return [];
293
- try {
294
- return safeAll(db,
295
- `SELECT c.title, c.content, c.content_type, s.label
296
- FROM chunks c JOIN sources s ON s.id = c.source_id
297
- WHERE c.source_id = ? ORDER BY c.rowid`, [sourceId]);
298
- } finally { db.close(); }
299
- }
300
-
301
- function apiSearchAll(query) {
302
- const results = [];
303
- for (const f of listDBFiles(CONTENT_DIR)) {
304
- const db = openDB(f.path);
305
- if (!db) continue;
306
- try {
307
- const rows = safeAll(db,
308
- `SELECT c.title, c.content, c.content_type, s.label,
309
- bm25(chunks, 5.0, 1.0) AS rank,
310
- highlight(chunks, 1, '«', '»') AS highlighted
311
- FROM chunks c JOIN sources s ON s.id = c.source_id
312
- WHERE chunks MATCH ?
313
- ORDER BY rank LIMIT 10`, [query]);
314
- results.push(...rows.map(r => ({ ...r, dbHash: f.name.replace(".db","") })));
315
- } finally { db.close(); }
316
- }
317
- if (results.length > 0) {
318
- return results.sort((a, b) => a.rank - b.rank).slice(0, 30);
319
- }
320
- // Fallback: LIKE search across content + session events
321
- const likeResults = [];
322
- const likePattern = `%${query}%`;
323
- for (const f of listDBFiles(CONTENT_DIR)) {
324
- const db = openDB(f.path);
325
- if (!db) continue;
326
- try {
327
- const rows = safeAll(db,
328
- `SELECT c.title, c.content, c.content_type, s.label
329
- FROM chunks c JOIN sources s ON s.id = c.source_id
330
- WHERE c.content LIKE ? LIMIT 10`, [likePattern]);
331
- likeResults.push(...rows.map(r => ({ ...r, rank: 0, highlighted: null, dbHash: f.name.replace(".db","") })));
332
- } finally { db.close(); }
333
- }
334
- for (const f of listDBFiles(SESSION_DIR)) {
335
- const db = openDB(f.path);
336
- if (!db) continue;
337
- try {
338
- const rows = safeAll(db,
339
- `SELECT se.type as title, se.data as content, 'session' as content_type,
340
- sm.project_dir as label
341
- FROM session_events se
342
- LEFT JOIN session_meta sm ON se.session_id = sm.session_id
343
- WHERE se.data LIKE ? LIMIT 10`, [likePattern]);
344
- likeResults.push(...rows.map(r => ({ ...r, rank: 0, highlighted: null, dbHash: "session:" + f.name.replace(".db","").slice(0, 8) })));
345
- } finally { db.close(); }
346
- }
347
- return likeResults.slice(0, 20);
348
- }
349
-
350
- function apiSessionDBs() {
351
- return listDBFiles(SESSION_DIR).map(f => {
352
- const db = openDB(f.path);
353
- if (!db) return { hash: f.name.replace(".db",""), size: formatBytes(f.size), sessions: [] };
354
- try {
355
- const sessions = safeAll(db,
356
- `SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count
357
- FROM session_meta ORDER BY started_at DESC`);
358
- return {
359
- hash: f.name.replace(".db",""), size: formatBytes(f.size), sizeBytes: f.size,
360
- sessions: sessions.map(s => ({ id: s.session_id, projectDir: s.project_dir,
361
- startedAt: s.started_at, lastEventAt: s.last_event_at,
362
- eventCount: s.event_count, compactCount: s.compact_count })),
363
- };
364
- } finally { db.close(); }
365
- });
366
- }
367
-
368
- function apiSessionEvents(dbHash, sessionId) {
369
- const db = openDB(join(SESSION_DIR, `${dbHash}.db`));
370
- if (!db) return { events: [], resume: null };
371
- try {
372
- const events = safeAll(db,
373
- `SELECT id, type, category, priority, data, source_hook, created_at
374
- FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT 500`, [sessionId]);
375
- const resume = safeGet(db,
376
- `SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?`, [sessionId]);
377
- return { events, resume };
378
- } finally { db.close(); }
379
- }
380
-
381
- function apiDeleteSource(dbHash, sourceId) {
382
- try {
383
- const dbPath = join(CONTENT_DIR, `${dbHash}.db`);
384
- const db = isBun ? new Database(dbPath) : new Database(dbPath);
385
- db.prepare("DELETE FROM chunks WHERE source_id = ?").run(sourceId);
386
- try { db.prepare("DELETE FROM chunks_trigram WHERE source_id = ?").run(sourceId); } catch {}
387
- db.prepare("DELETE FROM sources WHERE id = ?").run(sourceId);
388
- db.close();
389
- return { ok: true };
390
- } catch (e) { return { ok: false, error: e.message }; }
391
- }
392
-
393
- function apiAnalytics() {
394
- const sessionDurations = queryAllSessionDBs(db =>
395
- safeAll(db, `SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count,
396
- ROUND((julianday(last_event_at) - julianday(started_at)) * 24 * 60, 1) as duration_min
397
- FROM session_meta WHERE started_at IS NOT NULL AND last_event_at IS NOT NULL
398
- ORDER BY started_at DESC LIMIT 50`)
399
- );
400
- const sessionsByDate = queryAllSessionDBs(db =>
401
- safeAll(db, `SELECT date(started_at) as date, COUNT(*) as count,
402
- SUM(event_count) as events, SUM(compact_count) as compacts
403
- FROM session_meta WHERE started_at IS NOT NULL
404
- GROUP BY date(started_at) ORDER BY date`)
405
- );
406
- const toolUsage = queryAllSessionDBs(db =>
407
- safeAll(db, `SELECT
408
- CASE
409
- WHEN type = 'file_read' THEN 'Read'
410
- WHEN type = 'file_write' THEN 'Write/Edit'
411
- WHEN type = 'file_glob' THEN 'Glob'
412
- WHEN type = 'file_search' THEN 'Grep'
413
- WHEN type = 'mcp' THEN 'context-mode'
414
- WHEN type = 'git' THEN 'Git'
415
- WHEN type = 'subagent' THEN 'Agent'
416
- WHEN type = 'task' THEN 'Task'
417
- WHEN type = 'error_tool' THEN 'Error'
418
- ELSE type
419
- END as tool, COUNT(*) as count
420
- FROM session_events
421
- WHERE type NOT IN ('rule', 'rule_content', 'user_prompt', 'intent', 'data', 'role', 'cwd')
422
- GROUP BY tool ORDER BY count DESC`)
423
- );
424
- const mcpTools = queryAllSessionDBs(db =>
425
- safeAll(db, `SELECT
426
- CASE
427
- WHEN data LIKE 'batch_execute%' THEN 'batch_execute'
428
- WHEN data LIKE 'execute_file%' THEN 'execute_file'
429
- WHEN data LIKE 'execute%' THEN 'execute'
430
- WHEN data LIKE 'search%' THEN 'search'
431
- WHEN data LIKE 'index%' THEN 'index'
432
- WHEN data LIKE 'fetch%' THEN 'fetch_and_index'
433
- WHEN data LIKE 'stats%' THEN 'stats'
434
- WHEN data LIKE 'purge%' THEN 'purge'
435
- ELSE substr(data, 1, 20)
436
- END as tool, COUNT(*) as count
437
- FROM session_events WHERE type = 'mcp'
438
- GROUP BY tool ORDER BY count DESC`)
439
- );
440
- const readWriteRatio = queryAllSessionDBs(db =>
441
- safeAll(db, `SELECT
442
- SUM(CASE WHEN type = 'file_read' THEN 1 ELSE 0 END) as reads,
443
- SUM(CASE WHEN type = 'file_write' THEN 1 ELSE 0 END) as writes,
444
- SUM(CASE WHEN type IN ('file_read', 'file_write', 'file', 'file_glob', 'file_search') THEN 1 ELSE 0 END) as total_file_ops
445
- FROM session_events`)
446
- );
447
- const errors = queryAllSessionDBs(db =>
448
- safeAll(db, `SELECT data as detail, created_at, session_id FROM session_events
449
- WHERE type = 'error_tool' OR type = 'error' ORDER BY created_at DESC LIMIT 20`)
450
- );
451
- const errorCounts = queryAllSessionDBs(db =>
452
- safeAll(db, `SELECT COUNT(*) as count FROM session_events
453
- WHERE type = 'error_tool' OR type = 'error'`)
454
- );
455
- const fileActivity = queryAllSessionDBs(db =>
456
- safeAll(db, `SELECT data as file, type as op, COUNT(*) as count FROM session_events
457
- WHERE type IN ('file_read', 'file_write', 'file') AND data != ''
458
- GROUP BY data ORDER BY count DESC LIMIT 20`)
459
- );
460
- const workModes = queryAllSessionDBs(db =>
461
- safeAll(db, `SELECT data as mode, COUNT(*) as count
462
- FROM session_events WHERE type = 'intent' AND data != ''
463
- GROUP BY data ORDER BY count DESC`)
464
- );
465
- const timeToFirstCommit = queryAllSessionDBs(db =>
466
- safeAll(db, `SELECT sm.session_id, sm.started_at,
467
- MIN(se.created_at) as first_commit_at,
468
- ROUND((julianday(MIN(se.created_at)) - julianday(sm.started_at)) * 24 * 60, 1) as minutes_to_commit
469
- FROM session_meta sm
470
- JOIN session_events se ON se.session_id = sm.session_id
471
- WHERE se.type = 'git' AND se.data = 'commit'
472
- GROUP BY sm.session_id`)
473
- );
474
- const exploreExecRatio = queryAllSessionDBs(db =>
475
- safeAll(db, `SELECT
476
- SUM(CASE WHEN type IN ('file_read', 'file_glob', 'file_search') THEN 1 ELSE 0 END) as explore,
477
- SUM(CASE WHEN type IN ('file_write') THEN 1 ELSE 0 END) as execute,
478
- COUNT(*) as total
479
- FROM session_events WHERE type IN ('file_read', 'file_glob', 'file_search', 'file_write')`)
480
- );
481
- const reworkData = queryAllSessionDBs(db =>
482
- safeAll(db, `SELECT se.session_id, se.data as file, COUNT(*) as edit_count
483
- FROM session_events se
484
- WHERE se.type IN ('file_write', 'file_read') AND se.data != ''
485
- GROUP BY se.session_id, se.data HAVING edit_count > 1
486
- ORDER BY edit_count DESC LIMIT 20`)
487
- );
488
- const gitActivity = queryAllSessionDBs(db => {
489
- if (hasColumn(db, "session_events", "project_dir")) {
490
- return safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
491
- COALESCE(NULLIF(se.project_dir, ''), sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
492
- sm.started_at as session_start
493
- FROM session_events se
494
- LEFT JOIN session_meta sm ON se.session_id = sm.session_id
495
- WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`);
496
- }
497
- return safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
498
- COALESCE(sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir, sm.started_at as session_start
499
- FROM session_events se
500
- LEFT JOIN session_meta sm ON se.session_id = sm.session_id
501
- WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`);
502
- });
503
- const rawSubagents = queryAllSessionDBs(db =>
504
- safeAll(db, `SELECT data as task, created_at, session_id FROM session_events
505
- WHERE type = 'subagent' ORDER BY created_at ASC`)
506
- );
507
- const bursts = [];
508
- let currentBurst = [];
509
- for (const s of rawSubagents) {
510
- if (currentBurst.length === 0) { currentBurst.push(s); continue; }
511
- const last = currentBurst[currentBurst.length - 1];
512
- const gap = (new Date(s.created_at) - new Date(last.created_at)) / 1000;
513
- if (gap <= 30) { currentBurst.push(s); }
514
- else { if (currentBurst.length > 0) bursts.push([...currentBurst]); currentBurst = [s]; }
515
- }
516
- if (currentBurst.length > 0) bursts.push(currentBurst);
517
- const parallelBursts = bursts.filter(b => b.length >= 2);
518
- const subagents = {
519
- total: rawSubagents.length,
520
- bursts: parallelBursts.length,
521
- maxConcurrent: bursts.reduce((max, b) => Math.max(max, b.length), 0),
522
- parallelCount: parallelBursts.reduce((a, b) => a + b.length, 0),
523
- sequentialCount: rawSubagents.length - parallelBursts.reduce((a, b) => a + b.length, 0),
524
- timeSavedMin: parallelBursts.reduce((a, b) => a + (b.length - 1) * 2, 0),
525
- burstDetails: parallelBursts.map(b => ({ size: b.length, time: b[0].created_at })),
526
- };
527
- const projectActivity = queryAllSessionDBs(db => {
528
- if (hasColumn(db, "session_events", "project_dir")) {
529
- return safeAll(db, `SELECT
530
- COALESCE(NULLIF(se.project_dir, ''), '${UNKNOWN_PROJECT_KEY}') as project_dir,
531
- COUNT(DISTINCT se.session_id) as sessions,
532
- COUNT(*) as events,
533
- 0 as compacts,
534
- AVG(COALESCE(se.attribution_confidence, 0)) as avg_confidence,
535
- SUM(CASE WHEN COALESCE(se.attribution_confidence, 0) >= 0.8 THEN 1 ELSE 0 END) as high_conf_events
536
- FROM session_events se
537
- GROUP BY project_dir
538
- ORDER BY events DESC
539
- LIMIT 20`);
540
- }
541
- return legacyProjectAttribution(db).projectRows;
542
- });
543
-
544
- const attributionSummary = queryAllSessionDBs(db => {
545
- if (hasColumn(db, "session_events", "project_dir")) {
546
- return safeAll(db, `SELECT
547
- COUNT(*) as total_events,
548
- SUM(CASE WHEN COALESCE(project_dir, '') = '' THEN 1 ELSE 0 END) as unknown_events,
549
- AVG(COALESCE(attribution_confidence, 0)) as avg_confidence,
550
- SUM(CASE WHEN COALESCE(attribution_confidence, 0) >= 0.8 THEN 1 ELSE 0 END) as high_conf_events
551
- FROM session_events`);
552
- }
553
- return [legacyProjectAttribution(db)];
554
- });
555
- const hourlyPattern = queryAllSessionDBs(db =>
556
- safeAll(db, `SELECT CAST(strftime('%H', created_at) AS INTEGER) as hour, COUNT(*) as count
557
- FROM session_events WHERE created_at IS NOT NULL
558
- GROUP BY hour ORDER BY hour`)
559
- );
560
- const weeklyTrend = queryAllSessionDBs(db =>
561
- safeAll(db, `SELECT strftime('%Y-W%W', started_at) as week, COUNT(*) as sessions,
562
- SUM(event_count) as events
563
- FROM session_meta WHERE started_at IS NOT NULL
564
- GROUP BY week ORDER BY week`)
565
- );
566
- const tasks = queryAllSessionDBs(db =>
567
- safeAll(db, `SELECT substr(data, 1, 100) as task, created_at FROM session_events
568
- WHERE type = 'task' ORDER BY created_at DESC LIMIT 20`)
569
- );
570
- const taskCounts = queryAllSessionDBs(db =>
571
- safeAll(db, `SELECT COUNT(*) as count FROM session_events WHERE type = 'task'`)
572
- );
573
- const prompts = queryAllSessionDBs(db =>
574
- safeAll(db, `SELECT substr(data, 1, 100) as prompt, created_at, session_id FROM session_events
575
- WHERE type = 'user_prompt' ORDER BY created_at DESC LIMIT 20`)
576
- );
577
- const promptCounts = queryAllSessionDBs(db =>
578
- safeAll(db, `SELECT COUNT(*) as count FROM session_events WHERE type = 'user_prompt'`)
579
- );
580
-
581
- const rw = readWriteRatio.reduce((a, b) => ({
582
- reads: (a.reads || 0) + (b.reads || 0), writes: (a.writes || 0) + (b.writes || 0),
583
- total_file_ops: (a.total_file_ops || 0) + (b.total_file_ops || 0),
584
- }), { reads: 0, writes: 0, total_file_ops: 0 });
585
- const totalEvents = toolUsage.reduce((a, b) => a + b.count, 0);
586
- const totalErrors = errorCounts.reduce((a, b) => a + (b.count || 0), 0);
587
- const totalTasks = taskCounts.reduce((a, b) => a + (b.count || 0), 0);
588
- const totalPrompts = promptCounts.reduce((a, b) => a + (b.count || 0), 0);
589
- const totalCompacts = sessionDurations.reduce((a, b) => a + (b.compact_count || 0), 0);
590
- const sessionsWithCompact = sessionDurations.filter(s => s.compact_count > 0).length;
591
-
592
- // ── New metric queries ──────────────────────────────────
593
-
594
- // 1. Tool Mastery Curve — weekly error rate trend
595
- const masteryTrend = queryAllSessionDBs(db =>
596
- safeAll(db, `SELECT strftime('%Y-W%W', created_at) as week,
597
- SUM(CASE WHEN type = 'error_tool' THEN 1 ELSE 0 END) as errors,
598
- COUNT(*) as total,
599
- ROUND(100.0 * SUM(CASE WHEN type = 'error_tool' THEN 1 ELSE 0 END) / COUNT(*), 1) as error_rate
600
- FROM session_events WHERE created_at IS NOT NULL
601
- GROUP BY week ORDER BY week`)
602
- );
603
-
604
- // 2. Personal Commit Rate — commits per session
605
- const commitRate = queryAllSessionDBs(db => {
606
- if (hasColumn(db, "session_events", "project_dir")) {
607
- return safeAll(db, `SELECT
608
- sm.session_id,
609
- COALESCE(NULLIF(MAX(CASE WHEN se.type = 'git' THEN se.project_dir END), ''), sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
610
- SUM(CASE WHEN se.type = 'git' AND se.data = 'commit' THEN 1 ELSE 0 END) as commits
611
- FROM session_meta sm
612
- LEFT JOIN session_events se ON se.session_id = sm.session_id
613
- GROUP BY sm.session_id`);
614
- }
615
- return safeAll(db, `SELECT sm.session_id, COALESCE(sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
616
- SUM(CASE WHEN se.type = 'git' AND se.data = 'commit' THEN 1 ELSE 0 END) as commits
617
- FROM session_meta sm
618
- LEFT JOIN session_events se ON se.session_id = sm.session_id
619
- GROUP BY sm.session_id`);
620
- });
621
-
622
- // 3. Sandbox Adoption — context-mode MCP tool usage vs total
623
- const sandboxAdoption = queryAllSessionDBs(db =>
624
- safeAll(db, `SELECT
625
- SUM(CASE WHEN type = 'mcp' THEN 1 ELSE 0 END) as sandbox_calls,
626
- COUNT(*) as total_calls
627
- FROM session_events
628
- WHERE type NOT IN ('rule', 'rule_content', 'user_prompt', 'intent', 'data', 'role', 'cwd')`)
629
- );
630
-
631
- // 4. CLAUDE.md Freshness — rule files loaded, how many distinct
632
- const rulesFreshness = queryAllSessionDBs(db =>
633
- safeAll(db, `SELECT data as rule_path, MAX(created_at) as last_seen, COUNT(*) as load_count
634
- FROM session_events WHERE type = 'rule' AND data != ''
635
- GROUP BY data ORDER BY last_seen DESC`)
636
- );
637
-
638
- // 5. Edit-Test Cycle — write followed by error patterns
639
- const editTestCycles = queryAllSessionDBs(db =>
640
- safeAll(db, `SELECT se1.session_id, COUNT(*) as cycles
641
- FROM session_events se1
642
- JOIN session_events se2 ON se1.session_id = se2.session_id AND se2.id > se1.id
643
- AND se2.id = (SELECT MIN(id) FROM session_events WHERE id > se1.id AND session_id = se1.session_id)
644
- WHERE se1.type = 'file_write' AND se2.type = 'error_tool'
645
- GROUP BY se1.session_id`)
646
- );
647
-
648
- // 6. Bug-Fix Ratio — derived from workModes (already queried above)
649
-
650
- // ── Derived aggregates for new metrics ──────────────────
651
- const sandboxAgg = sandboxAdoption.reduce((a, b) => ({
652
- sandbox_calls: (a.sandbox_calls || 0) + (b.sandbox_calls || 0),
653
- total_calls: (a.total_calls || 0) + (b.total_calls || 0),
654
- }), { sandbox_calls: 0, total_calls: 0 });
655
- const attributionSchemaCoverage = queryAllSessionDBs(db => [{
656
- has_attribution_columns: hasColumn(db, "session_events", "project_dir") ? 1 : 0,
657
- }]);
658
- const fallbackOnly = attributionSchemaCoverage.length > 0
659
- && attributionSchemaCoverage.every((r) => !r.has_attribution_columns);
660
-
661
- const mergedProjectActivity = mergeByKey(projectActivity, "project_dir", (a, b) => {
662
- const aEvents = Number(a.events || 0);
663
- const bEvents = Number(b.events || 0);
664
- const aWeighted = Number(
665
- (a.weighted_confidence_sum ?? (Number(a.avg_confidence || 0) * aEvents)) || 0,
666
- );
667
- const bWeighted = Number(
668
- (b.weighted_confidence_sum ?? (Number(b.avg_confidence || 0) * bEvents)) || 0,
669
- );
670
- return {
671
- project_dir: a.project_dir,
672
- sessions: (a.sessions || 0) + (b.sessions || 0),
673
- events: aEvents + bEvents,
674
- compacts: (a.compacts || 0) + (b.compacts || 0),
675
- weighted_confidence_sum: aWeighted + bWeighted,
676
- high_conf_events: (a.high_conf_events || 0) + (b.high_conf_events || 0),
677
- };
678
- })
679
- .map((p) => ({
680
- project_dir: p.project_dir,
681
- sessions: p.sessions || 0,
682
- events: p.events || 0,
683
- compacts: p.compacts || 0,
684
- avg_confidence: (p.events || 0) > 0 ? (p.weighted_confidence_sum || 0) / p.events : 0,
685
- high_conf_events: p.high_conf_events || 0,
686
- }))
687
- .sort((a, b) => (b.events || 0) - (a.events || 0));
688
- const nonUnknownProjects = mergedProjectActivity.filter((p) => p.project_dir !== UNKNOWN_PROJECT_KEY);
689
-
690
- const attributionAgg = attributionSummary.reduce((a, b) => ({
691
- total_events: (a.total_events || 0) + (b.total_events || 0),
692
- unknown_events: (a.unknown_events || 0) + (b.unknown_events || 0),
693
- high_conf_events: (a.high_conf_events || 0) + (b.high_conf_events || 0),
694
- // weighted sum for avg_confidence
695
- weighted_confidence_sum: (a.weighted_confidence_sum || 0) + ((b.avg_confidence || 0) * (b.total_events || 0)),
696
- }), { total_events: 0, unknown_events: 0, high_conf_events: 0, weighted_confidence_sum: 0 });
697
-
698
- const attributedEvents = Math.max(0, attributionAgg.total_events - attributionAgg.unknown_events);
699
- const unknownPct = attributionAgg.total_events > 0
700
- ? Math.round(1000 * attributionAgg.unknown_events / attributionAgg.total_events) / 10
701
- : 100;
702
- const avgConfidencePct = attributionAgg.total_events > 0
703
- ? Math.round(1000 * attributionAgg.weighted_confidence_sum / attributionAgg.total_events) / 10
704
- : 0;
705
- const highConfidencePct = attributionAgg.total_events > 0
706
- ? Math.round(1000 * attributionAgg.high_conf_events / attributionAgg.total_events) / 10
707
- : 0;
708
-
709
- return {
710
- totals: {
711
- totalSessions: sessionDurations.length, totalEvents,
712
- avgSessionMin: sessionDurations.length > 0
713
- ? Math.round(sessionDurations.reduce((a, b) => a + (b.duration_min || 0), 0) / sessionDurations.length) : 0,
714
- totalErrors,
715
- errorRate: totalEvents > 0 ? Math.round(1000 * totalErrors / totalEvents) / 10 : 0,
716
- totalCompacts,
717
- compactRate: sessionDurations.length > 0 ? Math.round(100 * sessionsWithCompact / sessionDurations.length) : 0,
718
- reads: rw.reads, writes: rw.writes,
719
- readWriteRatio: rw.writes > 0 ? Math.round(10 * rw.reads / rw.writes) / 10 : rw.reads,
720
- totalFileOps: rw.total_file_ops, totalSubagents: subagents.total,
721
- totalTasks, totalPrompts,
722
- promptsPerSession: sessionDurations.length > 0
723
- ? Math.round(10 * totalPrompts / sessionDurations.length) / 10 : 0,
724
- uniqueProjects: nonUnknownProjects.length,
725
- totalCommits: commitRate.reduce((a, b) => a + (b.commits || 0), 0),
726
- commitsPerSession: sessionDurations.length > 0
727
- ? Math.round(10 * commitRate.reduce((a, b) => a + (b.commits || 0), 0) / sessionDurations.length) / 10 : 0,
728
- sandboxRate: sandboxAgg.total_calls > 0
729
- ? Math.round(1000 * sandboxAgg.sandbox_calls / sandboxAgg.total_calls) / 10 : 0,
730
- totalRules: rulesFreshness.length,
731
- totalEditTestCycles: editTestCycles.reduce((a, b) => a + (b.cycles || 0), 0),
732
- },
733
- attribution: {
734
- totalEvents: attributionAgg.total_events,
735
- attributedEvents,
736
- unknownEvents: attributionAgg.unknown_events,
737
- unknownPct,
738
- avgConfidencePct,
739
- highConfidencePct,
740
- isFallbackOnly: fallbackOnly,
741
- },
742
- sessionsByDate: mergeByKey(sessionsByDate, "date", (a, b) => ({
743
- date: a.date, count: a.count + b.count, events: a.events + b.events, compacts: a.compacts + b.compacts
744
- })),
745
- sessionDurations,
746
- toolUsage: mergeByKey(toolUsage, "tool", (a, b) => ({ tool: a.tool, count: a.count + b.count })).sort((a, b) => b.count - a.count),
747
- mcpTools: mergeByKey(mcpTools, "tool", (a, b) => ({ tool: a.tool, count: a.count + b.count })).sort((a, b) => b.count - a.count),
748
- errors, fileActivity: mergeByKey(fileActivity, "file", (a, b) => ({ file: a.file, op: a.op, count: a.count + b.count })).sort((a, b) => b.count - a.count).slice(0, 15),
749
- workModes: mergeByKey(workModes, "mode", (a, b) => ({ mode: a.mode, count: a.count + b.count })).sort((a, b) => b.count - a.count),
750
- timeToFirstCommit,
751
- exploreExecRatio: exploreExecRatio.reduce((a, b) => ({ explore: (a.explore||0)+(b.explore||0), execute: (a.execute||0)+(b.execute||0), total: (a.total||0)+(b.total||0) }), { explore: 0, execute: 0, total: 0 }),
752
- reworkData, gitActivity, subagents,
753
- projectActivity: mergedProjectActivity,
754
- hourlyPattern: mergeByKey(hourlyPattern, "hour", (a, b) => ({ hour: a.hour, count: a.count + b.count })),
755
- weeklyTrend: mergeByKey(weeklyTrend, "week", (a, b) => ({ week: a.week, sessions: a.sessions + b.sessions, events: a.events + b.events })),
756
- tasks, prompts,
757
- masteryTrend: mergeByKey(masteryTrend, "week", (a, b) => ({
758
- week: a.week, errors: a.errors + b.errors, total: a.total + b.total,
759
- error_rate: (a.total + b.total) > 0 ? Math.round(1000 * (a.errors + b.errors) / (a.total + b.total)) / 10 : 0,
760
- })),
761
- commitRate,
762
- sandboxAdoption: sandboxAgg,
763
- rulesFreshness,
764
- editTestCycles,
765
- };
766
- }
767
-
768
- // ── Category Analytics ──────────────────────────────────
769
-
770
- function apiCategoryAnalytics() {
771
- // 1. Category distribution
772
- const rawCategoryCounts = queryAllSessionDBs(db =>
773
- safeAll(db, `SELECT category, type, COUNT(*) as count FROM session_events GROUP BY category, type ORDER BY count DESC`)
774
- );
775
- // Add composite key for merge
776
- const catTypeRows = mergeByKey(
777
- rawCategoryCounts.map(r => ({ ...r, _cat_type: `${r.category}::${r.type}` })),
778
- "_cat_type",
779
- (a, b) => ({ _cat_type: a._cat_type, category: a.category, type: a.type, count: a.count + b.count })
780
- );
781
-
782
- const CATEGORY_MAP = {
783
- file: ["file_read", "file_write", "file_edit", "file_glob", "file_search"],
784
- git: ["git"],
785
- error: ["error_tool"],
786
- subagent: ["subagent_launched", "subagent_completed"],
787
- "rejected-approach": ["rejected"],
788
- latency: ["tool_latency"],
789
- decision: ["decision", "decision_question"],
790
- skill: ["skill"],
791
- rule: ["rule", "rule_content"],
792
- plan: ["plan_enter", "plan_exit", "plan_approved", "plan_rejected", "plan_file_write"],
793
- intent: ["intent"],
794
- "blocked-on": ["blocker", "blocker_resolved"],
795
- constraint: ["constraint_discovered"],
796
- "user-prompt": ["user_prompt"],
797
- "error-resolution": ["error_resolved"],
798
- "iteration-loop": ["retry_detected"],
799
- env: ["env", "worktree"],
800
- task: ["task_create", "task_update"],
801
- mcp: ["mcp"],
802
- "agent-finding": ["agent_finding"],
803
- "external-ref": ["external_ref"],
804
- role: ["role"],
805
- cwd: ["cwd"],
806
- data: ["data"],
807
- };
808
-
809
- const typeCountMap = new Map();
810
- for (const row of catTypeRows) {
811
- typeCountMap.set(`${row.category}::${row.type}`, row.count);
812
- }
813
-
814
- const categories = Object.entries(CATEGORY_MAP).map(([cat, types]) => {
815
- const typesObj = {};
816
- let total = 0;
817
- for (const t of types) {
818
- const c = typeCountMap.get(`${cat}::${t}`) || 0;
819
- typesObj[t] = c;
820
- total += c;
821
- }
822
- return { category: cat, count: total, types: typesObj };
823
- });
824
-
825
- // 2. Error intelligence
826
- const errorResolution = queryAllSessionDBs(db =>
827
- safeAll(db, `SELECT
828
- SUM(CASE WHEN category = 'error' THEN 1 ELSE 0 END) as total_errors,
829
- SUM(CASE WHEN category = 'error-resolution' THEN 1 ELSE 0 END) as resolved_errors
830
- FROM session_events`)
831
- );
832
- const totalErrors = errorResolution.reduce((s, r) => s + (r.total_errors || 0), 0);
833
- const resolvedErrors = errorResolution.reduce((s, r) => s + (r.resolved_errors || 0), 0);
834
- const resolutionRate = totalErrors > 0 ? Math.round(1000 * resolvedErrors / totalErrors) / 10 : 0;
835
-
836
- const retryStorms = queryAllSessionDBs(db =>
837
- safeAll(db, `SELECT session_id, COUNT(*) as retries FROM session_events WHERE category = 'iteration-loop' GROUP BY session_id HAVING COUNT(*) > 3`)
838
- );
839
-
840
- const latencyData = queryAllSessionDBs(db =>
841
- safeAll(db, `SELECT data FROM session_events WHERE category = 'latency'`)
842
- );
843
- const latencies = [];
844
- const latencyByToolMap = new Map();
845
- for (const row of latencyData) {
846
- if (!row.data) continue;
847
- const match = String(row.data).match(/^(.+?):\s*(\d+)\s*(?:ms)?$/);
848
- if (!match) continue;
849
- const tool = match[1].trim();
850
- const ms = parseInt(match[2], 10);
851
- if (isNaN(ms)) continue;
852
- latencies.push(ms);
853
- if (!latencyByToolMap.has(tool)) latencyByToolMap.set(tool, { sum: 0, count: 0, max: 0 });
854
- const entry = latencyByToolMap.get(tool);
855
- entry.sum += ms;
856
- entry.count += 1;
857
- entry.max = Math.max(entry.max, ms);
858
- }
859
- latencies.sort((a, b) => a - b);
860
- const avgLatencyMs = latencies.length > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0;
861
- const p95LatencyMs = latencies.length > 0 ? latencies[Math.floor(latencies.length * 0.95)] : 0;
862
-
863
- const latencyByTool = [...latencyByToolMap.entries()]
864
- .map(([tool, e]) => ({ tool, avg_ms: Math.round(e.sum / e.count), count: e.count, max_ms: e.max }))
865
- .sort((a, b) => b.avg_ms - a.avg_ms);
866
-
867
- let slowestTool = latencyByTool.length > 0 ? latencyByTool[0].tool : null;
868
-
869
- const topErrorTools = queryAllSessionDBs(db =>
870
- safeAll(db, `SELECT
871
- CASE
872
- WHEN data LIKE '%Bash%' THEN 'Bash'
873
- WHEN data LIKE '%Read%' THEN 'Read'
874
- WHEN data LIKE '%Edit%' THEN 'Edit'
875
- WHEN data LIKE '%Write%' THEN 'Write'
876
- WHEN data LIKE '%Agent%' THEN 'Agent'
877
- ELSE substr(data, 1, 30)
878
- END as tool,
879
- COUNT(*) as count
880
- FROM session_events WHERE category = 'error'
881
- GROUP BY tool ORDER BY count DESC LIMIT 5`)
882
- );
883
- const mergedTopErrorTools = mergeByKey(topErrorTools, "tool", (a, b) => ({ tool: a.tool, count: a.count + b.count }))
884
- .sort((a, b) => b.count - a.count).slice(0, 5);
885
-
886
- const errorIntelligence = {
887
- totalErrors,
888
- resolvedErrors,
889
- resolutionRate,
890
- retryStorms: retryStorms.length,
891
- avgLatencyMs,
892
- p95LatencyMs,
893
- p95SampleCount: latencies.length,
894
- slowestTool,
895
- topErrorTools: mergedTopErrorTools,
896
- latencyByTool,
897
- };
898
-
899
- // 3. Delegation metrics
900
- const subagentCat = categories.find(c => c.category === "subagent");
901
- const launched = subagentCat ? (subagentCat.types.subagent_launched || 0) : 0;
902
- let completed = subagentCat ? (subagentCat.types.subagent_completed || 0) : 0;
903
- if (completed > launched && launched > 0) completed = launched; // cap anomaly
904
- const completionRate = launched > 0 ? Math.round(1000 * completed / launched) / 10 : 0;
905
-
906
- // Parallel bursts: sessions with >1 subagent_launched in same session
907
- const parallelBurstData = queryAllSessionDBs(db =>
908
- safeAll(db, `SELECT session_id, COUNT(*) as cnt FROM session_events WHERE type = 'subagent_launched' GROUP BY session_id HAVING cnt > 1`)
909
- );
910
- const parallelBursts = parallelBurstData.length;
911
- const maxConcurrent = parallelBurstData.reduce((m, r) => Math.max(m, r.cnt || 0), 0);
912
- // Rough estimate: each completed subagent saves ~2 min
913
- const timeSavedMin = Math.round(completed * 2);
914
-
915
- const delegation = { launched, completed, completionRate, parallelBursts, maxConcurrent, timeSavedMin };
916
-
917
- // 4. Governance
918
- const rejectedData = queryAllSessionDBs(db =>
919
- safeAll(db, `SELECT data FROM session_events WHERE category = 'rejected-approach'`)
920
- );
921
- const rejectedToolMap = new Map();
922
- for (const row of rejectedData) {
923
- if (!row.data) continue;
924
- const tool = String(row.data).split(":")[0].trim() || "unknown";
925
- rejectedToolMap.set(tool, (rejectedToolMap.get(tool) || 0) + 1);
926
- }
927
- const topRejected = [...rejectedToolMap.entries()]
928
- .map(([tool, count]) => ({ tool, count }))
929
- .sort((a, b) => b.count - a.count)
930
- .slice(0, 5);
931
-
932
- const planCat = categories.find(c => c.category === "plan");
933
- const planApproved = planCat ? (planCat.types.plan_approved || 0) : 0;
934
- const planRejected = planCat ? (planCat.types.plan_rejected || 0) : 0;
935
- const totalPlans = planApproved + planRejected;
936
- const planApprovalRate = totalPlans > 0 ? Math.round(1000 * planApproved / totalPlans) / 10 : 0;
937
-
938
- const rejectedCat = categories.find(c => c.category === "rejected-approach");
939
- const decisionCat = categories.find(c => c.category === "decision");
940
- const constraintCat = categories.find(c => c.category === "constraint");
941
-
942
- const governance = {
943
- totalRejections: rejectedCat ? rejectedCat.count : 0,
944
- totalDecisions: decisionCat ? decisionCat.count : 0,
945
- totalConstraints: constraintCat ? constraintCat.count : 0,
946
- planApproved,
947
- planRejected,
948
- planApprovalRate,
949
- topRejected,
950
- };
951
-
952
- // 5. Git productivity
953
- const gitOps = queryAllSessionDBs(db =>
954
- safeAll(db, `SELECT data as operation, COUNT(*) as count FROM session_events WHERE category = 'git' AND data IS NOT NULL AND data != '' GROUP BY data ORDER BY count DESC`)
955
- );
956
- const mergedGitOps = mergeByKey(gitOps, "operation", (a, b) => ({ operation: a.operation, count: a.count + b.count }))
957
- .sort((a, b) => b.count - a.count);
958
- const totalCommits = mergedGitOps.find(o => o.operation === "commit")?.count || 0;
959
- const totalPushes = mergedGitOps.find(o => o.operation === "push")?.count || 0;
960
- const totalGitOps = mergedGitOps.reduce((s, o) => s + o.count, 0);
961
-
962
- const gitProductivity = {
963
- totalCommits,
964
- totalPushes,
965
- commitPushRatio: totalPushes > 0 ? Math.round(100 * totalCommits / totalPushes) / 100 : totalCommits,
966
- totalOperations: totalGitOps,
967
- operationMix: mergedGitOps,
968
- };
969
-
970
- // 6. Context health
971
- const uniqueSkills = queryAllSessionDBs(db =>
972
- safeAll(db, `SELECT DISTINCT data as skill FROM session_events WHERE category = 'skill' AND data != ''`)
973
- );
974
- const skillSet = [...new Set(uniqueSkills.map(r => r.skill).filter(Boolean))];
975
-
976
- const modeDistribution = queryAllSessionDBs(db =>
977
- safeAll(db, `SELECT data as mode, COUNT(*) as count FROM session_events WHERE category = 'intent' AND data != '' GROUP BY data ORDER BY count DESC`)
978
- );
979
- const mergedModes = mergeByKey(modeDistribution, "mode", (a, b) => ({ mode: a.mode, count: a.count + b.count }))
980
- .sort((a, b) => b.count - a.count);
981
- const totalModeEvents = mergedModes.reduce((s, m) => s + m.count, 0);
982
- const modesWithPct = mergedModes.map(m => ({ ...m, pct: totalModeEvents > 0 ? Math.round(1000 * m.count / totalModeEvents) / 10 : 0 }));
983
-
984
- const blockerData = queryAllSessionDBs(db =>
985
- safeAll(db, `SELECT type, COUNT(*) as count FROM session_events WHERE category = 'blocked-on' GROUP BY type`)
986
- );
987
- const mergedBlockers = mergeByKey(blockerData, "type", (a, b) => ({ type: a.type, count: a.count + b.count }));
988
- const totalBlockers = mergedBlockers.find(b => b.type === "blocker")?.count || 0;
989
- const resolvedBlockers = mergedBlockers.find(b => b.type === "blocker_resolved")?.count || 0;
990
-
991
- const ruleCat = categories.find(c => c.category === "rule");
992
- const ruleCount = ruleCat ? ruleCat.count : 0;
993
-
994
- // Unique rule files: count distinct data values for rule category
995
- const uniqueRuleFiles = queryAllSessionDBs(db =>
996
- safeAll(db, `SELECT DISTINCT data FROM session_events WHERE category = 'rule' AND type = 'rule' AND data IS NOT NULL AND data != ''`)
997
- );
998
- const uniqueRuleCount = new Set(uniqueRuleFiles.map(r => r.data)).size;
999
-
1000
- // Sessions + compacts in one query (avoids extra DB open cycle)
1001
- const sessionAgg = queryAllSessionDBs(db =>
1002
- safeAll(db, `SELECT COUNT(*) as cnt, COALESCE(SUM(compact_count), 0) as compacts FROM session_meta`)
1003
- );
1004
- const totalSessions = sessionAgg.reduce((s, r) => s + (r.cnt || 0), 0);
1005
- const totalCompacts = sessionAgg.reduce((s, r) => s + (r.compacts || 0), 0);
1006
- const compactRate = totalSessions > 0 ? Math.round(100 * totalCompacts / totalSessions) : 0;
1007
-
1008
- const contextHealth = {
1009
- uniqueRuleFiles: uniqueRuleCount,
1010
- ruleLoadsPerSession: totalSessions > 0 ? Math.round(100 * ruleCount / totalSessions) / 100 : 0,
1011
- uniqueSkills: skillSet.length,
1012
- skillList: skillSet,
1013
- modeDistribution: modesWithPct,
1014
- compactRate,
1015
- totalBlockers,
1016
- resolvedBlockers,
1017
- blockerResolutionRate: totalBlockers > 0 ? Math.round(1000 * resolvedBlockers / totalBlockers) / 10 : 0,
1018
- };
1019
-
1020
- // 7. File activity intelligence
1021
- const fileStats = queryAllSessionDBs(db =>
1022
- safeAll(db, `SELECT
1023
- SUM(CASE WHEN type = 'file_read' THEN 1 ELSE 0 END) as reads,
1024
- SUM(CASE WHEN type IN ('file_write','file_edit') THEN 1 ELSE 0 END) as writes,
1025
- SUM(CASE WHEN type IN ('file_glob','file_search') THEN 1 ELSE 0 END) as exploration,
1026
- COUNT(*) as total
1027
- FROM session_events WHERE category = 'file'`)
1028
- );
1029
- const reads = fileStats.reduce((s, r) => s + (r.reads || 0), 0);
1030
- const writes = fileStats.reduce((s, r) => s + (r.writes || 0), 0);
1031
- const exploration = fileStats.reduce((s, r) => s + (r.exploration || 0), 0);
1032
- const totalFileEvents = fileStats.reduce((s, r) => s + (r.total || 0), 0);
1033
-
1034
- const hotFiles = queryAllSessionDBs(db =>
1035
- safeAll(db, `SELECT data as file, COUNT(*) as touches
1036
- FROM session_events
1037
- WHERE category = 'file' AND type IN ('file_read','file_edit','file_write') AND data != ''
1038
- GROUP BY data HAVING COUNT(*) > 3
1039
- ORDER BY touches DESC LIMIT 10`)
1040
- );
1041
- const mergedHotFiles = mergeByKey(hotFiles, "file", (a, b) => ({ file: a.file, touches: a.touches + b.touches }))
1042
- .filter(f => f.touches > 3)
1043
- .sort((a, b) => b.touches - a.touches)
1044
- .slice(0, 10);
1045
-
1046
- // Unique edited + read files in one query for churn rate
1047
- const fileChurnData = queryAllSessionDBs(db =>
1048
- safeAll(db, `SELECT
1049
- COUNT(DISTINCT CASE WHEN type IN ('file_write','file_edit') THEN data END) as edited,
1050
- COUNT(DISTINCT CASE WHEN type = 'file_read' THEN data END) as read_files
1051
- FROM session_events WHERE category = 'file' AND data != ''`)
1052
- );
1053
- const uniqueEditedCount = fileChurnData.reduce((s, r) => s + (r.edited || 0), 0);
1054
- const uniqueReadCount = fileChurnData.reduce((s, r) => s + (r.read_files || 0), 0);
1055
-
1056
- const fileIntelligence = {
1057
- readWriteRatio: writes > 0 ? Math.round(100 * reads / writes) / 100 : reads,
1058
- explorationDepth: totalFileEvents > 0 ? Math.round(1000 * exploration / totalFileEvents) / 10 : 0,
1059
- hotFiles: mergedHotFiles,
1060
- fileChurnRate: uniqueReadCount > 0 ? Math.round(100 * uniqueEditedCount / uniqueReadCount) / 100 : 0,
1061
- };
1062
-
1063
- // 8. Composite scores (0-100)
1064
- const totalEvents = categories.reduce((s, c) => s + c.count, 0);
1065
-
1066
- // Productivity
1067
- const commitSessions = queryAllSessionDBs(db =>
1068
- safeAll(db, `SELECT COUNT(DISTINCT session_id) as cnt FROM session_events WHERE category = 'git' AND data = 'commit'`)
1069
- );
1070
- const sessionsWithCommits = commitSessions.reduce((s, r) => s + (r.cnt || 0), 0);
1071
- const commitRate = totalSessions > 0 ? (sessionsWithCommits / totalSessions) * 100 : 0;
1072
- // delegationRate: ratio of sessions with subagents (not events — avoids tiny % problem)
1073
- const delegationRate = totalSessions > 0 ? Math.min(100, (launched / totalSessions) * 100) : 0;
1074
- // fileChurnRate: unique edited / unique read (not count/events — fixes dimensional mismatch)
1075
- const fileChurnForScore = uniqueReadCount > 0 ? Math.min(100, (uniqueEditedCount / uniqueReadCount) * 100) : 0;
1076
- const productivityScore = Math.min(100, Math.round(
1077
- (commitRate * 0.3) + (delegationRate * 0.2) + ((100 - fileChurnForScore) * 0.2) + (resolutionRate * 0.3)
1078
- ));
1079
-
1080
- // Quality — zero errors = perfect quality (100), not penalized
1081
- const retryRate = totalSessions > 0 ? (retryStorms.length / totalSessions) * 100 : 0;
1082
- const errorRate = totalEvents > 0 ? (totalErrors / totalEvents) * 100 : 0;
1083
- const effectiveResolution = totalErrors === 0 ? 100 : resolutionRate; // no errors = perfect
1084
- const qualityScore = Math.min(100, Math.round(
1085
- (effectiveResolution * 0.4) + ((100 - retryRate) * 0.3) + ((100 - errorRate) * 0.3)
1086
- ));
1087
-
1088
- // Delegation
1089
- const agentFindingCat = categories.find(c => c.category === "agent-finding");
1090
- const findingCount = agentFindingCat ? agentFindingCat.count : 0;
1091
- const hasBursts = parallelBursts > 0 ? 100 : 0;
1092
- const delegationScore = Math.min(100, Math.round(
1093
- (completionRate * 0.5) + (hasBursts * 0.3) + (Math.min(findingCount / 5, 1) * 20)
1094
- ));
1095
-
1096
- // Context Health
1097
- const ruleFreshness = uniqueRuleCount > 0 ? 100 : 0;
1098
- const skillDiversity = Math.min(skillSet.length * 20, 100);
1099
- const planApprovalForScore = (planApproved + planRejected) > 0 ? planApprovalRate : 50;
1100
- const modeBalance = mergedModes.length > 1 ? 100 : 50;
1101
- const contextHealthScore = Math.min(100, Math.round(
1102
- (ruleFreshness * 0.3) + (skillDiversity * 0.2) + (planApprovalForScore * 0.25) + (modeBalance * 0.25)
1103
- ));
1104
-
1105
- const compositeScores = {
1106
- productivity: productivityScore,
1107
- quality: qualityScore,
1108
- delegation: delegationScore,
1109
- contextHealth: contextHealthScore,
1110
- };
1111
-
1112
- const insufficientData = totalEvents < 50 || totalSessions < 3;
1113
-
1114
- return {
1115
- categories,
1116
- errorIntelligence,
1117
- delegation,
1118
- governance,
1119
- gitProductivity,
1120
- contextHealth,
1121
- fileIntelligence,
1122
- compositeScores,
1123
- insufficientData,
1124
- };
1125
- }
1126
-
1127
- // ── Router ───────────────────────────────────────────────
1128
-
1129
- function route(method, pathname, params) {
1130
- if (pathname === "/api/overview") return apiOverview();
1131
- if (pathname === "/api/analytics") return cached("analytics", apiAnalytics);
1132
- if (pathname === "/api/category-analytics") return cached("category-analytics", apiCategoryAnalytics);
1133
- if (pathname === "/api/content") return apiContentDBs();
1134
- if (pathname === "/api/sessions") return apiSessionDBs();
1135
-
1136
- if (pathname.startsWith("/api/content/") && pathname.includes("/chunks/")) {
1137
- const parts = pathname.split("/");
1138
- if (!isValidHash(parts[3])) return { error: "invalid hash" };
1139
- return apiSourceChunks(parts[3], Number(parts[5]));
1140
- }
1141
- if (pathname === "/api/search") {
1142
- const q = params.get("q");
1143
- if (!q) return { error: "missing q param" };
1144
- return apiSearchAll(q);
1145
- }
1146
- if (pathname.startsWith("/api/sessions/") && pathname.includes("/events/")) {
1147
- const parts = pathname.split("/");
1148
- if (!isValidHash(parts[3])) return { error: "invalid hash" };
1149
- return apiSessionEvents(parts[3], decodeURIComponent(parts[5]));
1150
- }
1151
- if (method === "DELETE" && pathname.startsWith("/api/content/")) {
1152
- const parts = pathname.split("/");
1153
- if (!isValidHash(parts[3])) return { error: "invalid hash" };
1154
- return apiDeleteSource(parts[3], Number(parts[5]));
1155
- }
1156
- return null;
1157
- }
1158
-
1159
- // ── Static file serving ──────────────────────────────────
1160
-
1161
- const MIME = {
1162
- ".html": "text/html", ".js": "application/javascript", ".css": "text/css",
1163
- ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
1164
- ".woff2": "font/woff2", ".woff": "font/woff", ".ico": "image/x-icon",
1165
- };
1166
-
1167
- function serveStaticFile(pathname) {
1168
- const ext = extname(pathname);
1169
- const filePath = join(DIST_DIR, pathname);
1170
- try {
1171
- const content = readFileSync(filePath);
1172
- return { content, type: MIME[ext] || "application/octet-stream" };
1173
- } catch { return null; }
1174
- }
1175
-
1176
- // ── On-demand build: install + build if dist/ is missing ─
1177
- if (!existsSync(join(DIST_DIR, "index.html"))) {
1178
- const { execSync } = await import("node:child_process");
1179
- const shellOpts = { cwd: __dirname, stdio: "pipe", shell: true };
1180
- try {
1181
- console.error("\n ┌─ Insight Dashboard ─────────────────────────────┐");
1182
- console.error(" │ First run — building the dashboard UI. │");
1183
- console.error(" │ This only happens once. │");
1184
- console.error(" └─────────────────────────────────────────────────┘\n");
1185
- console.error(" [1/2] Installing dependencies...");
1186
- execSync("npm install --no-package-lock --no-save --silent", { ...shellOpts, timeout: 120000 });
1187
- console.error(" [2/2] Building dashboard...");
1188
- execSync("npm run build", { ...shellOpts, timeout: 60000 });
1189
- console.error(" ✓ Ready.\n");
1190
- } catch (e) {
1191
- console.error(" ✗ Build failed:", e.message);
1192
- console.error(" Try manually: cd insight && npm install && npm run build");
1193
- process.exit(1);
1194
- }
1195
- }
1196
-
1197
- // ── Server (dual runtime) ────────────────────────────────
1198
-
1199
- const indexHTML = readFileSync(join(DIST_DIR, "index.html"), "utf8");
1200
- const API_JSON_HEADERS = { "Content-Type": "application/json" };
1201
-
1202
- if (isBun) {
1203
- // Bun: use Bun.serve
1204
- Bun.serve({
1205
- port: PORT,
1206
- hostname: "127.0.0.1",
1207
- fetch(req) {
1208
- const url = new URL(req.url);
1209
- const data = route(req.method, url.pathname, url.searchParams);
1210
- if (data !== null) {
1211
- return new Response(JSON.stringify(data), {
1212
- headers: API_JSON_HEADERS,
1213
- });
1214
- }
1215
- if (url.pathname.startsWith("/assets/") || url.pathname.match(/\.\w{2,4}$/)) {
1216
- const file = serveStaticFile(url.pathname);
1217
- if (file) return new Response(file.content, {
1218
- headers: { "Content-Type": file.type, "Cache-Control": "public, max-age=31536000" },
1219
- });
1220
- }
1221
- return new Response(indexHTML, { headers: { "Content-Type": "text/html" } });
1222
- },
1223
- });
1224
- } else {
1225
- // Node: use http.createServer
1226
- const server = createHttpServer((req, res) => {
1227
- const url = new URL(req.url, `http://localhost:${PORT}`);
1228
- if (req.method === "OPTIONS") { res.writeHead(405); res.end(); return; }
1229
-
1230
- const data = route(req.method, url.pathname, url.searchParams);
1231
- if (data !== null) {
1232
- res.writeHead(200, { "Content-Type": "application/json" });
1233
- res.end(JSON.stringify(data));
1234
- return;
1235
- }
1236
- if (url.pathname.startsWith("/assets/") || url.pathname.match(/\.\w{2,4}$/)) {
1237
- const file = serveStaticFile(url.pathname);
1238
- if (file) {
1239
- res.writeHead(200, { "Content-Type": file.type, "Cache-Control": "public, max-age=31536000" });
1240
- res.end(file.content);
1241
- return;
1242
- }
1243
- }
1244
- res.writeHead(200, { "Content-Type": "text/html" });
1245
- res.end(indexHTML);
1246
- });
1247
- server.listen(PORT, "127.0.0.1");
1248
- }
1249
-
1250
- // Parent watchdog: exit when the MCP process that spawned us disappears.
1251
- // Fallback for SIGKILL / crash paths where shutdown() cannot run.
1252
- const PARENT_PID = Number(process.env.INSIGHT_PARENT_PID);
1253
- if (Number.isFinite(PARENT_PID) && PARENT_PID > 0) {
1254
- setInterval(() => {
1255
- try {
1256
- process.kill(PARENT_PID, 0);
1257
- } catch {
1258
- process.exit(0);
1259
- }
1260
- }, 5000).unref();
1261
- }
1262
-
1263
- console.log(`\n context-mode Insight`);
1264
- console.log(` http://localhost:${PORT}`);
1265
- console.log(` Runtime: ${isBun ? "Bun" : "Node.js"}\n`);