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.
- 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 +27 -141
- 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 +113 -47
- 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/pi-extension.js +24 -7
- package/build/runtime.js +24 -9
- package/build/security.d.ts +17 -1
- package/build/security.js +40 -6
- package/build/server.js +129 -21
- package/build/session/analytics.d.ts +8 -7
- package/build/session/analytics.js +95 -75
- 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 +7 -0
- package/build/store.js +117 -18
- package/build/truncate.d.ts +6 -0
- package/build/truncate.js +51 -29
- package/build/types.d.ts +8 -0
- package/cli.bundle.mjs +157 -136
- 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 +43 -20
- 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/session-snapshot.bundle.mjs +14 -14
- 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 +262 -32
- 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 +117 -99
- package/skills/ctx-insight/SKILL.md +1 -1
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;
|
|
@@ -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;
|