context-mode 1.0.89 → 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 (128) 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 +12 -140
  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 +13 -0
  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/runtime.js +24 -9
  54. package/build/security.d.ts +17 -1
  55. package/build/security.js +40 -6
  56. package/build/server.js +41 -9
  57. package/build/session/analytics.d.ts +8 -7
  58. package/build/session/analytics.js +95 -75
  59. package/build/session/db.d.ts +10 -1
  60. package/build/session/db.js +67 -8
  61. package/build/session/extract.js +10 -2
  62. package/build/session/project-attribution.d.ts +73 -0
  63. package/build/session/project-attribution.js +231 -0
  64. package/build/store.d.ts +4 -0
  65. package/build/store.js +58 -9
  66. package/build/types.d.ts +8 -0
  67. package/cli.bundle.mjs +135 -121
  68. package/configs/antigravity/GEMINI.md +31 -36
  69. package/configs/claude-code/CLAUDE.md +31 -37
  70. package/configs/codex/AGENTS.md +35 -49
  71. package/configs/cursor/context-mode.mdc +24 -25
  72. package/configs/gemini-cli/GEMINI.md +30 -36
  73. package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
  74. package/configs/jetbrains-copilot/hooks.json +16 -0
  75. package/configs/jetbrains-copilot/mcp.json +8 -0
  76. package/configs/kilo/AGENTS.md +30 -36
  77. package/configs/kiro/KIRO.md +30 -36
  78. package/configs/kiro/agent.json +1 -1
  79. package/configs/openclaw/AGENTS.md +30 -36
  80. package/configs/opencode/AGENTS.md +30 -36
  81. package/configs/pi/AGENTS.md +31 -36
  82. package/configs/qwen-code/QWEN.md +63 -0
  83. package/configs/vscode-copilot/copilot-instructions.md +30 -36
  84. package/configs/zed/AGENTS.md +31 -36
  85. package/hooks/codex/posttooluse.mjs +7 -7
  86. package/hooks/codex/pretooluse.mjs +3 -3
  87. package/hooks/codex/sessionstart.mjs +2 -1
  88. package/hooks/core/formatters.mjs +24 -0
  89. package/hooks/core/routing.mjs +40 -15
  90. package/hooks/core/tool-naming.mjs +2 -0
  91. package/hooks/cursor/posttooluse.mjs +7 -7
  92. package/hooks/cursor/pretooluse.mjs +3 -3
  93. package/hooks/cursor/sessionstart.mjs +2 -1
  94. package/hooks/cursor/stop.mjs +2 -2
  95. package/hooks/ensure-deps.mjs +22 -10
  96. package/hooks/gemini-cli/aftertool.mjs +8 -8
  97. package/hooks/gemini-cli/beforetool.mjs +3 -2
  98. package/hooks/gemini-cli/precompress.mjs +2 -2
  99. package/hooks/gemini-cli/sessionstart.mjs +12 -4
  100. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  101. package/hooks/jetbrains-copilot/precompact.mjs +54 -0
  102. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  103. package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
  104. package/hooks/kiro/posttooluse.mjs +6 -7
  105. package/hooks/kiro/pretooluse.mjs +3 -2
  106. package/hooks/posttooluse.mjs +8 -8
  107. package/hooks/precompact.mjs +3 -4
  108. package/hooks/pretooluse.mjs +5 -4
  109. package/hooks/routing-block.mjs +35 -33
  110. package/hooks/session-attribution.bundle.mjs +1 -0
  111. package/hooks/session-db.bundle.mjs +27 -8
  112. package/hooks/session-extract.bundle.mjs +2 -1
  113. package/hooks/session-helpers.mjs +44 -3
  114. package/hooks/session-loaders.mjs +37 -0
  115. package/hooks/sessionstart.mjs +5 -5
  116. package/hooks/userpromptsubmit.mjs +26 -9
  117. package/hooks/vscode-copilot/posttooluse.mjs +8 -8
  118. package/hooks/vscode-copilot/precompact.mjs +2 -2
  119. package/hooks/vscode-copilot/pretooluse.mjs +3 -2
  120. package/hooks/vscode-copilot/sessionstart.mjs +2 -2
  121. package/insight/server.mjs +237 -25
  122. package/insight/src/lib/api.ts +2 -1
  123. package/insight/src/routes/index.tsx +16 -3
  124. package/insight/src/routes/search.tsx +1 -1
  125. package/openclaw.plugin.json +1 -1
  126. package/package.json +11 -2
  127. package/server.bundle.mjs +94 -80
  128. package/skills/ctx-insight/SKILL.md +1 -1
@@ -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";
@@ -70,6 +70,107 @@ function safeGet(db, sql, params = []) {
70
70
  try { return db.prepare(sql).get(...params); } catch { return null; }
71
71
  }
72
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
+
73
174
  function listDBFiles(dir) {
74
175
  if (!existsSync(dir)) return [];
75
176
  return readdirSync(dir)
@@ -362,13 +463,21 @@ function apiAnalytics() {
362
463
  GROUP BY se.session_id, se.data HAVING edit_count > 1
363
464
  ORDER BY edit_count DESC LIMIT 20`)
364
465
  );
365
- const gitActivity = queryAllSessionDBs(db =>
366
- safeAll(db, `SELECT se.data as action, se.created_at, se.session_id,
367
- 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
368
477
  FROM session_events se
369
- JOIN session_meta sm ON se.session_id = sm.session_id
370
- WHERE se.type = 'git' ORDER BY se.created_at DESC LIMIT 20`)
371
- );
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
+ });
372
481
  const rawSubagents = queryAllSessionDBs(db =>
373
482
  safeAll(db, `SELECT data as task, created_at, session_id FROM session_events
374
483
  WHERE type = 'subagent' ORDER BY created_at ASC`)
@@ -393,12 +502,34 @@ function apiAnalytics() {
393
502
  timeSavedMin: parallelBursts.reduce((a, b) => a + (b.length - 1) * 2, 0),
394
503
  burstDetails: parallelBursts.map(b => ({ size: b.length, time: b[0].created_at })),
395
504
  };
396
- const projectActivity = queryAllSessionDBs(db =>
397
- safeAll(db, `SELECT project_dir, COUNT(*) as sessions, SUM(event_count) as events,
398
- SUM(compact_count) as compacts
399
- FROM session_meta WHERE project_dir IS NOT NULL
400
- GROUP BY project_dir ORDER BY events DESC LIMIT 10`)
401
- );
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
+ });
402
533
  const hourlyPattern = queryAllSessionDBs(db =>
403
534
  safeAll(db, `SELECT CAST(strftime('%H', created_at) AS INTEGER) as hour, COUNT(*) as count
404
535
  FROM session_events WHERE created_at IS NOT NULL
@@ -441,13 +572,22 @@ function apiAnalytics() {
441
572
  );
442
573
 
443
574
  // 2. Personal Commit Rate — commits per session
444
- const commitRate = queryAllSessionDBs(db =>
445
- 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,
446
586
  SUM(CASE WHEN se.type = 'git' AND se.data = 'commit' THEN 1 ELSE 0 END) as commits
447
587
  FROM session_meta sm
448
588
  LEFT JOIN session_events se ON se.session_id = sm.session_id
449
- GROUP BY sm.session_id`)
450
- );
589
+ GROUP BY sm.session_id`);
590
+ });
451
591
 
452
592
  // 3. Sandbox Adoption — context-mode MCP tool usage vs total
453
593
  const sandboxAdoption = queryAllSessionDBs(db =>
@@ -482,6 +622,59 @@ function apiAnalytics() {
482
622
  sandbox_calls: (a.sandbox_calls || 0) + (b.sandbox_calls || 0),
483
623
  total_calls: (a.total_calls || 0) + (b.total_calls || 0),
484
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;
485
678
 
486
679
  return {
487
680
  totals: {
@@ -498,7 +691,7 @@ function apiAnalytics() {
498
691
  totalTasks: tasks.length, totalPrompts: prompts.length,
499
692
  promptsPerSession: sessionDurations.length > 0
500
693
  ? Math.round(10 * prompts.length / sessionDurations.length) / 10 : 0,
501
- uniqueProjects: projectActivity.length,
694
+ uniqueProjects: nonUnknownProjects.length,
502
695
  totalCommits: commitRate.reduce((a, b) => a + (b.commits || 0), 0),
503
696
  commitsPerSession: sessionDurations.length > 0
504
697
  ? Math.round(10 * commitRate.reduce((a, b) => a + (b.commits || 0), 0) / sessionDurations.length) / 10 : 0,
@@ -507,6 +700,15 @@ function apiAnalytics() {
507
700
  totalRules: rulesFreshness.length,
508
701
  totalEditTestCycles: editTestCycles.reduce((a, b) => a + (b.cycles || 0), 0),
509
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
+ },
510
712
  sessionsByDate: mergeByKey(sessionsByDate, "date", (a, b) => ({
511
713
  date: a.date, count: a.count + b.count, events: a.events + b.events, compacts: a.compacts + b.compacts
512
714
  })),
@@ -518,9 +720,7 @@ function apiAnalytics() {
518
720
  timeToFirstCommit,
519
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 }),
520
722
  reworkData, gitActivity, subagents,
521
- projectActivity: mergeByKey(projectActivity, "project_dir", (a, b) => ({
522
- project_dir: a.project_dir, sessions: a.sessions + b.sessions, events: a.events + b.events, compacts: (a.compacts||0)+(b.compacts||0)
523
- })).sort((a, b) => b.events - a.events),
723
+ projectActivity: mergedProjectActivity,
524
724
  hourlyPattern: mergeByKey(hourlyPattern, "hour", (a, b) => ({ hour: a.hour, count: a.count + b.count })),
525
725
  weeklyTrend: mergeByKey(weeklyTrend, "week", (a, b) => ({ week: a.week, sessions: a.sessions + b.sessions, events: a.events + b.events })),
526
726
  tasks, prompts,
@@ -586,6 +786,7 @@ function serveStaticFile(pathname) {
586
786
  // ── Server (dual runtime) ────────────────────────────────
587
787
 
588
788
  const indexHTML = readFileSync(join(DIST_DIR, "index.html"), "utf8");
789
+ const API_JSON_HEADERS = { "Content-Type": "application/json" };
589
790
 
590
791
  if (isBun) {
591
792
  // Bun: use Bun.serve
@@ -597,7 +798,7 @@ if (isBun) {
597
798
  const data = route(req.method, url.pathname, url.searchParams);
598
799
  if (data !== null) {
599
800
  return new Response(JSON.stringify(data), {
600
- headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
801
+ headers: API_JSON_HEADERS,
601
802
  });
602
803
  }
603
804
  if (url.pathname.startsWith("/assets/") || url.pathname.match(/\.\w{2,4}$/)) {
@@ -613,9 +814,7 @@ if (isBun) {
613
814
  // Node: use http.createServer
614
815
  const server = createHttpServer((req, res) => {
615
816
  const url = new URL(req.url, `http://localhost:${PORT}`);
616
- res.setHeader("Access-Control-Allow-Origin", "*");
617
- res.setHeader("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS");
618
- if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
817
+ if (req.method === "OPTIONS") { res.writeHead(405); res.end(); return; }
619
818
 
620
819
  const data = route(req.method, url.pathname, url.searchParams);
621
820
  if (data !== null) {
@@ -637,6 +836,19 @@ if (isBun) {
637
836
  server.listen(PORT, "127.0.0.1");
638
837
  }
639
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
+
640
852
  console.log(`\n context-mode Insight`);
641
853
  console.log(` http://localhost:${PORT}`);
642
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.89",
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.89",
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"