context-mode 1.0.159 → 1.0.160

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.159"
9
+ "version": "1.0.160"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.159",
16
+ "version": "1.0.160",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -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.159",
6
+ "version": "1.0.160",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -71,12 +71,23 @@ await runHook(async () => {
71
71
  const colonIdx = rejectedData.indexOf(":");
72
72
  const rejTool = colonIdx > 0 ? rejectedData.slice(0, colonIdx) : rejectedData;
73
73
  const rejReason = colonIdx > 0 ? rejectedData.slice(colonIdx + 1) : "denied";
74
- db.insertEvent(sessionId, {
75
- type: "rejected",
76
- category: "rejected-approach",
77
- data: `${rejTool}: ${rejReason}`,
78
- priority: 2,
79
- }, "PreToolUse");
74
+ // v1.0.160: route through attributeAndInsertEvents so the bridge wire
75
+ // receives this event too. db.insertEvent only writes locally — the
76
+ // dashboard's rejection-rate widget needs the platform row.
77
+ attributeAndInsertEvents(
78
+ db,
79
+ sessionId,
80
+ [{
81
+ type: "rejected",
82
+ category: "rejected-approach",
83
+ data: `${rejTool}: ${rejReason}`,
84
+ priority: 2,
85
+ }],
86
+ input,
87
+ projectDir,
88
+ "PreToolUse",
89
+ resolveProjectAttributions,
90
+ );
80
91
  }
81
92
  } catch { /* best-effort */ }
82
93
 
@@ -108,17 +119,24 @@ await runHook(async () => {
108
119
  const summary = redirectData.slice(i3 + 1);
109
120
  const bytesAvoided = Number.parseInt(bytesRaw, 10);
110
121
  if (Number.isFinite(bytesAvoided) && bytesAvoided > 0) {
111
- db.insertEvent(
122
+ // v1.0.160: route through wire — context-saving (byte-accounting)
123
+ // widget on the platform reads category='redirect' rows. event
124
+ // carries bytes_avoided so the bytesList branch in
125
+ // attributeAndInsertEvents stamps the column.
126
+ attributeAndInsertEvents(
127
+ db,
112
128
  sessionId,
113
- {
129
+ [{
114
130
  type,
115
131
  category: "redirect",
116
132
  data: `${tool}: ${summary}`,
117
133
  priority: 2,
118
- },
134
+ bytes_avoided: bytesAvoided,
135
+ }],
136
+ input,
137
+ projectDir,
119
138
  "PreToolUse",
120
- undefined,
121
- { bytesAvoided, bytesReturned: 0 },
139
+ resolveProjectAttributions,
122
140
  );
123
141
  }
124
142
  }
@@ -140,12 +158,21 @@ await runHook(async () => {
140
158
  if (startTime && !isNaN(startTime)) {
141
159
  const duration = Date.now() - startTime;
142
160
  if (duration > 5000) {
143
- db.insertEvent(sessionId, {
144
- type: "tool_latency",
145
- category: "latency",
146
- data: `${toolName}: ${duration}ms`,
147
- priority: 3,
148
- }, "PostToolUse");
161
+ // v1.0.160: route through wire — slow-tool insights need this row.
162
+ attributeAndInsertEvents(
163
+ db,
164
+ sessionId,
165
+ [{
166
+ type: "tool_latency",
167
+ category: "latency",
168
+ data: `${toolName}: ${duration}ms`,
169
+ priority: 3,
170
+ }],
171
+ input,
172
+ projectDir,
173
+ "PostToolUse",
174
+ resolveProjectAttributions,
175
+ );
149
176
  }
150
177
  }
151
178
  }
@@ -17,16 +17,17 @@ await runHook(async () => {
17
17
  parseStdin,
18
18
  getSessionId,
19
19
  getSessionDBPath,
20
+ getInputProjectDir,
20
21
  resolveConfigDir,
21
22
  } = await import("./session-helpers.mjs");
22
- const { createSessionLoaders } = await import("./session-loaders.mjs");
23
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
23
24
  const { appendFileSync } = await import("node:fs");
24
25
  const { join, dirname } = await import("node:path");
25
26
  const { fileURLToPath } = await import("node:url");
26
27
 
27
28
  // Resolve absolute path for imports
28
29
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
29
- const { loadSessionDB, loadSnapshot } = createSessionLoaders(HOOK_DIR);
30
+ const { loadSessionDB, loadSnapshot, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
30
31
  const DEBUG_LOG = join(resolveConfigDir(), "context-mode", "precompact-debug.log");
31
32
 
32
33
  try {
@@ -52,31 +53,37 @@ await runHook(async () => {
52
53
  db.upsertResume(sessionId, snapshot, events.length);
53
54
  db.incrementCompactCount(sessionId);
54
55
 
55
- // Write compaction category event for analytics
56
- const fileEvents = events.filter(e => e.category === "file");
57
- db.insertEvent(sessionId, {
58
- type: "compaction_summary",
59
- category: "compaction",
60
- data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
61
- priority: 1,
62
- }, "PreCompact");
63
-
64
- // D2 PRD Phase 6.1: emit snapshot-built event with bytes_avoided=snapshot.length
65
- // Snapshot bytes are bytes the model would have re-read on resume but didn't.
56
+ // v1.0.160: route compaction lifecycle events through wire so
57
+ // dashboard's compact widget gets per-compaction rows (the engine
58
+ // joins on category='compaction' to compute snapshot insights).
66
59
  try {
67
- db.insertEvent(
60
+ const fileEvents = events.filter(e => e.category === "file");
61
+ const projectDirCompact = getInputProjectDir(input);
62
+ const { resolveProjectAttributions } = await loadProjectAttribution();
63
+ attributeAndInsertEvents(
64
+ db,
68
65
  sessionId,
69
- {
70
- type: "snapshot-built",
71
- category: "compaction",
72
- data: `Snapshot built. ${snapshot.length} bytes for ${events.length} events.`,
73
- priority: 1,
74
- },
66
+ [
67
+ {
68
+ type: "compaction_summary",
69
+ category: "compaction",
70
+ data: `Session compacted. ${events.length} events, ${fileEvents.length} files touched.`,
71
+ priority: 1,
72
+ },
73
+ {
74
+ type: "snapshot-built",
75
+ category: "compaction",
76
+ data: `Snapshot built. ${snapshot.length} bytes for ${events.length} events.`,
77
+ priority: 1,
78
+ bytes_avoided: snapshot.length,
79
+ },
80
+ ],
81
+ input,
82
+ projectDirCompact,
75
83
  "PreCompact",
76
- undefined,
77
- { bytesAvoided: snapshot.length, bytesReturned: 0 },
84
+ resolveProjectAttributions,
78
85
  );
79
- } catch { /* best-effort */ }
86
+ } catch { /* best-effort — never block PreCompact */ }
80
87
  }
81
88
 
82
89
  db.close();
@@ -83,13 +83,21 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
83
83
  // no event carries a positive value we leave bytesList undefined so
84
84
  // SessionDB falls back to its 0-default for bytes_avoided/bytes_returned
85
85
  // — preserves backward compat with older callers / tests.
86
+ // v1.0.160: handle both bytes_avoided (saved) and bytes_returned (resume
87
+ // snapshot replay) so the snapshot-consumed event from sessionstart.mjs
88
+ // routes through here without losing the bytes_returned column.
86
89
  let bytesList;
87
- if (events.some((e) => typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0)) {
88
- bytesList = events.map((e) =>
89
- typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0
90
- ? { bytesAvoided: e.bytes_avoided }
91
- : undefined,
92
- );
90
+ const hasBytes = events.some((e) =>
91
+ (typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0) ||
92
+ (typeof e?.bytes_returned === "number" && e.bytes_returned > 0),
93
+ );
94
+ if (hasBytes) {
95
+ bytesList = events.map((e) => {
96
+ const avoided = typeof e?.bytes_avoided === "number" && e.bytes_avoided > 0 ? e.bytes_avoided : 0;
97
+ const returned = typeof e?.bytes_returned === "number" && e.bytes_returned > 0 ? e.bytes_returned : 0;
98
+ if (avoided === 0 && returned === 0) return undefined;
99
+ return { bytesAvoided: avoided, bytesReturned: returned };
100
+ });
93
101
  }
94
102
  // Prefer bulk path (single transaction = single WAL commit). Falls back
95
103
  // to per-event insert for older SessionDB instances that lack bulkInsertEvents.
@@ -200,38 +200,37 @@ await runHook(async () => {
200
200
  // D2 PRD Phase 6.2: emit snapshot-consumed with bytes_returned=snapshot.length.
201
201
  // The resumed snapshot bytes ARE returned to the model — that's the whole
202
202
  // point of resume — so account them on bytes_returned, not bytes_avoided.
203
+ // v1.0.160: route through wire — resume metric on the platform reads
204
+ // category='session-resume' rows. Both snapshot-consumed (bytes
205
+ // returned) and resume_completed land here so the dashboard sees
206
+ // every resume boundary.
203
207
  try {
204
208
  const resumeRow = (resume && resume.snapshot)
205
209
  ? resume
206
210
  : (db.getResume?.(sessionId) ?? null);
207
211
  const snapshotBytes = resumeRow?.snapshot?.length ?? 0;
212
+ const { resolveProjectAttributions } = await loadProjectAttribution();
213
+ const projectDirResumeMeta = getInputProjectDir(input);
208
214
 
209
- db.insertEvent(
215
+ await attributeAndInsertEvents(
216
+ db,
210
217
  sessionId,
211
- {
218
+ [{
212
219
  type: "snapshot-consumed",
213
220
  category: "session-resume",
214
221
  data: `Session resumed from ${source}. Snapshot ${snapshotBytes} bytes injected.`,
215
222
  priority: 1,
216
- },
217
- "SessionStart",
218
- undefined,
219
- { bytesAvoided: 0, bytesReturned: snapshotBytes },
220
- );
221
- } catch { /* best-effort */ }
222
-
223
- // Legacy resume_completed event retained for back-compat with existing
224
- // analytics consumers that filter on `type === 'resume_completed'`.
225
- try {
226
- db.insertEvent(
227
- sessionId,
228
- {
223
+ bytes_returned: snapshotBytes,
224
+ }, {
229
225
  type: "resume_completed",
230
226
  category: "session-resume",
231
227
  data: `Session resumed from ${source}. Prior events loaded.`,
232
228
  priority: 1,
233
- },
229
+ }],
230
+ input,
231
+ projectDirResumeMeta,
234
232
  "SessionStart",
233
+ resolveProjectAttributions,
235
234
  );
236
235
  } catch { /* best-effort */ }
237
236
  }
@@ -322,22 +321,42 @@ await runHook(async () => {
322
321
  // context at startup, invisible to PostToolUse hooks. We read them from
323
322
  // disk so they survive compact/resume via the session events pipeline.
324
323
  const sessionId = getSessionId(input);
325
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
324
+ // v1.0.160: cross-adapter projectDir resolution (was hardcoded CC env).
325
+ const projectDir = getInputProjectDir(input);
326
326
  db.ensureSession(sessionId, projectDir);
327
327
  const claudeMdPaths = [
328
328
  join(resolveConfigDir(), "CLAUDE.md"),
329
329
  join(projectDir, "CLAUDE.md"),
330
330
  join(projectDir, ".claude", "CLAUDE.md"),
331
331
  ];
332
+ // v1.0.160: collect rule events into a batch and forward through wire.
333
+ // Dashboard's "CLAUDE.md adoption" widget COUNTs category='rule' rows on
334
+ // the platform — without this routing the widget reads 0 no matter how
335
+ // many CLAUDE.md files actually loaded.
336
+ const ruleEvents = [];
332
337
  for (const p of claudeMdPaths) {
333
338
  try {
334
339
  const content = readFileSync(p, "utf-8");
335
340
  if (content.trim()) {
336
- db.insertEvent(sessionId, { type: "rule", category: "rule", data: p, priority: 1 });
337
- db.insertEvent(sessionId, { type: "rule_content", category: "rule", data: content, priority: 1 });
341
+ ruleEvents.push({ type: "rule", category: "rule", data: p, priority: 1 });
342
+ ruleEvents.push({ type: "rule_content", category: "rule", data: content, priority: 1 });
338
343
  }
339
344
  } catch { /* file doesn't exist — skip */ }
340
345
  }
346
+ if (ruleEvents.length > 0) {
347
+ try {
348
+ const { resolveProjectAttributions } = await loadProjectAttribution();
349
+ attributeAndInsertEvents(
350
+ db,
351
+ sessionId,
352
+ ruleEvents,
353
+ input,
354
+ projectDir,
355
+ "SessionStart",
356
+ resolveProjectAttributions,
357
+ );
358
+ } catch { /* best-effort — rule capture must never block start */ }
359
+ }
341
360
 
342
361
  // Lifecycle anchor for a fresh session — emits BEFORE the CLAUDE.md
343
362
  // rule events have been forwarded so the `session_start` row lands
@@ -77,8 +77,20 @@ await runHook(async () => {
77
77
  workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
78
78
  lastKnownProjectDir: savedLastKnown || lastKnownProjectDir,
79
79
  });
80
- for (let i = 0; i < userEvents.length; i++) {
81
- db.insertEvent(sessionId, userEvents[i], "UserPromptSubmit", userAttributions[i]);
80
+ // v1.0.160: route through wire so prompt-derived events (decision /
81
+ // role / intent / data extractions) reach the platform. Previously
82
+ // they only landed in local SessionDB → dashboard's prompt-flow
83
+ // insights stayed at 0.
84
+ if (userEvents.length > 0) {
85
+ attributeAndInsertEvents(
86
+ db,
87
+ sessionId,
88
+ userEvents,
89
+ input,
90
+ projectDir,
91
+ "UserPromptSubmit",
92
+ resolveProjectAttributions,
93
+ );
82
94
  }
83
95
 
84
96
  db.close();
@@ -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.159",
6
+ "version": "1.0.160",
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.159",
3
+ "version": "1.0.160",
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",