context-mode 1.0.162 → 1.0.164
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +149 -30
- package/bin/statusline.mjs +24 -4
- package/build/adapters/antigravity/index.d.ts +1 -1
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/claude-code/hooks.d.ts +1 -0
- package/build/adapters/claude-code/hooks.js +3 -0
- package/build/adapters/claude-code/index.js +24 -5
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +5 -1
- package/build/adapters/codex/hooks.js +5 -1
- package/build/adapters/codex/index.d.ts +9 -1
- package/build/adapters/codex/index.js +87 -5
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/detect.d.ts +1 -1
- package/build/adapters/detect.js +71 -3
- package/build/adapters/openclaw/mcp-tools.js +1 -1
- package/build/adapters/opencode/index.js +31 -17
- package/build/adapters/opencode/zod3tov4.js +27 -6
- package/build/adapters/pi/extension.d.ts +2 -12
- package/build/adapters/pi/extension.js +128 -109
- package/build/adapters/types.d.ts +5 -4
- package/build/adapters/types.js +4 -3
- package/build/cache-heal.d.ts +48 -0
- package/build/cache-heal.js +150 -0
- package/build/cli.js +37 -97
- package/build/executor.d.ts +25 -0
- package/build/executor.js +143 -22
- package/build/lifecycle.d.ts +48 -0
- package/build/lifecycle.js +111 -0
- package/build/opencode-plugin.js +5 -2
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +0 -36
- package/build/runtime.js +107 -27
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/security.d.ts +73 -3
- package/build/security.js +293 -33
- package/build/server.d.ts +14 -0
- package/build/server.js +441 -354
- package/build/session/analytics.d.ts +1 -1
- package/build/session/analytics.js +5 -1
- package/build/session/db.js +23 -3
- package/build/session/extract.js +78 -0
- package/build/store.d.ts +1 -1
- package/build/store.js +139 -25
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/cli.bundle.mjs +253 -250
- package/configs/antigravity/GEMINI.md +2 -2
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +2 -2
- package/configs/codex/AGENTS.md +2 -2
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/gemini-cli/GEMINI.md +2 -2
- package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
- package/configs/kilo/AGENTS.md +2 -2
- package/configs/kiro/KIRO.md +2 -2
- package/configs/omp/SYSTEM.md +2 -2
- package/configs/openclaw/AGENTS.md +2 -2
- package/configs/opencode/AGENTS.md +2 -2
- package/configs/qwen-code/QWEN.md +2 -2
- package/configs/vscode-copilot/copilot-instructions.md +2 -2
- package/configs/zed/AGENTS.md +2 -2
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/codex/pretooluse.mjs +14 -4
- package/hooks/codex/stop.mjs +12 -4
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +158 -7
- package/hooks/core/mcp-ready.mjs +37 -8
- package/hooks/core/routing.mjs +94 -8
- package/hooks/core/tool-naming.mjs +3 -0
- package/hooks/hooks.json +12 -1
- package/hooks/pretooluse.mjs +6 -2
- package/hooks/routing-block.mjs +3 -4
- package/hooks/security.bundle.mjs +2 -1
- package/hooks/session-db.bundle.mjs +5 -5
- package/hooks/session-directive.mjs +88 -20
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +21 -0
- package/hooks/sessionstart.mjs +37 -5
- package/hooks/stop.mjs +49 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -10
- package/server.bundle.mjs +206 -200
- package/skills/ctx-insight/SKILL.md +12 -17
- package/build/util/db-lock.d.ts +0 -65
- package/build/util/db-lock.js +0 -166
- package/insight/index.html +0 -13
- package/insight/package.json +0 -55
- package/insight/server.mjs +0 -1265
- package/insight/src/components/analytics.tsx +0 -112
- package/insight/src/components/ui/badge.tsx +0 -52
- package/insight/src/components/ui/button.tsx +0 -58
- package/insight/src/components/ui/card.tsx +0 -103
- package/insight/src/components/ui/chart.tsx +0 -371
- package/insight/src/components/ui/collapsible.tsx +0 -19
- package/insight/src/components/ui/input.tsx +0 -20
- package/insight/src/components/ui/progress.tsx +0 -83
- package/insight/src/components/ui/scroll-area.tsx +0 -55
- package/insight/src/components/ui/separator.tsx +0 -23
- package/insight/src/components/ui/table.tsx +0 -114
- package/insight/src/components/ui/tabs.tsx +0 -82
- package/insight/src/components/ui/tooltip.tsx +0 -64
- package/insight/src/lib/api.ts +0 -144
- package/insight/src/lib/utils.ts +0 -6
- package/insight/src/main.tsx +0 -22
- package/insight/src/routeTree.gen.ts +0 -189
- package/insight/src/router.tsx +0 -19
- package/insight/src/routes/__root.tsx +0 -55
- package/insight/src/routes/enterprise.tsx +0 -316
- package/insight/src/routes/index.tsx +0 -1482
- package/insight/src/routes/knowledge.tsx +0 -221
- package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
- package/insight/src/routes/search.tsx +0 -97
- package/insight/src/routes/sessions.tsx +0 -179
- package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
- package/insight/src/styles.css +0 -104
- package/insight/tsconfig.json +0 -29
- package/insight/vite.config.ts +0 -19
package/insight/server.mjs
DELETED
|
@@ -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`);
|