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
@@ -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 (session_id, type, category, priority, data, source_hook, data_hash)
147
- VALUES (?, ?, ?, ?, ?, ?, ?)`);
148
- p(S.getEvents, `SELECT id, session_id, type, category, priority, data, source_hook, created_at, data_hash
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, source_hook, created_at, data_hash
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, source_hook, created_at, data_hash
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, source_hook, created_at, data_hash
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);
@@ -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;
@@ -0,0 +1,231 @@
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 { dirname, isAbsolute, normalize, resolve, sep } from "node:path";
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 const ATTRIBUTION_CONFIDENCE = {
18
+ /** Explicit workspace root from IDE/editor config */
19
+ WORKSPACE_ROOT: 0.98,
20
+ /** User explicitly navigated here (cd command) */
21
+ CWD_EVENT: 0.9,
22
+ /** Hook payload cwd — reliable but implicit */
23
+ INPUT_CWD: 0.88,
24
+ /** Session startup directory */
25
+ SESSION_ORIGIN: 0.82,
26
+ /** Carry-forward from previous high-confidence event */
27
+ LAST_SEEN: 0.76,
28
+ /** Inferred from file path prefix matching */
29
+ EVENT_PATH: 0.7,
30
+ /** Minimum confidence to carry forward as lastKnownProjectDir */
31
+ CARRY_FORWARD_THRESHOLD: 0.55,
32
+ /** Fallback: input_cwd without path signal */
33
+ FALLBACK_INPUT_CWD: 0.45,
34
+ /** Fallback: last_seen without path signal */
35
+ FALLBACK_LAST_SEEN: 0.4,
36
+ /** Fallback: session_origin without path signal */
37
+ FALLBACK_SESSION_ORIGIN: 0.35,
38
+ };
39
+ function normalizePath(path) {
40
+ const norm = normalize(path).replace(/\\/g, "/");
41
+ if (norm.length <= 1)
42
+ return norm;
43
+ return norm.replace(/\/+$/, "");
44
+ }
45
+ function isPrefixPath(path, prefix) {
46
+ if (!path || !prefix)
47
+ return false;
48
+ if (path === prefix)
49
+ return true;
50
+ return path.startsWith(`${prefix}/`);
51
+ }
52
+ function normalizeRoots(roots) {
53
+ if (!roots || roots.length === 0)
54
+ return [];
55
+ const normalized = roots
56
+ .filter((r) => typeof r === "string" && r.trim().length > 0)
57
+ .map((r) => normalizePath(r));
58
+ // dedupe + longest-first for stable best match
59
+ const unique = Array.from(new Set(normalized));
60
+ return unique.sort((a, b) => b.length - a.length);
61
+ }
62
+ function parseFileSearchPath(data) {
63
+ const marker = " in ";
64
+ const idx = data.lastIndexOf(marker);
65
+ if (idx < 0)
66
+ return null;
67
+ const path = data.slice(idx + marker.length).trim();
68
+ return path.length > 0 ? path : null;
69
+ }
70
+ function looksLikePath(value) {
71
+ if (!value)
72
+ return false;
73
+ // Fast path-like checks: separators, dot segments, drive roots.
74
+ return value.includes("/")
75
+ || value.includes("\\")
76
+ || value.startsWith(".")
77
+ || /^[A-Za-z]:[\\/]/.test(value);
78
+ }
79
+ function extractPathSignal(event) {
80
+ if (event.type === "cwd") {
81
+ return { rawPath: event.data, fromCwdEvent: true };
82
+ }
83
+ if (event.type === "file_search") {
84
+ const path = parseFileSearchPath(event.data);
85
+ if (path)
86
+ return { rawPath: path, fromCwdEvent: false };
87
+ }
88
+ const fileTypes = new Set([
89
+ "file_read",
90
+ "file_write",
91
+ "file_edit",
92
+ "file_glob",
93
+ "rule",
94
+ ]);
95
+ if (fileTypes.has(event.type) && looksLikePath(event.data)) {
96
+ return { rawPath: event.data, fromCwdEvent: false };
97
+ }
98
+ return null;
99
+ }
100
+ function absolutizePath(rawPath, context) {
101
+ if (!rawPath)
102
+ return null;
103
+ // Ignore broad glob-only patterns that aren't useful for attribution.
104
+ if (rawPath.includes("*") && !isAbsolute(rawPath) && !/^[A-Za-z]:[\\/]/.test(rawPath)) {
105
+ return null;
106
+ }
107
+ if (isAbsolute(rawPath) || /^[A-Za-z]:[\\/]/.test(rawPath)) {
108
+ return normalizePath(rawPath);
109
+ }
110
+ // For relative paths, anchor to the most recent known project first.
111
+ const anchor = context.lastKnownProjectDir
112
+ || context.inputProjectDir
113
+ || context.sessionOriginDir
114
+ || null;
115
+ if (!anchor)
116
+ return null;
117
+ return normalizePath(resolve(anchor, rawPath));
118
+ }
119
+ function inferProjectFromAbsolutePath(absPath, event, context) {
120
+ const normalizedRoots = normalizeRoots(context.workspaceRoots);
121
+ const normalizedOrigin = context.sessionOriginDir ? normalizePath(context.sessionOriginDir) : "";
122
+ const normalizedInput = context.inputProjectDir ? normalizePath(context.inputProjectDir) : "";
123
+ const normalizedLast = context.lastKnownProjectDir ? normalizePath(context.lastKnownProjectDir) : "";
124
+ // 1) Prefer explicit workspace roots (highest confidence).
125
+ const workspaceRoot = normalizedRoots.find((root) => isPrefixPath(absPath, root));
126
+ if (workspaceRoot) {
127
+ return { projectDir: workspaceRoot, source: "workspace_root", confidence: ATTRIBUTION_CONFIDENCE.WORKSPACE_ROOT };
128
+ }
129
+ // 2) Prefer stable known roots from session context.
130
+ if (normalizedInput && isPrefixPath(absPath, normalizedInput)) {
131
+ return { projectDir: normalizedInput, source: "input_cwd", confidence: ATTRIBUTION_CONFIDENCE.INPUT_CWD };
132
+ }
133
+ if (normalizedOrigin && isPrefixPath(absPath, normalizedOrigin)) {
134
+ return { projectDir: normalizedOrigin, source: "session_origin", confidence: ATTRIBUTION_CONFIDENCE.SESSION_ORIGIN };
135
+ }
136
+ if (normalizedLast && isPrefixPath(absPath, normalizedLast)) {
137
+ return { projectDir: normalizedLast, source: "last_seen", confidence: ATTRIBUTION_CONFIDENCE.LAST_SEEN };
138
+ }
139
+ // 3) Direct cwd events indicate explicit operator intent to shift project.
140
+ if (event.type === "cwd") {
141
+ return { projectDir: absPath, source: "cwd_event", confidence: ATTRIBUTION_CONFIDENCE.CWD_EVENT };
142
+ }
143
+ // 4) Fallback for out-of-root absolute paths.
144
+ // For known file events, use parent directory to avoid attributing to a file path.
145
+ const fileLike = new Set(["file_read", "file_write", "file_edit", "rule"]);
146
+ const projectDir = fileLike.has(event.type) ? normalizePath(dirname(absPath)) : absPath;
147
+ return { projectDir, source: "event_path", confidence: ATTRIBUTION_CONFIDENCE.EVENT_PATH };
148
+ }
149
+ function fallbackAttribution(context) {
150
+ if (context.inputProjectDir) {
151
+ return {
152
+ projectDir: normalizePath(context.inputProjectDir),
153
+ source: "input_cwd",
154
+ confidence: ATTRIBUTION_CONFIDENCE.FALLBACK_INPUT_CWD,
155
+ };
156
+ }
157
+ if (context.lastKnownProjectDir) {
158
+ return {
159
+ projectDir: normalizePath(context.lastKnownProjectDir),
160
+ source: "last_seen",
161
+ confidence: ATTRIBUTION_CONFIDENCE.FALLBACK_LAST_SEEN,
162
+ };
163
+ }
164
+ if (context.sessionOriginDir) {
165
+ return {
166
+ projectDir: normalizePath(context.sessionOriginDir),
167
+ source: "session_origin",
168
+ confidence: ATTRIBUTION_CONFIDENCE.FALLBACK_SESSION_ORIGIN,
169
+ };
170
+ }
171
+ return { projectDir: "", source: "unknown", confidence: 0 };
172
+ }
173
+ /**
174
+ * Resolve the most likely project directory for one event.
175
+ */
176
+ export function resolveProjectAttribution(event, context) {
177
+ try {
178
+ const pathSignal = extractPathSignal(event);
179
+ if (!pathSignal)
180
+ return fallbackAttribution(context);
181
+ const absPath = absolutizePath(pathSignal.rawPath, context);
182
+ if (!absPath)
183
+ return fallbackAttribution(context);
184
+ return inferProjectFromAbsolutePath(absPath, event, context);
185
+ }
186
+ catch {
187
+ return fallbackAttribution(context);
188
+ }
189
+ }
190
+ /**
191
+ * Convenience helper: resolve attributions for a stream of events while
192
+ * carrying forward the latest confident project as context.
193
+ */
194
+ export function resolveProjectAttributions(events, context) {
195
+ const out = [];
196
+ let lastKnown = context.lastKnownProjectDir ? normalizePath(context.lastKnownProjectDir) : "";
197
+ for (const ev of events) {
198
+ const attribution = resolveProjectAttribution(ev, {
199
+ ...context,
200
+ lastKnownProjectDir: lastKnown || context.lastKnownProjectDir || null,
201
+ });
202
+ out.push(attribution);
203
+ if (attribution.projectDir && attribution.confidence >= ATTRIBUTION_CONFIDENCE.CARRY_FORWARD_THRESHOLD) {
204
+ lastKnown = attribution.projectDir;
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+ /**
210
+ * 0..100 score for UI display.
211
+ */
212
+ export function confidenceToPercent(confidence) {
213
+ const clamped = Math.max(0, Math.min(1, confidence));
214
+ return Math.round(clamped * 100);
215
+ }
216
+ /**
217
+ * True when attribution is strong enough for project-level spending claims.
218
+ */
219
+ export function isHighConfidenceAttribution(confidence) {
220
+ return confidence >= 0.8;
221
+ }
222
+ /**
223
+ * Lightweight utility used by some hooks to normalize path separators
224
+ * before writing attribution metadata.
225
+ */
226
+ export function normalizeProjectDir(projectDir) {
227
+ return normalizePath(projectDir);
228
+ }
229
+ export const PROJECT_ATTRIBUTION_VERSION = 1;
230
+ // Keep explicit references to path separator for bundlers that tree-shake too aggressively.
231
+ void sep;
package/build/store.d.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  type SourceMatchMode = "like" | "exact";
11
11
  import type { IndexResult, SearchResult, StoreStats } from "./types.js";
12
12
  export type { IndexResult, SearchResult, StoreStats } from "./types.js";
13
+ export declare function sanitizeQuery(query: string, mode?: "AND" | "OR"): string;
14
+ export declare function sanitizeTrigramQuery(query: string, mode?: "AND" | "OR"): string;
13
15
  /**
14
16
  * Remove stale DB files from previous sessions whose processes no longer exist.
15
17
  */
@@ -24,6 +26,7 @@ export declare function cleanupStaleContentDBs(contentDir: string, maxAgeDays: n
24
26
  export declare class ContentStore {
25
27
  #private;
26
28
  static readonly OPTIMIZE_EVERY = 50;
29
+ static readonly FUZZY_CACHE_SIZE = 256;
27
30
  constructor(dbPath?: string);
28
31
  /** Delete this session's DB files. Call on process exit. */
29
32
  cleanup(): void;
@@ -50,11 +53,15 @@ export declare class ContentStore {
50
53
  searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
51
54
  fuzzyCorrect(query: string): string | null;
52
55
  searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose", sourceMatchMode?: SourceMatchMode): SearchResult[];
56
+ /** Number of sources auto-refreshed in the last searchWithFallback call. */
57
+ lastRefreshCount: number;
53
58
  getSourceMeta(label: string): {
54
59
  label: string;
55
60
  chunkCount: number;
56
61
  codeChunkCount: number;
57
62
  indexedAt: string;
63
+ filePath: string | null;
64
+ contentHash: string | null;
58
65
  } | null;
59
66
  listSources(): Array<{
60
67
  label: string;