context-mode 1.0.89 → 1.0.91
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/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +184 -60
- package/build/adapters/antigravity/index.d.ts +3 -5
- package/build/adapters/antigravity/index.js +7 -35
- package/build/adapters/base.d.ts +27 -0
- package/build/adapters/base.js +59 -0
- package/build/adapters/claude-code/index.d.ts +9 -25
- package/build/adapters/claude-code/index.js +12 -140
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.js +5 -0
- package/build/adapters/codex/hooks.d.ts +21 -14
- package/build/adapters/codex/hooks.js +22 -15
- package/build/adapters/codex/index.d.ts +6 -10
- package/build/adapters/codex/index.js +13 -43
- package/build/adapters/copilot-base.d.ts +78 -0
- package/build/adapters/copilot-base.js +281 -0
- package/build/adapters/cursor/index.d.ts +3 -5
- package/build/adapters/cursor/index.js +6 -34
- package/build/adapters/detect.d.ts +7 -0
- package/build/adapters/detect.js +57 -56
- package/build/adapters/gemini-cli/index.d.ts +3 -5
- package/build/adapters/gemini-cli/index.js +7 -35
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +51 -0
- package/build/adapters/jetbrains-copilot/hooks.js +82 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +24 -0
- package/build/adapters/jetbrains-copilot/index.js +119 -0
- package/build/adapters/kiro/hooks.d.ts +14 -0
- package/build/adapters/kiro/hooks.js +23 -0
- package/build/adapters/kiro/index.d.ts +3 -5
- package/build/adapters/kiro/index.js +10 -38
- package/build/adapters/openclaw/index.d.ts +3 -4
- package/build/adapters/openclaw/index.js +6 -22
- package/build/adapters/opencode/index.d.ts +2 -3
- package/build/adapters/opencode/index.js +5 -16
- package/build/adapters/qwen-code/index.d.ts +39 -0
- package/build/adapters/qwen-code/index.js +199 -0
- package/build/adapters/types.d.ts +1 -1
- package/build/adapters/vscode-copilot/index.d.ts +16 -46
- package/build/adapters/vscode-copilot/index.js +29 -320
- package/build/adapters/zed/index.d.ts +3 -5
- package/build/adapters/zed/index.js +7 -35
- package/build/cli.js +13 -0
- package/build/lifecycle.d.ts +23 -0
- package/build/lifecycle.js +54 -13
- package/build/opencode-plugin.d.ts +19 -7
- package/build/opencode-plugin.js +19 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +53 -10
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +107 -76
- package/build/session/db.d.ts +10 -1
- package/build/session/db.js +67 -8
- package/build/session/extract.js +10 -2
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +231 -0
- package/build/store.d.ts +4 -0
- package/build/store.js +58 -9
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +135 -121
- package/configs/antigravity/GEMINI.md +31 -36
- package/configs/claude-code/CLAUDE.md +31 -37
- package/configs/codex/AGENTS.md +35 -49
- package/configs/cursor/context-mode.mdc +24 -25
- package/configs/gemini-cli/GEMINI.md +30 -36
- package/configs/jetbrains-copilot/copilot-instructions.md +59 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +8 -0
- package/configs/kilo/AGENTS.md +30 -36
- package/configs/kiro/KIRO.md +30 -36
- package/configs/kiro/agent.json +1 -1
- package/configs/openclaw/AGENTS.md +30 -36
- package/configs/opencode/AGENTS.md +30 -36
- package/configs/pi/AGENTS.md +31 -36
- package/configs/qwen-code/QWEN.md +63 -0
- package/configs/vscode-copilot/copilot-instructions.md +30 -36
- package/configs/zed/AGENTS.md +31 -36
- package/hooks/codex/posttooluse.mjs +7 -7
- package/hooks/codex/pretooluse.mjs +3 -3
- package/hooks/codex/sessionstart.mjs +2 -1
- package/hooks/core/formatters.mjs +24 -0
- package/hooks/core/routing.mjs +40 -15
- package/hooks/core/tool-naming.mjs +2 -0
- package/hooks/cursor/posttooluse.mjs +7 -7
- package/hooks/cursor/pretooluse.mjs +3 -3
- package/hooks/cursor/sessionstart.mjs +2 -1
- package/hooks/cursor/stop.mjs +2 -2
- package/hooks/ensure-deps.mjs +22 -10
- package/hooks/gemini-cli/aftertool.mjs +8 -8
- package/hooks/gemini-cli/beforetool.mjs +3 -2
- package/hooks/gemini-cli/precompress.mjs +2 -2
- package/hooks/gemini-cli/sessionstart.mjs +12 -4
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +54 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +119 -0
- package/hooks/kiro/posttooluse.mjs +6 -7
- package/hooks/kiro/pretooluse.mjs +3 -2
- package/hooks/posttooluse.mjs +8 -8
- package/hooks/precompact.mjs +3 -4
- package/hooks/pretooluse.mjs +5 -4
- package/hooks/routing-block.mjs +35 -33
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +27 -8
- package/hooks/session-extract.bundle.mjs +2 -1
- package/hooks/session-helpers.mjs +44 -3
- package/hooks/session-loaders.mjs +37 -0
- package/hooks/sessionstart.mjs +5 -5
- package/hooks/userpromptsubmit.mjs +26 -9
- package/hooks/vscode-copilot/posttooluse.mjs +8 -8
- package/hooks/vscode-copilot/precompact.mjs +2 -2
- package/hooks/vscode-copilot/pretooluse.mjs +3 -2
- package/hooks/vscode-copilot/sessionstart.mjs +2 -2
- package/insight/server.mjs +237 -25
- package/insight/src/lib/api.ts +2 -1
- package/insight/src/routes/index.tsx +16 -3
- package/insight/src/routes/search.tsx +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -2
- package/server.bundle.mjs +94 -80
- package/skills/ctx-insight/SKILL.md +1 -1
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
* const engine = new AnalyticsEngine(sessionDb);
|
|
9
9
|
* const report = engine.queryAll(runtimeStats);
|
|
10
10
|
*/
|
|
11
|
+
function semverNewer(a, b) {
|
|
12
|
+
const pa = a.split(".").map(Number);
|
|
13
|
+
const pb = b.split(".").map(Number);
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
16
|
+
return true;
|
|
17
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
11
22
|
// ─────────────────────────────────────────────────────────
|
|
12
23
|
// Category labels and hints for session continuity display
|
|
13
24
|
// ─────────────────────────────────────────────────────────
|
|
@@ -216,7 +227,7 @@ export class AnalyticsEngine {
|
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
// ─────────────────────────────────────────────────────────
|
|
219
|
-
// formatReport — renders FullReport as
|
|
230
|
+
// formatReport — renders FullReport as sales-grade savings dashboard
|
|
220
231
|
// ─────────────────────────────────────────────────────────
|
|
221
232
|
/** Format bytes as human-readable KB or MB. */
|
|
222
233
|
function kb(b) {
|
|
@@ -224,7 +235,7 @@ function kb(b) {
|
|
|
224
235
|
return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
225
236
|
if (b >= 1024)
|
|
226
237
|
return `${(b / 1024).toFixed(1)} KB`;
|
|
227
|
-
return `${b} B`;
|
|
238
|
+
return `${Math.round(b)} B`;
|
|
228
239
|
}
|
|
229
240
|
/** Format session uptime as human-readable duration. */
|
|
230
241
|
function formatDuration(uptimeMin) {
|
|
@@ -237,28 +248,34 @@ function formatDuration(uptimeMin) {
|
|
|
237
248
|
const m = Math.round(min % 60);
|
|
238
249
|
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
239
250
|
}
|
|
251
|
+
/** Format large numbers with K/M suffixes */
|
|
252
|
+
function fmtNum(n) {
|
|
253
|
+
if (n >= 1_000_000)
|
|
254
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
255
|
+
if (n >= 1_000)
|
|
256
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
257
|
+
return String(n);
|
|
258
|
+
}
|
|
240
259
|
/**
|
|
241
|
-
* Build a
|
|
242
|
-
*
|
|
243
|
-
* The "without" bar is always full (40 chars).
|
|
244
|
-
* The "with" bar is proportional to the ratio of returned vs total.
|
|
260
|
+
* Build a proportional bar using █ chars, scaled to a fixed width.
|
|
261
|
+
* Returns e.g. "████████████████████████████████████████" for full width.
|
|
245
262
|
*/
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
return { withoutBar, withBar };
|
|
263
|
+
function dataBar(bytes, maxBytes, width = 40) {
|
|
264
|
+
if (maxBytes <= 0)
|
|
265
|
+
return "░".repeat(width);
|
|
266
|
+
const filled = Math.max(1, Math.round((bytes / maxBytes) * width));
|
|
267
|
+
return "█".repeat(Math.min(filled, width)) + "░".repeat(Math.max(0, width - filled));
|
|
252
268
|
}
|
|
253
269
|
/**
|
|
254
|
-
* Render a FullReport as a
|
|
270
|
+
* Render a FullReport as a visual savings dashboard designed for screenshotting.
|
|
255
271
|
*
|
|
256
|
-
* Design
|
|
257
|
-
* -
|
|
258
|
-
* -
|
|
259
|
-
* - Per-tool
|
|
260
|
-
* -
|
|
261
|
-
* -
|
|
272
|
+
* Design principles:
|
|
273
|
+
* - Before/After comparison bar is the HERO — one glance = "wow"
|
|
274
|
+
* - "tokens saved" is the number people share
|
|
275
|
+
* - Per-tool breakdown shows what each tool SAVED, sorted by impact
|
|
276
|
+
* - Session memory: one line, reframed as value
|
|
277
|
+
* - No: Pct column, category tables, tips, jargon
|
|
278
|
+
* - Under 22 lines for heavy sessions, under 10 for fresh
|
|
262
279
|
*/
|
|
263
280
|
export function formatReport(report, version, latestVersion) {
|
|
264
281
|
const lines = [];
|
|
@@ -267,86 +284,100 @@ export function formatReport(report, version, latestVersion) {
|
|
|
267
284
|
const totalKeptOut = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
|
|
268
285
|
const totalReturned = report.savings.total_bytes_returned;
|
|
269
286
|
const totalCalls = report.savings.total_calls;
|
|
270
|
-
|
|
287
|
+
const grandTotal = totalKeptOut + totalReturned;
|
|
288
|
+
const savingsPct = grandTotal > 0 ? (totalKeptOut / grandTotal) * 100 : 0;
|
|
289
|
+
const tokensSaved = Math.round(totalKeptOut / 4);
|
|
290
|
+
// ── Fresh session: no savings yet ──
|
|
271
291
|
if (totalKeptOut === 0) {
|
|
272
|
-
lines.push(`context-mode
|
|
292
|
+
lines.push(`context-mode ${duration} ${totalCalls} calls`);
|
|
273
293
|
lines.push("");
|
|
274
294
|
if (totalCalls === 0) {
|
|
275
|
-
lines.push("No tool calls yet.");
|
|
295
|
+
lines.push("No tool calls yet. Use batch_execute or execute to start saving tokens.");
|
|
276
296
|
}
|
|
277
297
|
else {
|
|
278
|
-
|
|
279
|
-
lines.push(`${callLabel} | ${kb(totalReturned)} in context | no savings yet`);
|
|
298
|
+
lines.push(`${kb(totalReturned)} entered context | 0 tokens saved`);
|
|
280
299
|
}
|
|
300
|
+
// Footer
|
|
281
301
|
lines.push("");
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
lines.push(`Update available: v${version} -> v${latestVersion} | Run: ctx_upgrade`);
|
|
302
|
+
const versionStr = version ? `v${version}` : "context-mode";
|
|
303
|
+
lines.push(versionStr);
|
|
304
|
+
if (version && latestVersion && latestVersion !== "unknown" && semverNewer(latestVersion, version)) {
|
|
305
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | ctx_upgrade`);
|
|
287
306
|
}
|
|
288
307
|
return lines.join("\n");
|
|
289
308
|
}
|
|
290
|
-
// ── Active session
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const minSaved = Math.round(totalKeptOut / 4 / 1000);
|
|
298
|
-
lines.push(`context-mode -- session (${duration})`);
|
|
309
|
+
// ── Active session: visual savings dashboard ──
|
|
310
|
+
// Line 1: Hero metric — the screenshottable number
|
|
311
|
+
lines.push(`${fmtNum(tokensSaved)} tokens saved · ${savingsPct.toFixed(1)}% reduction · ${duration}`);
|
|
312
|
+
lines.push("");
|
|
313
|
+
// Lines 2-3: Before/After comparison bars — the visual proof
|
|
314
|
+
lines.push(`Without context-mode |${dataBar(grandTotal, grandTotal)}| ${kb(grandTotal)}`);
|
|
315
|
+
lines.push(`With context-mode |${dataBar(totalReturned, grandTotal)}| ${kb(totalReturned)}`);
|
|
299
316
|
lines.push("");
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
lines.push(`Without context-mode: |${withoutBar}| ${kb(grandTotal)} in your conversation`);
|
|
303
|
-
lines.push(`With context-mode: |${withBar}| ${kb(totalReturned)} in your conversation`);
|
|
317
|
+
// Value statement — the line people share
|
|
318
|
+
lines.push(`${kb(totalKeptOut)} kept out of your conversation. Never entered context.`);
|
|
304
319
|
lines.push("");
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
? `+${Math.floor(minSaved / 60)}h ${minSaved % 60}m`
|
|
310
|
-
: `+${minSaved}m`;
|
|
311
|
-
lines.push(`${timeSaved} session time gained.`);
|
|
320
|
+
// Compact stats row
|
|
321
|
+
const statParts = [`${totalCalls} calls`];
|
|
322
|
+
if (report.cache && report.cache.hits > 0) {
|
|
323
|
+
statParts.push(`${report.cache.hits} cache hits (+${kb(report.cache.bytes_saved)})`);
|
|
312
324
|
}
|
|
313
|
-
|
|
325
|
+
lines.push(statParts.join(" · "));
|
|
326
|
+
// ── Per-tool breakdown (only if 2+ tools, sorted by saved) ──
|
|
314
327
|
const activatedTools = report.savings.by_tool.filter((t) => t.calls > 0);
|
|
315
328
|
if (activatedTools.length >= 2) {
|
|
316
329
|
lines.push("");
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
330
|
+
// Estimate per-tool saved using global savings ratio
|
|
331
|
+
const toolRows = activatedTools.map((t) => {
|
|
332
|
+
const returnedBytes = t.context_kb * 1024;
|
|
333
|
+
const estimatedTotal = savingsPct < 100
|
|
334
|
+
? returnedBytes / (1 - savingsPct / 100)
|
|
335
|
+
: returnedBytes;
|
|
336
|
+
const estimatedSaved = Math.max(0, estimatedTotal - returnedBytes);
|
|
337
|
+
return { ...t, returnedBytes, estimatedSaved };
|
|
338
|
+
}).sort((a, b) => b.estimatedSaved - a.estimatedSaved);
|
|
339
|
+
// Compact table: tool name, calls, saved
|
|
340
|
+
for (const t of toolRows) {
|
|
341
|
+
const name = t.tool.length > 22 ? t.tool.slice(0, 19) + "..." : t.tool;
|
|
342
|
+
lines.push(` ${name.padEnd(22)} ${String(t.calls).padStart(4)} calls ${kb(t.estimatedSaved).padStart(8)} saved`);
|
|
321
343
|
}
|
|
322
344
|
}
|
|
323
|
-
// ── Session
|
|
324
|
-
if (report.continuity.
|
|
345
|
+
// ── Session memory — business-friendly ──
|
|
346
|
+
if (report.continuity.total_events > 0) {
|
|
325
347
|
lines.push("");
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
const cats = report.continuity.by_category;
|
|
349
|
+
// Pick the top 3-4 most impactful categories for a human-readable summary
|
|
350
|
+
const highlights = [];
|
|
351
|
+
const fileCount = cats.find(c => c.category === "file")?.count;
|
|
352
|
+
const gitCount = cats.find(c => c.category === "git")?.count;
|
|
353
|
+
const promptCount = cats.find(c => c.category === "prompt")?.count;
|
|
354
|
+
const errorCount = cats.find(c => c.category === "error")?.count;
|
|
355
|
+
const taskCount = cats.find(c => c.category === "task")?.count;
|
|
356
|
+
if (fileCount)
|
|
357
|
+
highlights.push(`${fileCount} files`);
|
|
358
|
+
if (gitCount)
|
|
359
|
+
highlights.push(`${gitCount} git ops`);
|
|
360
|
+
if (promptCount)
|
|
361
|
+
highlights.push(`${promptCount} prompts`);
|
|
362
|
+
if (errorCount)
|
|
363
|
+
highlights.push(`${errorCount} errors`);
|
|
364
|
+
if (taskCount)
|
|
365
|
+
highlights.push(`${taskCount} tasks`);
|
|
366
|
+
const summary = highlights.length > 0 ? ` · ${highlights.join(", ")}` : "";
|
|
367
|
+
if (report.continuity.compact_count > 0) {
|
|
368
|
+
lines.push(`${fmtNum(report.continuity.total_events)} events remembered across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}${summary}`);
|
|
369
|
+
lines.push("Zero knowledge lost — picks up exactly where you left off.");
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
lines.push(`${fmtNum(report.continuity.total_events)} events tracked${summary}`);
|
|
333
373
|
}
|
|
334
374
|
}
|
|
335
|
-
// ── Footer
|
|
336
|
-
const footerParts = [];
|
|
337
|
-
if (report.continuity.by_category.length === 0 && report.continuity.compact_count > 0) {
|
|
338
|
-
footerParts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
|
|
339
|
-
}
|
|
340
|
-
if (report.continuity.by_category.length === 0 && report.continuity.total_events > 0) {
|
|
341
|
-
footerParts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
|
|
342
|
-
}
|
|
343
|
-
const versionStr = version ? `v${version}` : "context-mode";
|
|
344
|
-
footerParts.push(versionStr);
|
|
375
|
+
// ── Footer ──
|
|
345
376
|
lines.push("");
|
|
346
|
-
|
|
347
|
-
|
|
377
|
+
const versionStr = version ? `v${version}` : "context-mode";
|
|
378
|
+
lines.push(versionStr);
|
|
348
379
|
if (version && latestVersion && latestVersion !== "unknown" && latestVersion !== version) {
|
|
349
|
-
lines.push(`Update available: v${version} -> v${latestVersion} |
|
|
380
|
+
lines.push(`Update available: v${version} -> v${latestVersion} | ctx_upgrade`);
|
|
350
381
|
}
|
|
351
382
|
return lines.join("\n");
|
|
352
383
|
}
|
package/build/session/db.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { SQLiteBase } from "../db-base.js";
|
|
9
9
|
import type { SessionEvent } from "../types.js";
|
|
10
|
+
import type { ProjectAttribution } from "./project-attribution.js";
|
|
10
11
|
/**
|
|
11
12
|
* Returns the worktree suffix to append to session identifiers.
|
|
12
13
|
* Returns empty string when running in the main working tree.
|
|
@@ -24,6 +25,9 @@ export interface StoredEvent {
|
|
|
24
25
|
category: string;
|
|
25
26
|
priority: number;
|
|
26
27
|
data: string;
|
|
28
|
+
project_dir: string;
|
|
29
|
+
attribution_source: string;
|
|
30
|
+
attribution_confidence: number;
|
|
27
31
|
source_hook: string;
|
|
28
32
|
created_at: string;
|
|
29
33
|
data_hash: string;
|
|
@@ -71,7 +75,7 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
71
75
|
* Eviction: if session exceeds MAX_EVENTS_PER_SESSION, evicts the
|
|
72
76
|
* lowest-priority (then oldest) event.
|
|
73
77
|
*/
|
|
74
|
-
insertEvent(sessionId: string, event: SessionEvent, sourceHook?: string): void;
|
|
78
|
+
insertEvent(sessionId: string, event: SessionEvent, sourceHook?: string, attribution?: Partial<ProjectAttribution>): void;
|
|
75
79
|
/**
|
|
76
80
|
* Retrieve events for a session with optional filtering.
|
|
77
81
|
*/
|
|
@@ -84,8 +88,13 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
84
88
|
* Get the total event count for a session.
|
|
85
89
|
*/
|
|
86
90
|
getEventCount(sessionId: string): number;
|
|
91
|
+
/**
|
|
92
|
+
* Return the most recently attributed project dir for a session.
|
|
93
|
+
*/
|
|
94
|
+
getLatestAttributedProjectDir(sessionId: string): string | null;
|
|
87
95
|
/**
|
|
88
96
|
* Ensure a session metadata entry exists. Idempotent (INSERT OR IGNORE).
|
|
97
|
+
* `projectDir` is the session origin directory, not per-event attribution.
|
|
89
98
|
*/
|
|
90
99
|
ensureSession(sessionId: string, projectDir: string): void;
|
|
91
100
|
/**
|
package/build/session/db.js
CHANGED
|
@@ -62,6 +62,7 @@ const S = {
|
|
|
62
62
|
getEventsByPriority: "getEventsByPriority",
|
|
63
63
|
getEventsByTypeAndPriority: "getEventsByTypeAndPriority",
|
|
64
64
|
getEventCount: "getEventCount",
|
|
65
|
+
getLatestAttributedProject: "getLatestAttributedProject",
|
|
65
66
|
checkDuplicate: "checkDuplicate",
|
|
66
67
|
evictLowestPriority: "evictLowestPriority",
|
|
67
68
|
updateMetaLastEvent: "updateMetaLastEvent",
|
|
@@ -109,6 +110,9 @@ export class SessionDB extends SQLiteBase {
|
|
|
109
110
|
category TEXT NOT NULL,
|
|
110
111
|
priority INTEGER NOT NULL DEFAULT 2,
|
|
111
112
|
data TEXT NOT NULL,
|
|
113
|
+
project_dir TEXT NOT NULL DEFAULT '',
|
|
114
|
+
attribution_source TEXT NOT NULL DEFAULT 'unknown',
|
|
115
|
+
attribution_confidence REAL NOT NULL DEFAULT 0,
|
|
112
116
|
source_hook TEXT NOT NULL,
|
|
113
117
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
114
118
|
data_hash TEXT NOT NULL DEFAULT ''
|
|
@@ -136,6 +140,24 @@ export class SessionDB extends SQLiteBase {
|
|
|
136
140
|
consumed INTEGER NOT NULL DEFAULT 0
|
|
137
141
|
);
|
|
138
142
|
`);
|
|
143
|
+
// Migration: add per-event attribution columns for existing DBs.
|
|
144
|
+
try {
|
|
145
|
+
const colInfo = this.db.pragma("table_xinfo(session_events)");
|
|
146
|
+
const cols = new Set(colInfo.map((c) => c.name));
|
|
147
|
+
if (!cols.has("project_dir")) {
|
|
148
|
+
this.db.exec("ALTER TABLE session_events ADD COLUMN project_dir TEXT NOT NULL DEFAULT ''");
|
|
149
|
+
}
|
|
150
|
+
if (!cols.has("attribution_source")) {
|
|
151
|
+
this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_source TEXT NOT NULL DEFAULT 'unknown'");
|
|
152
|
+
}
|
|
153
|
+
if (!cols.has("attribution_confidence")) {
|
|
154
|
+
this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_confidence REAL NOT NULL DEFAULT 0");
|
|
155
|
+
}
|
|
156
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)");
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// best-effort migration only
|
|
160
|
+
}
|
|
139
161
|
}
|
|
140
162
|
prepareStatements() {
|
|
141
163
|
this.stmts = new Map();
|
|
@@ -143,17 +165,34 @@ export class SessionDB extends SQLiteBase {
|
|
|
143
165
|
this.stmts.set(key, this.db.prepare(sql));
|
|
144
166
|
};
|
|
145
167
|
// ── Events ──
|
|
146
|
-
p(S.insertEvent, `INSERT INTO session_events (
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
p(S.insertEvent, `INSERT INTO session_events (
|
|
169
|
+
session_id, type, category, priority, data,
|
|
170
|
+
project_dir, attribution_source, attribution_confidence,
|
|
171
|
+
source_hook, data_hash
|
|
172
|
+
)
|
|
173
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
174
|
+
p(S.getEvents, `SELECT id, session_id, type, category, priority, data,
|
|
175
|
+
project_dir, attribution_source, attribution_confidence,
|
|
176
|
+
source_hook, created_at, data_hash
|
|
149
177
|
FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`);
|
|
150
|
-
p(S.getEventsByType, `SELECT id, session_id, type, category, priority, data,
|
|
178
|
+
p(S.getEventsByType, `SELECT id, session_id, type, category, priority, data,
|
|
179
|
+
project_dir, attribution_source, attribution_confidence,
|
|
180
|
+
source_hook, created_at, data_hash
|
|
151
181
|
FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`);
|
|
152
|
-
p(S.getEventsByPriority, `SELECT id, session_id, type, category, priority, data,
|
|
182
|
+
p(S.getEventsByPriority, `SELECT id, session_id, type, category, priority, data,
|
|
183
|
+
project_dir, attribution_source, attribution_confidence,
|
|
184
|
+
source_hook, created_at, data_hash
|
|
153
185
|
FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`);
|
|
154
|
-
p(S.getEventsByTypeAndPriority, `SELECT id, session_id, type, category, priority, data,
|
|
186
|
+
p(S.getEventsByTypeAndPriority, `SELECT id, session_id, type, category, priority, data,
|
|
187
|
+
project_dir, attribution_source, attribution_confidence,
|
|
188
|
+
source_hook, created_at, data_hash
|
|
155
189
|
FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`);
|
|
156
190
|
p(S.getEventCount, `SELECT COUNT(*) AS cnt FROM session_events WHERE session_id = ?`);
|
|
191
|
+
p(S.getLatestAttributedProject, `SELECT project_dir
|
|
192
|
+
FROM session_events
|
|
193
|
+
WHERE session_id = ? AND project_dir != ''
|
|
194
|
+
ORDER BY id DESC
|
|
195
|
+
LIMIT 1`);
|
|
157
196
|
p(S.checkDuplicate, `SELECT 1 FROM (
|
|
158
197
|
SELECT type, data_hash FROM session_events
|
|
159
198
|
WHERE session_id = ? ORDER BY id DESC LIMIT ?
|
|
@@ -201,13 +240,25 @@ export class SessionDB extends SQLiteBase {
|
|
|
201
240
|
* Eviction: if session exceeds MAX_EVENTS_PER_SESSION, evicts the
|
|
202
241
|
* lowest-priority (then oldest) event.
|
|
203
242
|
*/
|
|
204
|
-
insertEvent(sessionId, event, sourceHook = "PostToolUse") {
|
|
243
|
+
insertEvent(sessionId, event, sourceHook = "PostToolUse", attribution) {
|
|
205
244
|
// SHA256-based dedup hash (first 16 hex chars = 8 bytes of entropy)
|
|
206
245
|
const dataHash = createHash("sha256")
|
|
207
246
|
.update(event.data)
|
|
208
247
|
.digest("hex")
|
|
209
248
|
.slice(0, 16)
|
|
210
249
|
.toUpperCase();
|
|
250
|
+
const projectDir = String(attribution?.projectDir
|
|
251
|
+
?? event.project_dir
|
|
252
|
+
?? "").trim();
|
|
253
|
+
const attributionSource = String(attribution?.source
|
|
254
|
+
?? event.attribution_source
|
|
255
|
+
?? "unknown");
|
|
256
|
+
const rawConfidence = Number(attribution?.confidence
|
|
257
|
+
?? event.attribution_confidence
|
|
258
|
+
?? 0);
|
|
259
|
+
const attributionConfidence = Number.isFinite(rawConfidence)
|
|
260
|
+
? Math.max(0, Math.min(1, rawConfidence))
|
|
261
|
+
: 0;
|
|
211
262
|
// Atomic: dedup check + eviction + insert in a single transaction
|
|
212
263
|
// to prevent race conditions from concurrent hook calls.
|
|
213
264
|
const transaction = this.db.transaction(() => {
|
|
@@ -221,7 +272,7 @@ export class SessionDB extends SQLiteBase {
|
|
|
221
272
|
this.stmt(S.evictLowestPriority).run(sessionId);
|
|
222
273
|
}
|
|
223
274
|
// Insert the event
|
|
224
|
-
this.stmt(S.insertEvent).run(sessionId, event.type, event.category, event.priority, event.data, sourceHook, dataHash);
|
|
275
|
+
this.stmt(S.insertEvent).run(sessionId, event.type, event.category, event.priority, event.data, projectDir, attributionSource, attributionConfidence, sourceHook, dataHash);
|
|
225
276
|
// Update meta if session exists
|
|
226
277
|
this.stmt(S.updateMetaLastEvent).run(sessionId);
|
|
227
278
|
});
|
|
@@ -252,11 +303,19 @@ export class SessionDB extends SQLiteBase {
|
|
|
252
303
|
const row = this.stmt(S.getEventCount).get(sessionId);
|
|
253
304
|
return row.cnt;
|
|
254
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Return the most recently attributed project dir for a session.
|
|
308
|
+
*/
|
|
309
|
+
getLatestAttributedProjectDir(sessionId) {
|
|
310
|
+
const row = this.stmt(S.getLatestAttributedProject).get(sessionId);
|
|
311
|
+
return row?.project_dir || null;
|
|
312
|
+
}
|
|
255
313
|
// ═══════════════════════════════════════════
|
|
256
314
|
// Meta
|
|
257
315
|
// ═══════════════════════════════════════════
|
|
258
316
|
/**
|
|
259
317
|
* Ensure a session metadata entry exists. Idempotent (INSERT OR IGNORE).
|
|
318
|
+
* `projectDir` is the session origin directory, not per-event attribution.
|
|
260
319
|
*/
|
|
261
320
|
ensureSession(sessionId, projectDir) {
|
|
262
321
|
this.stmt(S.ensureSession).run(sessionId, projectDir);
|
package/build/session/extract.js
CHANGED
|
@@ -364,7 +364,7 @@ function extractSubagent(input) {
|
|
|
364
364
|
* MCP tool calls (context7, playwright, claude-mem, ctx-stats, etc.).
|
|
365
365
|
*/
|
|
366
366
|
function extractMcp(input) {
|
|
367
|
-
const { tool_name, tool_input } = input;
|
|
367
|
+
const { tool_name, tool_input, tool_response } = input;
|
|
368
368
|
if (!tool_name.startsWith("mcp__"))
|
|
369
369
|
return [];
|
|
370
370
|
// Extract readable tool name: last segment after __
|
|
@@ -373,10 +373,18 @@ function extractMcp(input) {
|
|
|
373
373
|
// Extract first string argument for context
|
|
374
374
|
const firstArg = Object.values(tool_input).find((v) => typeof v === "string");
|
|
375
375
|
const argStr = firstArg ? `: ${safeString(String(firstArg))}` : "";
|
|
376
|
+
// Append tool_response so ctx_search can find what the MCP returned — not
|
|
377
|
+
// just the call shape. Without this, bodies from external MCPs (jira tickets,
|
|
378
|
+
// grafana loki lines, sentry issues, context7 docs) are invisible to search.
|
|
379
|
+
// No truncation: matches the rule_content precedent above — SQLite TEXT is
|
|
380
|
+
// unbounded and large responses are the ones a cache most wants to preserve.
|
|
381
|
+
const responseStr = tool_response && tool_response.length > 0
|
|
382
|
+
? `\nresponse: ${safeString(tool_response)}`
|
|
383
|
+
: "";
|
|
376
384
|
return [{
|
|
377
385
|
type: "mcp",
|
|
378
386
|
category: "mcp",
|
|
379
|
-
data: safeString(`${toolShort}${argStr}`),
|
|
387
|
+
data: safeString(`${toolShort}${argStr}${responseStr}`),
|
|
380
388
|
priority: 3,
|
|
381
389
|
}];
|
|
382
390
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project attribution heuristics for session events.
|
|
3
|
+
*
|
|
4
|
+
* Goal: avoid pinning all activity to the startup directory when work shifts
|
|
5
|
+
* across projects mid-session. This module resolves a best-effort project
|
|
6
|
+
* directory per event and attaches a confidence score + source signal.
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionEvent } from "../types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Confidence scores for project attribution sources.
|
|
11
|
+
*
|
|
12
|
+
* Higher = more reliable signal. The hierarchy reflects how directly
|
|
13
|
+
* the signal indicates the user's intended project:
|
|
14
|
+
* - Explicit config (workspace roots) > explicit navigation (cd) > implicit context
|
|
15
|
+
* - Path-bearing events score higher than fallbacks without path signals
|
|
16
|
+
*/
|
|
17
|
+
export declare const ATTRIBUTION_CONFIDENCE: {
|
|
18
|
+
/** Explicit workspace root from IDE/editor config */
|
|
19
|
+
readonly WORKSPACE_ROOT: 0.98;
|
|
20
|
+
/** User explicitly navigated here (cd command) */
|
|
21
|
+
readonly CWD_EVENT: 0.9;
|
|
22
|
+
/** Hook payload cwd — reliable but implicit */
|
|
23
|
+
readonly INPUT_CWD: 0.88;
|
|
24
|
+
/** Session startup directory */
|
|
25
|
+
readonly SESSION_ORIGIN: 0.82;
|
|
26
|
+
/** Carry-forward from previous high-confidence event */
|
|
27
|
+
readonly LAST_SEEN: 0.76;
|
|
28
|
+
/** Inferred from file path prefix matching */
|
|
29
|
+
readonly EVENT_PATH: 0.7;
|
|
30
|
+
/** Minimum confidence to carry forward as lastKnownProjectDir */
|
|
31
|
+
readonly CARRY_FORWARD_THRESHOLD: 0.55;
|
|
32
|
+
/** Fallback: input_cwd without path signal */
|
|
33
|
+
readonly FALLBACK_INPUT_CWD: 0.45;
|
|
34
|
+
/** Fallback: last_seen without path signal */
|
|
35
|
+
readonly FALLBACK_LAST_SEEN: 0.4;
|
|
36
|
+
/** Fallback: session_origin without path signal */
|
|
37
|
+
readonly FALLBACK_SESSION_ORIGIN: 0.35;
|
|
38
|
+
};
|
|
39
|
+
export type AttributionSource = "event_path" | "cwd_event" | "input_cwd" | "workspace_root" | "last_seen" | "session_origin" | "unknown";
|
|
40
|
+
export interface ProjectAttribution {
|
|
41
|
+
projectDir: string;
|
|
42
|
+
source: AttributionSource;
|
|
43
|
+
confidence: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AttributionContext {
|
|
46
|
+
sessionOriginDir?: string | null;
|
|
47
|
+
inputProjectDir?: string | null;
|
|
48
|
+
workspaceRoots?: string[] | null;
|
|
49
|
+
lastKnownProjectDir?: string | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the most likely project directory for one event.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveProjectAttribution(event: SessionEvent, context: AttributionContext): ProjectAttribution;
|
|
55
|
+
/**
|
|
56
|
+
* Convenience helper: resolve attributions for a stream of events while
|
|
57
|
+
* carrying forward the latest confident project as context.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveProjectAttributions(events: SessionEvent[], context: AttributionContext): ProjectAttribution[];
|
|
60
|
+
/**
|
|
61
|
+
* 0..100 score for UI display.
|
|
62
|
+
*/
|
|
63
|
+
export declare function confidenceToPercent(confidence: number): number;
|
|
64
|
+
/**
|
|
65
|
+
* True when attribution is strong enough for project-level spending claims.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isHighConfidenceAttribution(confidence: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Lightweight utility used by some hooks to normalize path separators
|
|
70
|
+
* before writing attribution metadata.
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeProjectDir(projectDir: string): string;
|
|
73
|
+
export declare const PROJECT_ATTRIBUTION_VERSION = 1;
|