context-mode 1.0.88 → 1.0.90

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 (132) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +184 -60
  6. package/build/adapters/antigravity/index.d.ts +3 -5
  7. package/build/adapters/antigravity/index.js +7 -35
  8. package/build/adapters/base.d.ts +27 -0
  9. package/build/adapters/base.js +59 -0
  10. package/build/adapters/claude-code/index.d.ts +9 -25
  11. package/build/adapters/claude-code/index.js +27 -141
  12. package/build/adapters/claude-code-base.d.ts +49 -0
  13. package/build/adapters/claude-code-base.js +113 -0
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +21 -14
  16. package/build/adapters/codex/hooks.js +22 -15
  17. package/build/adapters/codex/index.d.ts +6 -10
  18. package/build/adapters/codex/index.js +13 -43
  19. package/build/adapters/copilot-base.d.ts +78 -0
  20. package/build/adapters/copilot-base.js +281 -0
  21. package/build/adapters/cursor/index.d.ts +3 -5
  22. package/build/adapters/cursor/index.js +6 -34
  23. package/build/adapters/detect.d.ts +7 -0
  24. package/build/adapters/detect.js +57 -56
  25. package/build/adapters/gemini-cli/index.d.ts +3 -5
  26. package/build/adapters/gemini-cli/index.js +7 -35
  27. package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
  28. package/build/adapters/jetbrains-copilot/config.js +8 -0
  29. package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
  30. package/build/adapters/jetbrains-copilot/hooks.js +82 -0
  31. package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
  32. package/build/adapters/jetbrains-copilot/index.js +119 -0
  33. package/build/adapters/kiro/hooks.d.ts +14 -0
  34. package/build/adapters/kiro/hooks.js +23 -0
  35. package/build/adapters/kiro/index.d.ts +3 -5
  36. package/build/adapters/kiro/index.js +10 -38
  37. package/build/adapters/openclaw/index.d.ts +3 -4
  38. package/build/adapters/openclaw/index.js +6 -22
  39. package/build/adapters/opencode/index.d.ts +2 -3
  40. package/build/adapters/opencode/index.js +5 -16
  41. package/build/adapters/qwen-code/index.d.ts +39 -0
  42. package/build/adapters/qwen-code/index.js +199 -0
  43. package/build/adapters/types.d.ts +1 -1
  44. package/build/adapters/vscode-copilot/index.d.ts +16 -46
  45. package/build/adapters/vscode-copilot/index.js +29 -320
  46. package/build/adapters/zed/index.d.ts +3 -5
  47. package/build/adapters/zed/index.js +7 -35
  48. package/build/cli.js +113 -47
  49. package/build/lifecycle.d.ts +23 -0
  50. package/build/lifecycle.js +54 -13
  51. package/build/opencode-plugin.d.ts +19 -7
  52. package/build/opencode-plugin.js +19 -7
  53. package/build/pi-extension.js +24 -7
  54. package/build/runtime.js +24 -9
  55. package/build/security.d.ts +17 -1
  56. package/build/security.js +40 -6
  57. package/build/server.js +129 -21
  58. package/build/session/analytics.d.ts +8 -7
  59. package/build/session/analytics.js +95 -75
  60. package/build/session/db.d.ts +10 -1
  61. package/build/session/db.js +67 -8
  62. package/build/session/extract.js +10 -2
  63. package/build/session/project-attribution.d.ts +73 -0
  64. package/build/session/project-attribution.js +231 -0
  65. package/build/store.d.ts +7 -0
  66. package/build/store.js +117 -18
  67. package/build/truncate.d.ts +6 -0
  68. package/build/truncate.js +51 -29
  69. package/build/types.d.ts +8 -0
  70. package/cli.bundle.mjs +157 -136
  71. package/configs/antigravity/GEMINI.md +31 -36
  72. package/configs/claude-code/CLAUDE.md +31 -37
  73. package/configs/codex/AGENTS.md +35 -49
  74. package/configs/cursor/context-mode.mdc +24 -25
  75. package/configs/gemini-cli/GEMINI.md +30 -36
  76. package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
  77. package/configs/jetbrains-copilot/hooks.json +16 -0
  78. package/configs/jetbrains-copilot/mcp.json +8 -0
  79. package/configs/kilo/AGENTS.md +30 -36
  80. package/configs/kiro/KIRO.md +30 -36
  81. package/configs/kiro/agent.json +1 -1
  82. package/configs/openclaw/AGENTS.md +30 -36
  83. package/configs/opencode/AGENTS.md +30 -36
  84. package/configs/pi/AGENTS.md +31 -36
  85. package/configs/qwen-code/QWEN.md +63 -0
  86. package/configs/vscode-copilot/copilot-instructions.md +30 -36
  87. package/configs/zed/AGENTS.md +31 -36
  88. package/hooks/codex/posttooluse.mjs +7 -7
  89. package/hooks/codex/pretooluse.mjs +3 -3
  90. package/hooks/codex/sessionstart.mjs +2 -1
  91. package/hooks/core/formatters.mjs +24 -0
  92. package/hooks/core/routing.mjs +40 -15
  93. package/hooks/core/tool-naming.mjs +2 -0
  94. package/hooks/cursor/posttooluse.mjs +7 -7
  95. package/hooks/cursor/pretooluse.mjs +3 -3
  96. package/hooks/cursor/sessionstart.mjs +2 -1
  97. package/hooks/cursor/stop.mjs +2 -2
  98. package/hooks/ensure-deps.mjs +22 -10
  99. package/hooks/gemini-cli/aftertool.mjs +8 -8
  100. package/hooks/gemini-cli/beforetool.mjs +3 -2
  101. package/hooks/gemini-cli/precompress.mjs +2 -2
  102. package/hooks/gemini-cli/sessionstart.mjs +12 -4
  103. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  104. package/hooks/jetbrains-copilot/precompact.mjs +54 -0
  105. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  106. package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
  107. package/hooks/kiro/posttooluse.mjs +6 -7
  108. package/hooks/kiro/pretooluse.mjs +3 -2
  109. package/hooks/posttooluse.mjs +8 -8
  110. package/hooks/precompact.mjs +3 -4
  111. package/hooks/pretooluse.mjs +43 -20
  112. package/hooks/routing-block.mjs +35 -33
  113. package/hooks/session-attribution.bundle.mjs +1 -0
  114. package/hooks/session-db.bundle.mjs +27 -8
  115. package/hooks/session-extract.bundle.mjs +2 -1
  116. package/hooks/session-helpers.mjs +44 -3
  117. package/hooks/session-loaders.mjs +37 -0
  118. package/hooks/session-snapshot.bundle.mjs +14 -14
  119. package/hooks/sessionstart.mjs +5 -5
  120. package/hooks/userpromptsubmit.mjs +26 -9
  121. package/hooks/vscode-copilot/posttooluse.mjs +8 -8
  122. package/hooks/vscode-copilot/precompact.mjs +2 -2
  123. package/hooks/vscode-copilot/pretooluse.mjs +3 -2
  124. package/hooks/vscode-copilot/sessionstart.mjs +2 -2
  125. package/insight/server.mjs +262 -32
  126. package/insight/src/lib/api.ts +2 -1
  127. package/insight/src/routes/index.tsx +16 -3
  128. package/insight/src/routes/search.tsx +1 -1
  129. package/openclaw.plugin.json +1 -1
  130. package/package.json +11 -2
  131. package/server.bundle.mjs +117 -99
  132. package/skills/ctx-insight/SKILL.md +1 -1
@@ -10,32 +10,34 @@ import "../ensure-deps.mjs";
10
10
  * Must be fast (<20ms). No network, no LLM, just SQLite writes.
11
11
  */
12
12
 
13
- import { createSessionLoaders } from "../session-loaders.mjs";
14
- import { readStdin, getSessionId, getSessionDBPath, getProjectDir, VSCODE_OPTS } from "../session-helpers.mjs";
13
+ import { createSessionLoaders, attributeAndInsertEvents } from "../session-loaders.mjs";
14
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, getInputProjectDir, VSCODE_OPTS } from "../session-helpers.mjs";
15
15
  import { appendFileSync } from "node:fs";
16
16
  import { join, dirname } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { homedir } from "node:os";
19
19
 
20
20
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
21
- const { loadSessionDB, loadExtract } = createSessionLoaders(HOOK_DIR);
21
+ const { loadSessionDB, loadExtract, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
22
22
  const OPTS = VSCODE_OPTS;
23
23
  const DEBUG_LOG = join(homedir(), ".vscode", "context-mode", "posttooluse-debug.log");
24
24
 
25
25
  try {
26
26
  const raw = await readStdin();
27
- const input = JSON.parse(raw);
27
+ const input = parseStdin(raw);
28
+ const projectDir = getInputProjectDir(input, OPTS);
28
29
 
29
30
  appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] CALL: ${input.tool_name}\n`);
30
31
 
31
32
  const { extractEvents } = await loadExtract();
33
+ const { resolveProjectAttributions } = await loadProjectAttribution();
32
34
  const { SessionDB } = await loadSessionDB();
33
35
 
34
36
  const dbPath = getSessionDBPath(OPTS);
35
37
  const db = new SessionDB({ dbPath });
36
38
  const sessionId = getSessionId(input, OPTS);
37
39
 
38
- db.ensureSession(sessionId, getProjectDir(OPTS));
40
+ db.ensureSession(sessionId, projectDir);
39
41
 
40
42
  const events = extractEvents({
41
43
  tool_name: input.tool_name,
@@ -46,9 +48,7 @@ try {
46
48
  tool_output: input.tool_output,
47
49
  });
48
50
 
49
- for (const event of events) {
50
- db.insertEvent(sessionId, event, "PostToolUse");
51
- }
51
+ attributeAndInsertEvents(db, sessionId, events, input, projectDir, "PostToolUse", resolveProjectAttributions);
52
52
 
53
53
  appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] OK: ${input.tool_name} → ${events.length} events\n`);
54
54
  db.close();
@@ -10,7 +10,7 @@ import "../ensure-deps.mjs";
10
10
  */
11
11
 
12
12
  import { createSessionLoaders } from "../session-loaders.mjs";
13
- import { readStdin, getSessionId, getSessionDBPath, VSCODE_OPTS } from "../session-helpers.mjs";
13
+ import { readStdin, parseStdin, getSessionId, getSessionDBPath, VSCODE_OPTS } from "../session-helpers.mjs";
14
14
  import { appendFileSync } from "node:fs";
15
15
  import { join, dirname } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
@@ -23,7 +23,7 @@ const DEBUG_LOG = join(homedir(), ".vscode", "context-mode", "precompact-debug.l
23
23
 
24
24
  try {
25
25
  const raw = await readStdin();
26
- const input = JSON.parse(raw);
26
+ const input = parseStdin(raw);
27
27
 
28
28
  const { buildResumeSnapshot } = await loadSnapshot();
29
29
  const { SessionDB } = await loadSessionDB();
@@ -10,16 +10,17 @@ import { fileURLToPath } from "node:url";
10
10
  import { readStdin } from "../core/stdin.mjs";
11
11
  import { routePreToolUse, initSecurity } from "../core/routing.mjs";
12
12
  import { formatDecision } from "../core/formatters.mjs";
13
+ import { parseStdin, getSessionId, VSCODE_OPTS } from "../session-helpers.mjs";
13
14
 
14
15
  const __hookDir = dirname(fileURLToPath(import.meta.url));
15
16
  await initSecurity(resolve(__hookDir, "..", "..", "build"));
16
17
 
17
18
  const raw = await readStdin();
18
- const input = JSON.parse(raw);
19
+ const input = parseStdin(raw);
19
20
  const tool = input.tool_name ?? "";
20
21
  const toolInput = input.tool_input ?? {};
21
22
 
22
- const decision = routePreToolUse(tool, toolInput, process.env.VSCODE_CWD || process.env.CLAUDE_PROJECT_DIR, "vscode-copilot");
23
+ const decision = routePreToolUse(tool, toolInput, process.env.VSCODE_CWD || process.env.CLAUDE_PROJECT_DIR, "vscode-copilot", getSessionId(input, VSCODE_OPTS));
23
24
  const response = formatDecision("vscode-copilot", decision);
24
25
  if (response !== null) {
25
26
  process.stdout.write(JSON.stringify(response) + "\n");
@@ -19,7 +19,7 @@ const toolNamer = createToolNamer("vscode-copilot");
19
19
  const ROUTING_BLOCK = createRoutingBlock(toolNamer);
20
20
  import { writeSessionEventsFile, buildSessionDirective, getSessionEvents, getLatestSessionEvents } from "../session-directive.mjs";
21
21
  import {
22
- readStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
22
+ readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
23
23
  getProjectDir, VSCODE_OPTS,
24
24
  } from "../session-helpers.mjs";
25
25
  import { join } from "node:path";
@@ -35,7 +35,7 @@ let additionalContext = ROUTING_BLOCK;
35
35
 
36
36
  try {
37
37
  const raw = await readStdin();
38
- const input = JSON.parse(raw);
38
+ const input = parseStdin(raw);
39
39
  const source = input.source ?? "startup";
40
40
 
41
41
  if (source === "compact") {
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { readFileSync, readdirSync, statSync, existsSync, mkdirSync } from "node:fs";
12
- import { join, dirname, extname } from "node:path";
12
+ import { join, dirname, extname, normalize } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { createServer as createHttpServer } from "node:http";
@@ -28,18 +28,28 @@ if (isBun) {
28
28
  } else {
29
29
  try {
30
30
  Database = (await import("better-sqlite3")).default;
31
- } catch {
32
- console.error("\n Error: better-sqlite3 not found.");
33
- console.error(" Install it: npm install better-sqlite3");
34
- console.error(" Or use Bun: bun insight/server.mjs\n");
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
+ }
35
46
  process.exit(1);
36
47
  }
37
48
  }
38
49
 
39
50
  // ── Paths ────────────────────────────────────────────────
40
- const BASE = join(homedir(), ".claude", "context-mode");
41
- const CONTENT_DIR = join(BASE, "content");
42
- const SESSION_DIR = join(BASE, "sessions");
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");
43
53
  const DIST_DIR = join(__dirname, "dist");
44
54
 
45
55
  // ── SQLite helpers ───────────────────────────────────────
@@ -60,6 +70,107 @@ function safeGet(db, sql, params = []) {
60
70
  try { return db.prepare(sql).get(...params); } catch { return null; }
61
71
  }
62
72
 
73
+ function hasColumn(db, table, column) {
74
+ try {
75
+ const rows = db.prepare(`PRAGMA table_xinfo(${table})`).all();
76
+ return rows.some(r => r.name === column);
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ const UNKNOWN_PROJECT_KEY = "__unknown__";
83
+
84
+ function normalizeFsPath(path) {
85
+ const norm = normalize(String(path || "")).replace(/\\/g, "/");
86
+ if (norm.length <= 1) return norm;
87
+ return norm.replace(/\/+$/, "");
88
+ }
89
+
90
+ function parseFileSearchPath(data) {
91
+ const marker = " in ";
92
+ const idx = String(data || "").lastIndexOf(marker);
93
+ if (idx < 0) return null;
94
+ const p = String(data || "").slice(idx + marker.length).trim();
95
+ return p || null;
96
+ }
97
+
98
+ function isLikelyPath(value) {
99
+ const v = String(value || "");
100
+ return v.includes("/") || v.includes("\\") || v.startsWith(".") || /^[A-Za-z]:[\\/]/.test(v);
101
+ }
102
+
103
+ function legacyProjectAttribution(db) {
104
+ const origins = new Map(
105
+ safeAll(db, "SELECT session_id, project_dir FROM session_meta")
106
+ .map((r) => [r.session_id, r.project_dir || UNKNOWN_PROJECT_KEY]),
107
+ );
108
+
109
+ const events = safeAll(db, `SELECT id, session_id, type, data FROM session_events ORDER BY id ASC`);
110
+ const lastProjectBySession = new Map();
111
+ const projectAgg = new Map();
112
+ let unknownEvents = 0;
113
+
114
+ function addProject(projectDir, sessionId) {
115
+ const key = projectDir || UNKNOWN_PROJECT_KEY;
116
+ const existing = projectAgg.get(key) || { project_dir: key, sessionsSet: new Set(), events: 0, compacts: 0, avg_confidence: 0, high_conf_events: 0 };
117
+ existing.events += 1;
118
+ existing.sessionsSet.add(sessionId);
119
+ projectAgg.set(key, existing);
120
+ }
121
+
122
+ for (const ev of events) {
123
+ const sessionId = ev.session_id;
124
+ const origin = origins.get(sessionId) || UNKNOWN_PROJECT_KEY;
125
+ const last = lastProjectBySession.get(sessionId) || "";
126
+ let projectDir = "";
127
+
128
+ if (ev.type === "cwd" && isLikelyPath(ev.data)) {
129
+ projectDir = normalizeFsPath(ev.data);
130
+ } else if (ev.type === "file_read" || ev.type === "file_write" || ev.type === "file_edit" || ev.type === "rule") {
131
+ if (isLikelyPath(ev.data)) {
132
+ const p = normalizeFsPath(ev.data);
133
+ if (origin !== UNKNOWN_PROJECT_KEY && (p === origin || p.startsWith(`${origin}/`))) projectDir = origin;
134
+ else projectDir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : p;
135
+ }
136
+ } else if (ev.type === "file_search") {
137
+ const p = parseFileSearchPath(ev.data);
138
+ if (p && isLikelyPath(p)) {
139
+ const pp = normalizeFsPath(p);
140
+ if (origin !== UNKNOWN_PROJECT_KEY && (pp === origin || pp.startsWith(`${origin}/`))) projectDir = origin;
141
+ else projectDir = pp;
142
+ }
143
+ }
144
+
145
+ if (!projectDir) {
146
+ projectDir = last || origin || UNKNOWN_PROJECT_KEY;
147
+ }
148
+ if (!projectDir || projectDir === UNKNOWN_PROJECT_KEY) unknownEvents += 1;
149
+
150
+ addProject(projectDir, sessionId);
151
+ if (projectDir && projectDir !== UNKNOWN_PROJECT_KEY) {
152
+ lastProjectBySession.set(sessionId, projectDir);
153
+ }
154
+ }
155
+
156
+ const rows = [...projectAgg.values()].map((r) => ({
157
+ project_dir: r.project_dir,
158
+ sessions: r.sessionsSet.size,
159
+ events: r.events,
160
+ compacts: 0,
161
+ avg_confidence: 0,
162
+ high_conf_events: 0,
163
+ })).sort((a, b) => b.events - a.events);
164
+
165
+ return {
166
+ projectRows: rows,
167
+ total_events: events.length,
168
+ unknown_events: unknownEvents,
169
+ avg_confidence: 0,
170
+ high_conf_events: 0,
171
+ };
172
+ }
173
+
63
174
  function listDBFiles(dir) {
64
175
  if (!existsSync(dir)) return [];
65
176
  return readdirSync(dir)
@@ -103,6 +214,11 @@ function mergeByKey(arr, key, mergeFn) {
103
214
  return [...map.values()];
104
215
  }
105
216
 
217
+ // ── Input validation ────────────────────────────────────
218
+ function isValidHash(hash) {
219
+ return /^[a-f0-9_]+$/.test(hash);
220
+ }
221
+
106
222
  // ── API Handlers ─────────────────────────────────────────
107
223
 
108
224
  function apiOverview() {
@@ -347,13 +463,21 @@ function apiAnalytics() {
347
463
  GROUP BY se.session_id, se.data HAVING edit_count > 1
348
464
  ORDER BY edit_count DESC LIMIT 20`)
349
465
  );
350
- const gitActivity = queryAllSessionDBs(db =>
351
- safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
352
- sm.project_dir, sm.started_at as session_start
466
+ const gitActivity = queryAllSessionDBs(db => {
467
+ if (hasColumn(db, "session_events", "project_dir")) {
468
+ return safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
469
+ COALESCE(NULLIF(se.project_dir, ''), sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
470
+ sm.started_at as session_start
471
+ FROM session_events se
472
+ LEFT JOIN session_meta sm ON se.session_id = sm.session_id
473
+ WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`);
474
+ }
475
+ return safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
476
+ COALESCE(sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir, sm.started_at as session_start
353
477
  FROM session_events se
354
- JOIN session_meta sm ON se.session_id = sm.session_id
355
- WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`)
356
- );
478
+ LEFT JOIN session_meta sm ON se.session_id = sm.session_id
479
+ WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`);
480
+ });
357
481
  const rawSubagents = queryAllSessionDBs(db =>
358
482
  safeAll(db, `SELECT data as task, created_at, session_id FROM session_events
359
483
  WHERE type = 'subagent' ORDER BY created_at ASC`)
@@ -378,12 +502,34 @@ function apiAnalytics() {
378
502
  timeSavedMin: parallelBursts.reduce((a, b) => a + (b.length - 1) * 2, 0),
379
503
  burstDetails: parallelBursts.map(b => ({ size: b.length, time: b[0].created_at })),
380
504
  };
381
- const projectActivity = queryAllSessionDBs(db =>
382
- safeAll(db, `SELECT project_dir, COUNT(*) as sessions, SUM(event_count) as events,
383
- SUM(compact_count) as compacts
384
- FROM session_meta WHERE project_dir IS NOT NULL
385
- GROUP BY project_dir ORDER BY events DESC LIMIT 10`)
386
- );
505
+ const projectActivity = queryAllSessionDBs(db => {
506
+ if (hasColumn(db, "session_events", "project_dir")) {
507
+ return safeAll(db, `SELECT
508
+ COALESCE(NULLIF(se.project_dir, ''), '${UNKNOWN_PROJECT_KEY}') as project_dir,
509
+ COUNT(DISTINCT se.session_id) as sessions,
510
+ COUNT(*) as events,
511
+ 0 as compacts,
512
+ AVG(COALESCE(se.attribution_confidence, 0)) as avg_confidence,
513
+ SUM(CASE WHEN COALESCE(se.attribution_confidence, 0) >= 0.8 THEN 1 ELSE 0 END) as high_conf_events
514
+ FROM session_events se
515
+ GROUP BY project_dir
516
+ ORDER BY events DESC
517
+ LIMIT 20`);
518
+ }
519
+ return legacyProjectAttribution(db).projectRows;
520
+ });
521
+
522
+ const attributionSummary = queryAllSessionDBs(db => {
523
+ if (hasColumn(db, "session_events", "project_dir")) {
524
+ return safeAll(db, `SELECT
525
+ COUNT(*) as total_events,
526
+ SUM(CASE WHEN COALESCE(project_dir, '') = '' THEN 1 ELSE 0 END) as unknown_events,
527
+ AVG(COALESCE(attribution_confidence, 0)) as avg_confidence,
528
+ SUM(CASE WHEN COALESCE(attribution_confidence, 0) >= 0.8 THEN 1 ELSE 0 END) as high_conf_events
529
+ FROM session_events`);
530
+ }
531
+ return [legacyProjectAttribution(db)];
532
+ });
387
533
  const hourlyPattern = queryAllSessionDBs(db =>
388
534
  safeAll(db, `SELECT CAST(strftime('%H', created_at) AS INTEGER) as hour, COUNT(*) as count
389
535
  FROM session_events WHERE created_at IS NOT NULL
@@ -426,13 +572,22 @@ function apiAnalytics() {
426
572
  );
427
573
 
428
574
  // 2. Personal Commit Rate — commits per session
429
- const commitRate = queryAllSessionDBs(db =>
430
- safeAll(db, `SELECT sm.session_id, sm.project_dir,
575
+ const commitRate = queryAllSessionDBs(db => {
576
+ if (hasColumn(db, "session_events", "project_dir")) {
577
+ return safeAll(db, `SELECT
578
+ sm.session_id,
579
+ COALESCE(NULLIF(MAX(CASE WHEN se.type = 'git' THEN se.project_dir END), ''), sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
580
+ SUM(CASE WHEN se.type = 'git' AND se.data = 'commit' THEN 1 ELSE 0 END) as commits
581
+ FROM session_meta sm
582
+ LEFT JOIN session_events se ON se.session_id = sm.session_id
583
+ GROUP BY sm.session_id`);
584
+ }
585
+ return safeAll(db, `SELECT sm.session_id, COALESCE(sm.project_dir, '${UNKNOWN_PROJECT_KEY}') as project_dir,
431
586
  SUM(CASE WHEN se.type = 'git' AND se.data = 'commit' THEN 1 ELSE 0 END) as commits
432
587
  FROM session_meta sm
433
588
  LEFT JOIN session_events se ON se.session_id = sm.session_id
434
- GROUP BY sm.session_id`)
435
- );
589
+ GROUP BY sm.session_id`);
590
+ });
436
591
 
437
592
  // 3. Sandbox Adoption — context-mode MCP tool usage vs total
438
593
  const sandboxAdoption = queryAllSessionDBs(db =>
@@ -467,6 +622,59 @@ function apiAnalytics() {
467
622
  sandbox_calls: (a.sandbox_calls || 0) + (b.sandbox_calls || 0),
468
623
  total_calls: (a.total_calls || 0) + (b.total_calls || 0),
469
624
  }), { sandbox_calls: 0, total_calls: 0 });
625
+ const attributionSchemaCoverage = queryAllSessionDBs(db => [{
626
+ has_attribution_columns: hasColumn(db, "session_events", "project_dir") ? 1 : 0,
627
+ }]);
628
+ const fallbackOnly = attributionSchemaCoverage.length > 0
629
+ && attributionSchemaCoverage.every((r) => !r.has_attribution_columns);
630
+
631
+ const mergedProjectActivity = mergeByKey(projectActivity, "project_dir", (a, b) => {
632
+ const aEvents = Number(a.events || 0);
633
+ const bEvents = Number(b.events || 0);
634
+ const aWeighted = Number(
635
+ (a.weighted_confidence_sum ?? (Number(a.avg_confidence || 0) * aEvents)) || 0,
636
+ );
637
+ const bWeighted = Number(
638
+ (b.weighted_confidence_sum ?? (Number(b.avg_confidence || 0) * bEvents)) || 0,
639
+ );
640
+ return {
641
+ project_dir: a.project_dir,
642
+ sessions: (a.sessions || 0) + (b.sessions || 0),
643
+ events: aEvents + bEvents,
644
+ compacts: (a.compacts || 0) + (b.compacts || 0),
645
+ weighted_confidence_sum: aWeighted + bWeighted,
646
+ high_conf_events: (a.high_conf_events || 0) + (b.high_conf_events || 0),
647
+ };
648
+ })
649
+ .map((p) => ({
650
+ project_dir: p.project_dir,
651
+ sessions: p.sessions || 0,
652
+ events: p.events || 0,
653
+ compacts: p.compacts || 0,
654
+ avg_confidence: (p.events || 0) > 0 ? (p.weighted_confidence_sum || 0) / p.events : 0,
655
+ high_conf_events: p.high_conf_events || 0,
656
+ }))
657
+ .sort((a, b) => (b.events || 0) - (a.events || 0));
658
+ const nonUnknownProjects = mergedProjectActivity.filter((p) => p.project_dir !== UNKNOWN_PROJECT_KEY);
659
+
660
+ const attributionAgg = attributionSummary.reduce((a, b) => ({
661
+ total_events: (a.total_events || 0) + (b.total_events || 0),
662
+ unknown_events: (a.unknown_events || 0) + (b.unknown_events || 0),
663
+ high_conf_events: (a.high_conf_events || 0) + (b.high_conf_events || 0),
664
+ // weighted sum for avg_confidence
665
+ weighted_confidence_sum: (a.weighted_confidence_sum || 0) + ((b.avg_confidence || 0) * (b.total_events || 0)),
666
+ }), { total_events: 0, unknown_events: 0, high_conf_events: 0, weighted_confidence_sum: 0 });
667
+
668
+ const attributedEvents = Math.max(0, attributionAgg.total_events - attributionAgg.unknown_events);
669
+ const unknownPct = attributionAgg.total_events > 0
670
+ ? Math.round(1000 * attributionAgg.unknown_events / attributionAgg.total_events) / 10
671
+ : 100;
672
+ const avgConfidencePct = attributionAgg.total_events > 0
673
+ ? Math.round(1000 * attributionAgg.weighted_confidence_sum / attributionAgg.total_events) / 10
674
+ : 0;
675
+ const highConfidencePct = attributionAgg.total_events > 0
676
+ ? Math.round(1000 * attributionAgg.high_conf_events / attributionAgg.total_events) / 10
677
+ : 0;
470
678
 
471
679
  return {
472
680
  totals: {
@@ -483,7 +691,7 @@ function apiAnalytics() {
483
691
  totalTasks: tasks.length, totalPrompts: prompts.length,
484
692
  promptsPerSession: sessionDurations.length > 0
485
693
  ? Math.round(10 * prompts.length / sessionDurations.length) / 10 : 0,
486
- uniqueProjects: projectActivity.length,
694
+ uniqueProjects: nonUnknownProjects.length,
487
695
  totalCommits: commitRate.reduce((a, b) => a + (b.commits || 0), 0),
488
696
  commitsPerSession: sessionDurations.length > 0
489
697
  ? Math.round(10 * commitRate.reduce((a, b) => a + (b.commits || 0), 0) / sessionDurations.length) / 10 : 0,
@@ -492,6 +700,15 @@ function apiAnalytics() {
492
700
  totalRules: rulesFreshness.length,
493
701
  totalEditTestCycles: editTestCycles.reduce((a, b) => a + (b.cycles || 0), 0),
494
702
  },
703
+ attribution: {
704
+ totalEvents: attributionAgg.total_events,
705
+ attributedEvents,
706
+ unknownEvents: attributionAgg.unknown_events,
707
+ unknownPct,
708
+ avgConfidencePct,
709
+ highConfidencePct,
710
+ isFallbackOnly: fallbackOnly,
711
+ },
495
712
  sessionsByDate: mergeByKey(sessionsByDate, "date", (a, b) => ({
496
713
  date: a.date, count: a.count + b.count, events: a.events + b.events, compacts: a.compacts + b.compacts
497
714
  })),
@@ -503,9 +720,7 @@ function apiAnalytics() {
503
720
  timeToFirstCommit,
504
721
  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 }),
505
722
  reworkData, gitActivity, subagents,
506
- projectActivity: mergeByKey(projectActivity, "project_dir", (a, b) => ({
507
- project_dir: a.project_dir, sessions: a.sessions + b.sessions, events: a.events + b.events, compacts: (a.compacts||0)+(b.compacts||0)
508
- })).sort((a, b) => b.events - a.events),
723
+ projectActivity: mergedProjectActivity,
509
724
  hourlyPattern: mergeByKey(hourlyPattern, "hour", (a, b) => ({ hour: a.hour, count: a.count + b.count })),
510
725
  weeklyTrend: mergeByKey(weeklyTrend, "week", (a, b) => ({ week: a.week, sessions: a.sessions + b.sessions, events: a.events + b.events })),
511
726
  tasks, prompts,
@@ -530,6 +745,7 @@ function route(method, pathname, params) {
530
745
 
531
746
  if (pathname.startsWith("/api/content/") && pathname.includes("/chunks/")) {
532
747
  const parts = pathname.split("/");
748
+ if (!isValidHash(parts[3])) return { error: "invalid hash" };
533
749
  return apiSourceChunks(parts[3], Number(parts[5]));
534
750
  }
535
751
  if (pathname === "/api/search") {
@@ -539,10 +755,12 @@ function route(method, pathname, params) {
539
755
  }
540
756
  if (pathname.startsWith("/api/sessions/") && pathname.includes("/events/")) {
541
757
  const parts = pathname.split("/");
758
+ if (!isValidHash(parts[3])) return { error: "invalid hash" };
542
759
  return apiSessionEvents(parts[3], decodeURIComponent(parts[5]));
543
760
  }
544
761
  if (method === "DELETE" && pathname.startsWith("/api/content/")) {
545
762
  const parts = pathname.split("/");
763
+ if (!isValidHash(parts[3])) return { error: "invalid hash" };
546
764
  return apiDeleteSource(parts[3], Number(parts[5]));
547
765
  }
548
766
  return null;
@@ -568,6 +786,7 @@ function serveStaticFile(pathname) {
568
786
  // ── Server (dual runtime) ────────────────────────────────
569
787
 
570
788
  const indexHTML = readFileSync(join(DIST_DIR, "index.html"), "utf8");
789
+ const API_JSON_HEADERS = { "Content-Type": "application/json" };
571
790
 
572
791
  if (isBun) {
573
792
  // Bun: use Bun.serve
@@ -579,7 +798,7 @@ if (isBun) {
579
798
  const data = route(req.method, url.pathname, url.searchParams);
580
799
  if (data !== null) {
581
800
  return new Response(JSON.stringify(data), {
582
- headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
801
+ headers: API_JSON_HEADERS,
583
802
  });
584
803
  }
585
804
  if (url.pathname.startsWith("/assets/") || url.pathname.match(/\.\w{2,4}$/)) {
@@ -595,9 +814,7 @@ if (isBun) {
595
814
  // Node: use http.createServer
596
815
  const server = createHttpServer((req, res) => {
597
816
  const url = new URL(req.url, `http://localhost:${PORT}`);
598
- res.setHeader("Access-Control-Allow-Origin", "*");
599
- res.setHeader("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS");
600
- if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
817
+ if (req.method === "OPTIONS") { res.writeHead(405); res.end(); return; }
601
818
 
602
819
  const data = route(req.method, url.pathname, url.searchParams);
603
820
  if (data !== null) {
@@ -619,6 +836,19 @@ if (isBun) {
619
836
  server.listen(PORT, "127.0.0.1");
620
837
  }
621
838
 
839
+ // Parent watchdog: exit when the MCP process that spawned us disappears.
840
+ // Fallback for SIGKILL / crash paths where shutdown() cannot run.
841
+ const PARENT_PID = Number(process.env.INSIGHT_PARENT_PID);
842
+ if (Number.isFinite(PARENT_PID) && PARENT_PID > 0) {
843
+ setInterval(() => {
844
+ try {
845
+ process.kill(PARENT_PID, 0);
846
+ } catch {
847
+ process.exit(0);
848
+ }
849
+ }, 5000).unref();
850
+ }
851
+
622
852
  console.log(`\n context-mode Insight`);
623
853
  console.log(` http://localhost:${PORT}`);
624
854
  console.log(` Runtime: ${isBun ? "Bun" : "Node.js"}\n`);
@@ -40,7 +40,8 @@ export interface AnalyticsData {
40
40
  exploreExecRatio: { explore: number; execute: number; total: number };
41
41
  reworkData: { session_id: string; file: string; edit_count: number }[];
42
42
  gitActivity: { action: string; created_at: string; session_id: string; project_dir: string; session_start: string }[];
43
- projectActivity: { project_dir: string; sessions: number; events: number }[];
43
+ projectActivity: { project_dir: string; sessions: number; events: number; avg_confidence?: number; high_conf_events?: number }[];
44
+ attribution?: { totalEvents: number; attributedEvents: number; unknownEvents: number; unknownPct: number; avgConfidencePct: number; highConfidencePct: number; isFallbackOnly: boolean };
44
45
  hourlyPattern: { hour: number; count: number }[];
45
46
  weeklyTrend: { week: string; sessions: number; events: number }[];
46
47
  tasks: { task: string; created_at: string }[];
@@ -505,18 +505,31 @@ function Dashboard() {
505
505
  <div className="grid grid-cols-3 gap-4 mb-4">
506
506
  <Mini label="Projects" value={t.uniqueProjects} />
507
507
  <Mini label="Top Project" value={topProject?.project_dir?.split("/").pop() || "-"} color="text-emerald-500" />
508
- <Mini label="Top Events" value={topProject?.events || 0} />
508
+ <Mini label="Match Quality" value={data.attribution?.avgConfidencePct != null ? (data.attribution.avgConfidencePct >= 80 ? "Strong" : data.attribution.avgConfidencePct >= 55 ? "Fair" : "Weak") : "-"} color={data.attribution && data.attribution.avgConfidencePct >= 80 ? "text-emerald-500" : data.attribution && data.attribution.avgConfidencePct >= 55 ? "text-amber-500" : "text-red-400"} />
509
509
  </div>
510
+ {data.attribution?.isFallbackOnly && (
511
+ <div className="mb-3 px-3 py-2 rounded-md bg-muted/50 border border-border text-xs text-muted-foreground flex items-center gap-1.5">
512
+ <Lightbulb className="h-3 w-3 shrink-0" />
513
+ Limited tracking detail — project time is estimated from session data
514
+ </div>
515
+ )}
510
516
  <div className="space-y-2.5 pt-2 border-t border-border">
511
517
  {data.projectActivity.slice(0, 6).map((p, i) => {
512
518
  const maxEv = data.projectActivity[0]?.events || 1;
513
519
  const pct = Math.round((p.events / maxEv) * 100);
514
520
  const name = p.project_dir?.split("/").filter(Boolean).slice(-2).join("/") || "Unknown";
521
+ const conf = p.avg_confidence != null ? Math.round(p.avg_confidence * 100) : null;
522
+ const qualityLabel = conf != null ? (conf >= 80 ? "Strong" : conf >= 55 ? "Fair" : "Weak") : null;
523
+ const qualityIcon = conf != null ? (conf >= 80 ? "✓" : conf >= 55 ? "~" : "!") : null;
524
+ const qualityColor = conf != null ? (conf >= 80 ? "text-emerald-500" : conf >= 55 ? "text-amber-500" : "text-red-400") : "";
515
525
  return (
516
526
  <div key={i}>
517
527
  <div className="flex justify-between text-xs mb-1">
518
- <span className="font-mono truncate max-w-[220px]">{name}</span>
519
- <span className="text-muted-foreground tabular-nums">{p.sessions}s · {p.events}e</span>
528
+ <span className="font-mono truncate max-w-[200px]">{name}</span>
529
+ <span className="text-muted-foreground tabular-nums">
530
+ {p.sessions} sessions · {p.events} events
531
+ {qualityLabel != null && <span className={`ml-1.5 text-[11px] font-medium ${qualityColor}`} title={`Match quality: ${conf}%`}>{qualityIcon} {qualityLabel}</span>}
532
+ </span>
520
533
  </div>
521
534
  <div className="h-1.5 bg-secondary rounded-full overflow-hidden">
522
535
  <div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: COLORS[i % COLORS.length] }} />
@@ -79,7 +79,7 @@ function SearchPage() {
79
79
  <pre
80
80
  className="text-xs text-muted-foreground whitespace-pre-wrap break-words font-mono leading-relaxed"
81
81
  dangerouslySetInnerHTML={{
82
- __html: (r.highlighted || esc(r.content))
82
+ __html: (r.highlighted ? esc(r.highlighted) : esc(r.content))
83
83
  .replace(/«/g, '<mark class="bg-amber-500/20 text-foreground rounded px-0.5">')
84
84
  .replace(/»/g, "</mark>"),
85
85
  }}
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.88",
6
+ "version": "1.0.90",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.88",
3
+ "version": "1.0.90",
4
4
  "type": "module",
5
5
  "description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -19,13 +19,22 @@
19
19
  "sandbox",
20
20
  "code-execution",
21
21
  "fts5",
22
- "bm25"
22
+ "bm25",
23
+ "pi-package"
23
24
  ],
24
25
  "repository": {
25
26
  "type": "git",
26
27
  "url": "https://github.com/mksglu/context-mode"
27
28
  },
28
29
  "homepage": "https://github.com/mksglu/context-mode#readme",
30
+ "pi": {
31
+ "extensions": [
32
+ "./build/pi-extension.js"
33
+ ],
34
+ "skills": [
35
+ "./skills"
36
+ ]
37
+ },
29
38
  "openclaw": {
30
39
  "extensions": [
31
40
  "./build/openclaw-plugin.js"