context-mode 1.0.101 → 1.0.104
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 +66 -5
- package/bin/statusline.mjs +321 -0
- package/build/adapters/antigravity/index.d.ts +6 -0
- package/build/adapters/antigravity/index.js +10 -0
- package/build/adapters/base.d.ts +23 -0
- package/build/adapters/base.js +29 -0
- package/build/adapters/codex/index.d.ts +10 -0
- package/build/adapters/codex/index.js +22 -4
- package/build/adapters/cursor/index.d.ts +7 -0
- package/build/adapters/cursor/index.js +11 -0
- package/build/adapters/detect.d.ts +12 -1
- package/build/adapters/detect.js +69 -7
- package/build/adapters/gemini-cli/index.d.ts +8 -1
- package/build/adapters/gemini-cli/index.js +19 -7
- package/build/adapters/jetbrains-copilot/index.d.ts +7 -0
- package/build/adapters/jetbrains-copilot/index.js +12 -0
- package/build/adapters/kiro/index.d.ts +8 -0
- package/build/adapters/kiro/index.js +12 -0
- package/build/adapters/openclaw/index.d.ts +17 -0
- package/build/adapters/openclaw/index.js +29 -4
- package/build/adapters/opencode/index.d.ts +8 -0
- package/build/adapters/opencode/index.js +18 -6
- package/build/adapters/qwen-code/index.d.ts +1 -0
- package/build/adapters/qwen-code/index.js +3 -0
- package/build/adapters/types.d.ts +33 -0
- package/build/adapters/vscode-copilot/index.d.ts +6 -0
- package/build/adapters/vscode-copilot/index.js +10 -0
- package/build/adapters/zed/index.d.ts +1 -0
- package/build/adapters/zed/index.js +3 -0
- package/build/cli.d.ts +15 -0
- package/build/cli.js +62 -16
- package/build/concurrency/runPool.d.ts +36 -0
- package/build/concurrency/runPool.js +51 -0
- package/build/executor.d.ts +11 -1
- package/build/executor.js +59 -16
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +6 -2
- package/build/lifecycle.js +29 -2
- package/build/opencode-plugin.d.ts +6 -0
- package/build/opencode-plugin.js +60 -1
- package/build/routing-block.d.ts +8 -0
- package/build/routing-block.js +86 -0
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +54 -3
- package/build/search/auto-memory.d.ts +23 -10
- package/build/search/auto-memory.js +64 -26
- package/build/search/unified.d.ts +3 -0
- package/build/search/unified.js +2 -2
- package/build/server.d.ts +42 -0
- package/build/server.js +693 -164
- package/build/session/analytics.d.ts +49 -1
- package/build/session/analytics.js +278 -16
- package/build/session/db.d.ts +39 -8
- package/build/session/db.js +170 -19
- package/build/session/extract.js +124 -2
- package/build/tool-naming.d.ts +4 -0
- package/build/tool-naming.js +24 -0
- package/cli.bundle.mjs +201 -159
- package/configs/antigravity/GEMINI.md +11 -0
- package/configs/claude-code/CLAUDE.md +11 -0
- package/configs/codex/AGENTS.md +11 -0
- package/configs/cursor/context-mode.mdc +11 -0
- package/configs/gemini-cli/GEMINI.md +11 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +3 -0
- package/configs/kilo/AGENTS.md +11 -0
- package/configs/kiro/KIRO.md +11 -0
- package/configs/openclaw/AGENTS.md +11 -0
- package/configs/opencode/AGENTS.md +11 -0
- package/configs/pi/AGENTS.md +11 -0
- package/configs/qwen-code/QWEN.md +11 -0
- package/configs/vscode-copilot/copilot-instructions.md +3 -0
- package/configs/zed/AGENTS.md +11 -0
- package/hooks/auto-injection.mjs +36 -10
- package/hooks/cache-heal-utils.mjs +231 -0
- package/hooks/codex/sessionstart.mjs +7 -4
- package/hooks/core/routing.mjs +5 -0
- package/hooks/cursor/sessionstart.mjs +7 -4
- package/hooks/formatters/claude-code.mjs +20 -0
- package/hooks/gemini-cli/sessionstart.mjs +7 -2
- package/hooks/jetbrains-copilot/sessionstart.mjs +7 -2
- package/hooks/normalize-hooks.mjs +184 -0
- package/hooks/session-db.bundle.mjs +33 -14
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-helpers.mjs +68 -20
- package/hooks/session-loaders.mjs +8 -2
- package/hooks/sessionstart.mjs +8 -2
- package/hooks/vscode-copilot/sessionstart.mjs +7 -2
- package/insight/src/routes/index.tsx +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server.bundle.mjs +164 -125
- package/skills/ctx-insight/SKILL.md +1 -1
- package/start.mjs +63 -3
|
@@ -16,7 +16,7 @@ import { createToolNamer } from "../core/tool-naming.mjs";
|
|
|
16
16
|
|
|
17
17
|
const toolNamer = createToolNamer("gemini-cli");
|
|
18
18
|
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
19
|
-
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents
|
|
19
|
+
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "../session-directive.mjs";
|
|
20
20
|
import {
|
|
21
21
|
readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
|
|
22
22
|
getProjectDir, GEMINI_OPTS,
|
|
@@ -63,7 +63,12 @@ try {
|
|
|
63
63
|
const dbPath = getSessionDBPath(OPTS);
|
|
64
64
|
const db = new SessionDB({ dbPath });
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// Filter events to the session being resumed. Falling back to
|
|
67
|
+
// getLatestSessionEvents(db) leaks events from any other session whose
|
|
68
|
+
// session_meta.started_at is more recent — observed cross-session bleed
|
|
69
|
+
// when a different session started after this one and before the resume.
|
|
70
|
+
const sessionId = getSessionId(input, OPTS);
|
|
71
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
67
72
|
if (events.length > 0) {
|
|
68
73
|
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
|
|
69
74
|
additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
|
|
@@ -17,7 +17,7 @@ import { createToolNamer } from "../core/tool-naming.mjs";
|
|
|
17
17
|
|
|
18
18
|
const toolNamer = createToolNamer("jetbrains-copilot");
|
|
19
19
|
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
20
|
-
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents
|
|
20
|
+
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "../session-directive.mjs";
|
|
21
21
|
import {
|
|
22
22
|
readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
|
|
23
23
|
getProjectDir, JETBRAINS_OPTS,
|
|
@@ -63,7 +63,12 @@ try {
|
|
|
63
63
|
const dbPath = getSessionDBPath(OPTS);
|
|
64
64
|
const db = new SessionDB({ dbPath });
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// Filter events to the session being resumed. Falling back to
|
|
67
|
+
// getLatestSessionEvents(db) leaks events from any other session whose
|
|
68
|
+
// session_meta.started_at is more recent — observed cross-session bleed
|
|
69
|
+
// when a different session started after this one and before the resume.
|
|
70
|
+
const sessionId = getSessionId(input, OPTS);
|
|
71
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
67
72
|
if (events.length > 0) {
|
|
68
73
|
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
|
|
69
74
|
additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// normalize-hooks.mjs — fixes #378
|
|
2
|
+
//
|
|
3
|
+
// Static committed files (hooks/hooks.json, .claude-plugin/plugin.json) ship
|
|
4
|
+
// with `${CLAUDE_PLUGIN_ROOT}` placeholder + bare `node` command. On Windows
|
|
5
|
+
// + Claude Code this triggers cjs/loader:1479 errors because:
|
|
6
|
+
// 1. bare `node` may not resolve via PATH (Git Bash, see #369)
|
|
7
|
+
// 2. `${CLAUDE_PLUGIN_ROOT}` resolution can hit MSYS path mangling (#372)
|
|
8
|
+
// 3. backslash paths get corrupted in shell quoting
|
|
9
|
+
//
|
|
10
|
+
// Our buildNodeCommand() fix handles dynamically-generated settings.json but
|
|
11
|
+
// not the static committed files. Solution: start.mjs detects the placeholder
|
|
12
|
+
// pattern on every MCP boot and rewrites with absolute paths using
|
|
13
|
+
// process.execPath + forward slashes. Idempotent — only rewrites when needed.
|
|
14
|
+
// Survives upgrades because it runs at every start.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
|
|
20
|
+
|
|
21
|
+
/** Convert any path string to forward slashes (MSYS-safe). */
|
|
22
|
+
function fwd(p) {
|
|
23
|
+
return String(p).replace(/\\/g, "/");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pure detection: does this content contain an unresolved CLAUDE_PLUGIN_ROOT
|
|
28
|
+
* placeholder that should be normalized?
|
|
29
|
+
*/
|
|
30
|
+
export function needsHookNormalization(content) {
|
|
31
|
+
if (!content || typeof content !== "string") return false;
|
|
32
|
+
return content.includes(PLACEHOLDER);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rewrite hooks.json content. Replaces:
|
|
37
|
+
* - `node "${CLAUDE_PLUGIN_ROOT}/x.mjs"` →
|
|
38
|
+
* `"<execPath>" "<pluginRoot>/x.mjs"` (forward slashes, double-quoted)
|
|
39
|
+
*
|
|
40
|
+
* Pure function — takes content + paths, returns new content.
|
|
41
|
+
* Idempotent — leaves already-normalized content unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeHooksJson(content, nodePath, pluginRoot) {
|
|
44
|
+
if (!needsHookNormalization(content)) return content;
|
|
45
|
+
|
|
46
|
+
const safeNode = fwd(nodePath);
|
|
47
|
+
const safeRoot = fwd(pluginRoot);
|
|
48
|
+
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(content);
|
|
52
|
+
} catch {
|
|
53
|
+
return content;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hooks = parsed?.hooks;
|
|
57
|
+
if (!hooks || typeof hooks !== "object") return content;
|
|
58
|
+
|
|
59
|
+
let mutated = false;
|
|
60
|
+
for (const eventName of Object.keys(hooks)) {
|
|
61
|
+
const matchers = hooks[eventName];
|
|
62
|
+
if (!Array.isArray(matchers)) continue;
|
|
63
|
+
for (const matcher of matchers) {
|
|
64
|
+
const inner = matcher?.hooks;
|
|
65
|
+
if (!Array.isArray(inner)) continue;
|
|
66
|
+
for (const h of inner) {
|
|
67
|
+
if (typeof h?.command !== "string") continue;
|
|
68
|
+
if (!h.command.includes(PLACEHOLDER)) continue;
|
|
69
|
+
// Replace placeholder with absolute root (forward-slash).
|
|
70
|
+
let next = h.command.replaceAll(PLACEHOLDER, safeRoot);
|
|
71
|
+
// Replace bare `node ` prefix with quoted execPath. Match both
|
|
72
|
+
// `node ` and `node\t` at start, with optional surrounding whitespace.
|
|
73
|
+
next = next.replace(/^\s*node\s+/, `"${safeNode}" `);
|
|
74
|
+
h.command = next;
|
|
75
|
+
mutated = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!mutated) return content;
|
|
81
|
+
|
|
82
|
+
// Preserve 2-space indent (matches committed format).
|
|
83
|
+
return JSON.stringify(parsed, null, 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite plugin.json mcpServers. Replaces:
|
|
88
|
+
* - `command: "node"` → `command: "<execPath-fwd>"`
|
|
89
|
+
* - `args: ["${CLAUDE_PLUGIN_ROOT}/start.mjs"]` →
|
|
90
|
+
* `args: ["<pluginRoot-fwd>/start.mjs"]`
|
|
91
|
+
*
|
|
92
|
+
* Idempotent.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizePluginJson(content, nodePath, pluginRoot) {
|
|
95
|
+
if (!needsHookNormalization(content)) return content;
|
|
96
|
+
|
|
97
|
+
const safeNode = fwd(nodePath);
|
|
98
|
+
const safeRoot = fwd(pluginRoot);
|
|
99
|
+
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(content);
|
|
103
|
+
} catch {
|
|
104
|
+
return content;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const servers = parsed?.mcpServers;
|
|
108
|
+
if (!servers || typeof servers !== "object") return content;
|
|
109
|
+
|
|
110
|
+
let mutated = false;
|
|
111
|
+
for (const name of Object.keys(servers)) {
|
|
112
|
+
const srv = servers[name];
|
|
113
|
+
if (!srv || typeof srv !== "object") continue;
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(srv.args)) {
|
|
116
|
+
const before = srv.args;
|
|
117
|
+
const after = before.map((a) =>
|
|
118
|
+
typeof a === "string" && a.includes(PLACEHOLDER)
|
|
119
|
+
? a.replaceAll(PLACEHOLDER, safeRoot)
|
|
120
|
+
: a,
|
|
121
|
+
);
|
|
122
|
+
if (after.some((v, i) => v !== before[i])) {
|
|
123
|
+
srv.args = after;
|
|
124
|
+
mutated = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (srv.command === "node" && mutated) {
|
|
129
|
+
// Only swap bare `node` when we also rewrote args — otherwise we'd
|
|
130
|
+
// touch user-customized server entries unrelated to placeholders.
|
|
131
|
+
srv.command = safeNode;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!mutated) return content;
|
|
136
|
+
return JSON.stringify(parsed, null, 2);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply normalization to hooks.json and plugin.json on startup.
|
|
141
|
+
*
|
|
142
|
+
* Options:
|
|
143
|
+
* - pluginRoot: absolute path to plugin install dir (e.g. __dirname of start.mjs)
|
|
144
|
+
* - nodePath: process.execPath
|
|
145
|
+
* - platform: process.platform (only "win32" triggers a write)
|
|
146
|
+
*
|
|
147
|
+
* Best-effort — never throws.
|
|
148
|
+
*/
|
|
149
|
+
export function normalizeHooksOnStartup({ pluginRoot, nodePath, platform }) {
|
|
150
|
+
if (platform !== "win32") return;
|
|
151
|
+
if (!pluginRoot || !nodePath) return;
|
|
152
|
+
|
|
153
|
+
// hooks/hooks.json
|
|
154
|
+
try {
|
|
155
|
+
const hooksPath = resolve(pluginRoot, "hooks", "hooks.json");
|
|
156
|
+
if (existsSync(hooksPath)) {
|
|
157
|
+
const original = readFileSync(hooksPath, "utf-8");
|
|
158
|
+
if (needsHookNormalization(original)) {
|
|
159
|
+
const next = normalizeHooksJson(original, nodePath, pluginRoot);
|
|
160
|
+
if (next !== original) {
|
|
161
|
+
writeFileSync(hooksPath, next, "utf-8");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
/* best effort */
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// .claude-plugin/plugin.json
|
|
170
|
+
try {
|
|
171
|
+
const pluginPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
|
|
172
|
+
if (existsSync(pluginPath)) {
|
|
173
|
+
const original = readFileSync(pluginPath, "utf-8");
|
|
174
|
+
if (needsHookNormalization(original)) {
|
|
175
|
+
const next = normalizePluginJson(original, nodePath, pluginRoot);
|
|
176
|
+
if (next !== original) {
|
|
177
|
+
writeFileSync(pluginPath, next, "utf-8");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
/* best effort */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as
|
|
1
|
+
import{createRequire as I}from"node:module";import{existsSync as U,unlinkSync as v,renameSync as M}from"node:fs";import{tmpdir as x}from"node:os";import{join as F}from"node:path";var g=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){let s="",e=null;for(let o=0;o<t.length;o++){let a=t[o];if(e)s+=a,a===e&&(e=null);else if(a==="'"||a==='"')s+=a,e=a;else if(a===";"){let c=s.trim();c&&this.#t.prepare(c).run(),s=""}else s+=a}let r=s.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>{let r=s.get(...e);return r===null?void 0:r},all:(...e)=>s.all(...e),iterate:(...e)=>s.iterate(...e)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},h=class{#t;constructor(t){this.#t=t}pragma(t){let e=this.#t.prepare(`PRAGMA ${t}`).all();if(!e||e.length===0)return;if(e.length>1)return e;let r=Object.values(e[0]);return r.length===1?r[0]:e[0]}exec(t){return this.#t.exec(t),this}prepare(t){let s=this.#t.prepare(t);return{run:(...e)=>s.run(...e),get:(...e)=>s.get(...e),all:(...e)=>s.all(...e),iterate:(...e)=>typeof s.iterate=="function"?s.iterate(...e):s.all(...e)[Symbol.iterator]()}}transaction(t){return(...s)=>{this.#t.exec("BEGIN");try{let e=t(...s);return this.#t.exec("COMMIT"),e}catch(e){throw this.#t.exec("ROLLBACK"),e}}}close(){this.#t.close()}},d=null;function B(){if(!d){let i=I(import.meta.url);if(globalThis.Bun){let t=i(["bun","sqlite"].join(":")).Database;d=function(e,r){let o=new t(e,{readonly:r?.readonly,create:!0}),a=new g(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(process.platform==="linux")try{let{DatabaseSync:t}=i(["node","sqlite"].join(":"));d=function(e,r){let o=new t(e,{readOnly:r?.readonly??!1});return new h(o)}}catch{d=i("better-sqlite3")}else d=i("better-sqlite3")}return d}function b(i){i.pragma("journal_mode = WAL"),i.pragma("synchronous = NORMAL");try{i.pragma("mmap_size = 268435456")}catch{}}function N(i){if(!U(i))for(let t of["-wal","-shm"])try{v(i+t)}catch{}}function P(i){for(let t of["","-wal","-shm"])try{v(i+t)}catch{}}function y(i){try{i.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{i.close()}catch{}}function C(i="context-mode"){return F(x(),`${i}-${process.pid}.db`)}function k(i,t=[100,500,2e3]){let s;for(let e=0;e<=t.length;e++)try{return i()}catch(r){let o=r instanceof Error?r.message:String(r);if(!o.includes("SQLITE_BUSY")&&!o.includes("database is locked"))throw r;if(s=r instanceof Error?r:new Error(o),e<t.length){let a=t[e],c=Date.now();for(;Date.now()-c<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${s?.message}`)}function j(i){return i.includes("SQLITE_CORRUPT")||i.includes("SQLITE_NOTADB")||i.includes("database disk image is malformed")||i.includes("file is not a database")}function X(i){let t=Date.now();for(let s of["","-wal","-shm"])try{M(i+s,`${i}${s}.corrupt-${t}`)}catch{}}var m=Symbol.for("__context_mode_live_dbs__"),p=(()=>{let i=globalThis;return i[m]||(i[m]=new Set,process.on("exit",()=>{for(let t of i[m])y(t);i[m].clear()})),i[m]})(),T=class{#t;#e;constructor(t){let s=B();this.#t=t,N(t);let e;try{e=new s(t,{timeout:3e4}),b(e)}catch(r){let o=r instanceof Error?r.message:String(r);if(j(o)){X(t),N(t);try{e=new s(t,{timeout:3e4}),b(e)}catch(a){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw r}this.#e=e,p.add(this.#e),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){p.delete(this.#e),y(this.#e)}withRetry(t){return k(t)}cleanup(){p.delete(this.#e),y(this.#e),P(this.#t)}};import{createHash as f}from"node:crypto";import{execFileSync as W}from"node:child_process";var l;function z(){let i=process.env.CONTEXT_MODE_SESSION_SUFFIX,t=process.cwd();if(l&&l.cwd===t&&l.envSuffix===i)return l.suffix;let s="";if(i!==void 0)s=i?`__${i}`:"";else try{let e=W("git",["worktree","list","--porcelain"],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).split(/\r?\n/).find(r=>r.startsWith("worktree "))?.replace("worktree ","")?.trim();e&&t!==e&&(s=`__${f("sha256").update(t).digest("hex").slice(0,8)}`)}catch{}return l={cwd:t,envSuffix:i,suffix:s},s}function J(){l=void 0}var O=1e3,D=5,n={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool"},A=class extends T{constructor(t){super(t?.dbPath??C("session"))}stmt(t){return this.stmts.get(t)}initSchema(){try{let s=this.db.pragma("table_xinfo(session_events)").find(e=>e.name==="data_hash");s&&s.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS session_events (
|
|
3
3
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
4
|
session_id TEXT NOT NULL,
|
|
@@ -35,48 +35,67 @@ import{createRequire as b}from"node:module";import{existsSync as N,unlinkSync as
|
|
|
35
35
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
36
36
|
consumed INTEGER NOT NULL DEFAULT 0
|
|
37
37
|
);
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
40
|
+
session_id TEXT NOT NULL,
|
|
41
|
+
tool TEXT NOT NULL,
|
|
42
|
+
calls INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
bytes_returned INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
45
|
+
PRIMARY KEY (session_id, tool)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
|
|
49
|
+
`);try{let t=this.db.pragma("table_xinfo(session_events)"),s=new Set(t.map(e=>e.name));s.has("project_dir")||this.db.exec("ALTER TABLE session_events ADD COLUMN project_dir TEXT NOT NULL DEFAULT ''"),s.has("attribution_source")||this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_source TEXT NOT NULL DEFAULT 'unknown'"),s.has("attribution_confidence")||this.db.exec("ALTER TABLE session_events ADD COLUMN attribution_confidence REAL NOT NULL DEFAULT 0"),this.db.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)")}catch{}}prepareStatements(){this.stmts=new Map;let t=(s,e)=>{this.stmts.set(s,this.db.prepare(e))};t(n.insertEvent,`INSERT INTO session_events (
|
|
39
50
|
session_id, type, category, priority, data,
|
|
40
51
|
project_dir, attribution_source, attribution_confidence,
|
|
41
52
|
source_hook, data_hash
|
|
42
53
|
)
|
|
43
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),t(
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),t(n.getEvents,`SELECT id, session_id, type, category, priority, data,
|
|
44
55
|
project_dir, attribution_source, attribution_confidence,
|
|
45
56
|
source_hook, created_at, data_hash
|
|
46
|
-
FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`),t(
|
|
57
|
+
FROM session_events WHERE session_id = ? ORDER BY id ASC LIMIT ?`),t(n.getEventsByType,`SELECT id, session_id, type, category, priority, data,
|
|
47
58
|
project_dir, attribution_source, attribution_confidence,
|
|
48
59
|
source_hook, created_at, data_hash
|
|
49
|
-
FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`),t(
|
|
60
|
+
FROM session_events WHERE session_id = ? AND type = ? ORDER BY id ASC LIMIT ?`),t(n.getEventsByPriority,`SELECT id, session_id, type, category, priority, data,
|
|
50
61
|
project_dir, attribution_source, attribution_confidence,
|
|
51
62
|
source_hook, created_at, data_hash
|
|
52
|
-
FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(
|
|
63
|
+
FROM session_events WHERE session_id = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(n.getEventsByTypeAndPriority,`SELECT id, session_id, type, category, priority, data,
|
|
53
64
|
project_dir, attribution_source, attribution_confidence,
|
|
54
65
|
source_hook, created_at, data_hash
|
|
55
|
-
FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(
|
|
66
|
+
FROM session_events WHERE session_id = ? AND type = ? AND priority >= ? ORDER BY id ASC LIMIT ?`),t(n.getEventCount,"SELECT COUNT(*) AS cnt FROM session_events WHERE session_id = ?"),t(n.getLatestAttributedProject,`SELECT project_dir
|
|
56
67
|
FROM session_events
|
|
57
68
|
WHERE session_id = ? AND project_dir != ''
|
|
58
69
|
ORDER BY id DESC
|
|
59
|
-
LIMIT 1`),t(
|
|
70
|
+
LIMIT 1`),t(n.checkDuplicate,`SELECT 1 FROM (
|
|
60
71
|
SELECT type, data_hash FROM session_events
|
|
61
72
|
WHERE session_id = ? ORDER BY id DESC LIMIT ?
|
|
62
73
|
) AS recent
|
|
63
74
|
WHERE recent.type = ? AND recent.data_hash = ?
|
|
64
|
-
LIMIT 1`),t(
|
|
75
|
+
LIMIT 1`),t(n.evictLowestPriority,`DELETE FROM session_events WHERE id = (
|
|
65
76
|
SELECT id FROM session_events WHERE session_id = ?
|
|
66
77
|
ORDER BY priority ASC, id ASC LIMIT 1
|
|
67
|
-
)`),t(
|
|
78
|
+
)`),t(n.updateMetaLastEvent,`UPDATE session_meta
|
|
68
79
|
SET last_event_at = datetime('now'), event_count = event_count + 1
|
|
69
|
-
WHERE session_id = ?`),t(
|
|
70
|
-
FROM session_meta WHERE session_id = ?`),t(
|
|
80
|
+
WHERE session_id = ?`),t(n.ensureSession,"INSERT OR IGNORE INTO session_meta (session_id, project_dir) VALUES (?, ?)"),t(n.getSessionStats,`SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count
|
|
81
|
+
FROM session_meta WHERE session_id = ?`),t(n.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),t(n.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
|
|
71
82
|
VALUES (?, ?, ?)
|
|
72
83
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
73
84
|
snapshot = excluded.snapshot,
|
|
74
85
|
event_count = excluded.event_count,
|
|
75
86
|
created_at = datetime('now'),
|
|
76
|
-
consumed = 0`),t(
|
|
87
|
+
consumed = 0`),t(n.getResume,"SELECT snapshot, event_count, consumed FROM session_resume WHERE session_id = ?"),t(n.markResumeConsumed,"UPDATE session_resume SET consumed = 1 WHERE session_id = ?"),t(n.deleteEvents,"DELETE FROM session_events WHERE session_id = ?"),t(n.deleteMeta,"DELETE FROM session_meta WHERE session_id = ?"),t(n.deleteResume,"DELETE FROM session_resume WHERE session_id = ?"),t(n.searchEvents,`SELECT id, session_id, category, type, data, created_at
|
|
77
88
|
FROM session_events
|
|
78
89
|
WHERE project_dir = ?
|
|
79
90
|
AND (data LIKE '%' || ? || '%' ESCAPE '\\' OR category LIKE '%' || ? || '%' ESCAPE '\\')
|
|
80
91
|
AND (? IS NULL OR category = ?)
|
|
81
92
|
ORDER BY id ASC
|
|
82
|
-
LIMIT ?`),t(
|
|
93
|
+
LIMIT ?`),t(n.getOldSessions,"SELECT session_id FROM session_meta WHERE started_at < datetime('now', ? || ' days')"),t(n.incrementToolCall,`INSERT INTO tool_calls (session_id, tool, calls, bytes_returned)
|
|
94
|
+
VALUES (?, ?, 1, ?)
|
|
95
|
+
ON CONFLICT(session_id, tool) DO UPDATE SET
|
|
96
|
+
calls = calls + 1,
|
|
97
|
+
bytes_returned = bytes_returned + excluded.bytes_returned,
|
|
98
|
+
updated_at = datetime('now')`),t(n.getToolCallTotals,`SELECT COALESCE(SUM(calls), 0) AS calls,
|
|
99
|
+
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
100
|
+
FROM tool_calls WHERE session_id = ?`),t(n.getToolCallByTool,`SELECT tool, calls, bytes_returned
|
|
101
|
+
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`)}insertEvent(t,s,e="PostToolUse",r){let o=f("sha256").update(s.data).digest("hex").slice(0,16).toUpperCase(),a=String(r?.projectDir??s.project_dir??"").trim(),c=String(r?.source??s.attribution_source??"unknown"),u=Number(r?.confidence??s.attribution_confidence??0),_=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,E=this.db.transaction(()=>{if(this.stmt(n.checkDuplicate).get(t,D,s.type,o))return;this.stmt(n.getEventCount).get(t).cnt>=O&&this.stmt(n.evictLowestPriority).run(t),this.stmt(n.insertEvent).run(t,s.type,s.category,s.priority,s.data,a,c,_,e,o),this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>E())}bulkInsertEvents(t,s,e="PostToolUse",r){if(!s||s.length===0)return;if(s.length===1){this.insertEvent(t,s[0],e,r?.[0]);return}let o=s.map((c,u)=>{let _=f("sha256").update(c.data).digest("hex").slice(0,16).toUpperCase(),E=r?.[u],S=String(E?.projectDir??c.project_dir??"").trim(),L=String(E?.source??c.attribution_source??"unknown"),R=Number(E?.confidence??c.attribution_confidence??0),w=Number.isFinite(R)?Math.max(0,Math.min(1,R)):0;return{event:c,dataHash:_,projectDir:S,attributionSource:L,attributionConfidence:w}}),a=this.db.transaction(()=>{let c=this.stmt(n.getEventCount).get(t).cnt;for(let u of o)this.stmt(n.checkDuplicate).get(t,D,u.event.type,u.dataHash)||(c>=O?this.stmt(n.evictLowestPriority).run(t):c++,this.stmt(n.insertEvent).run(t,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,e,u.dataHash));this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>a())}getEvents(t,s){let e=s?.limit??1e3,r=s?.type,o=s?.minPriority;return r&&o!==void 0?this.stmt(n.getEventsByTypeAndPriority).all(t,r,o,e):r?this.stmt(n.getEventsByType).all(t,r,e):o!==void 0?this.stmt(n.getEventsByPriority).all(t,o,e):this.stmt(n.getEvents).all(t,e)}getEventCount(t){return this.stmt(n.getEventCount).get(t).cnt}getLatestAttributedProjectDir(t){return this.stmt(n.getLatestAttributedProject).get(t)?.project_dir||null}searchEvents(t,s,e,r){try{let o=t.replace(/[%_]/g,c=>"\\"+c),a=r??null;return this.stmt(n.searchEvents).all(e,o,o,a,a,s)}catch{return[]}}ensureSession(t,s){this.stmt(n.ensureSession).run(t,s)}getSessionStats(t){return this.stmt(n.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(n.incrementCompactCount).run(t)}upsertResume(t,s,e){this.stmt(n.upsertResume).run(t,s,e??0)}getResume(t){return this.stmt(n.getResume).get(t)??null}markResumeConsumed(t){this.stmt(n.markResumeConsumed).run(t)}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(t,s,e=0){let r=Number.isFinite(e)&&e>0?Math.round(e):0;try{this.stmt(n.incrementToolCall).run(t,s,r)}catch{}}getToolCallStats(t){try{let s=this.stmt(n.getToolCallTotals).get(t),e=this.stmt(n.getToolCallByTool).all(t),r={};for(let o of e)r[o.tool]={calls:o.calls,bytesReturned:o.bytes_returned};return{totalCalls:s?.calls??0,totalBytesReturned:s?.bytes_returned??0,byTool:r}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(t){this.db.transaction(()=>{this.stmt(n.deleteEvents).run(t),this.stmt(n.deleteResume).run(t),this.stmt(n.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let s=`-${t}`,e=this.stmt(n.getOldSessions).all(s);for(let{session_id:r}of e)this.deleteSession(r);return e.length}};export{A as SessionDB,J as _resetWorktreeSuffixCacheForTests,z as getWorktreeSuffix};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function
|
|
2
|
-
response: ${i
|
|
1
|
+
function s(t){return t==null?"":String(t)}function f(t){return t==null?"":typeof t=="string"?t:JSON.stringify(t)}function b(t){let{tool_name:e,tool_input:o,tool_response:i}=t,r=[];if(e==="Read"){let n=String(o.file_path??"");return(/(?:CLAUDE|AGENTS(?:\.override)?|GEMINI|QWEN|KIRO)\.md$/i.test(n)||/\/copilot-instructions\.md$/i.test(n)||/\/context-mode\.mdc$/i.test(n)||/\.claude[\\/]/i.test(n)||/[\\/]memor(?:y|ies)[\\/][^\\/]+\.md$/i.test(n))&&(r.push({type:"rule",category:"rule",data:s(n),priority:1}),i&&i.length>0&&r.push({type:"rule_content",category:"rule",data:s(i),priority:1})),r.push({type:"file_read",category:"file",data:s(n),priority:1}),r}if(e==="Edit"){let n=String(o.file_path??"");return r.push({type:"file_edit",category:"file",data:s(n),priority:1}),r}if(e==="NotebookEdit"){let n=String(o.notebook_path??"");return r.push({type:"file_edit",category:"file",data:s(n),priority:1}),r}if(e==="Write"){let n=String(o.file_path??"");return r.push({type:"file_write",category:"file",data:s(n),priority:1}),r}if(e==="Glob"){let n=String(o.pattern??"");return r.push({type:"file_glob",category:"file",data:s(n),priority:3}),r}if(e==="Grep"){let n=String(o.pattern??""),a=String(o.path??"");return r.push({type:"file_search",category:"file",data:s(`${n} in ${a}`),priority:3}),r}return r}function y(t){if(t.tool_name!=="Bash")return[];let o=String(t.tool_input.command??"").match(/\bcd\s+("([^"]+)"|'([^']+)'|(\S+))/);if(!o)return[];let i=o[2]??o[3]??o[4]??"";return[{type:"cwd",category:"cwd",data:s(i),priority:2}]}function h(t){let{tool_name:e,tool_input:o,tool_response:i,tool_output:r}=t,n=String(i??""),a=r?.isError===!0;return!(e==="Bash"&&/exit code [1-9]|error:|Error:|FAIL|failed/i.test(n))&&!a?[]:[{type:"error_tool",category:"error",data:s(n),priority:2}]}var _=[{pattern:/\bgit\s+checkout\b/,operation:"branch"},{pattern:/\bgit\s+commit\b/,operation:"commit"},{pattern:/\bgit\s+merge\s+\S+/,operation:"merge"},{pattern:/\bgit\s+rebase\b/,operation:"rebase"},{pattern:/\bgit\s+stash\b/,operation:"stash"},{pattern:/\bgit\s+push\b/,operation:"push"},{pattern:/\bgit\s+pull\b/,operation:"pull"},{pattern:/\bgit\s+log\b/,operation:"log"},{pattern:/\bgit\s+diff\b/,operation:"diff"},{pattern:/\bgit\s+status\b/,operation:"status"},{pattern:/\bgit\s+branch\b/,operation:"branch"},{pattern:/\bgit\s+reset\b/,operation:"reset"},{pattern:/\bgit\s+add\b/,operation:"add"},{pattern:/\bgit\s+cherry-pick\b/,operation:"cherry-pick"},{pattern:/\bgit\s+tag\b/,operation:"tag"},{pattern:/\bgit\s+fetch\b/,operation:"fetch"},{pattern:/\bgit\s+clone\b/,operation:"clone"},{pattern:/\bgit\s+worktree\b/,operation:"worktree"}];function m(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??""),o=_.find(i=>i.pattern.test(e));return o?[{type:"git",category:"git",data:s(o.operation),priority:2}]:[]}function S(t){return new Set(["TodoWrite","TaskCreate","TaskUpdate"]).has(t.tool_name)?[{type:t.tool_name==="TaskUpdate"?"task_update":t.tool_name==="TaskCreate"?"task_create":"task",category:"task",data:s(JSON.stringify(t.tool_input)),priority:1}]:[]}function E(t){if(t.tool_name==="EnterPlanMode")return[{type:"plan_enter",category:"plan",data:"entered plan mode",priority:2}];if(t.tool_name==="ExitPlanMode"){let e=[],o=t.tool_input.allowedPrompts,i=Array.isArray(o)&&o.length>0?`exited plan mode (allowed: ${f(o.map(n=>typeof n=="object"&&n!==null&&"prompt"in n?String(n.prompt):String(n)).join(", "))})`:"exited plan mode";e.push({type:"plan_exit",category:"plan",data:s(i),priority:2});let r=String(t.tool_response??"").toLowerCase();return r.includes("approved")||r.includes("approve")?e.push({type:"plan_approved",category:"plan",data:"plan approved by user",priority:1}):(r.includes("rejected")||r.includes("decline")||r.includes("denied"))&&e.push({type:"plan_rejected",category:"plan",data:s(`plan rejected: ${t.tool_response??""}`),priority:2}),e}if(t.tool_name==="Write"||t.tool_name==="Edit"){let e=String(t.tool_input.file_path??"");if(/[/\\]\.claude[/\\]plans[/\\]/.test(e))return[{type:"plan_file_write",category:"plan",data:s(`plan file: ${e.split(/[/\\]/).pop()??e}`),priority:2}]}return[]}var k=[/\bsource\s+\S*activate\b/,/\bexport\s+\w+=/,/\bnvm\s+use\b/,/\bpyenv\s+(shell|local|global)\b/,/\bconda\s+activate\b/,/\brbenv\s+(shell|local|global)\b/,/\bnpm\s+install\b/,/\bnpm\s+ci\b/,/\bpip\s+install\b/,/\bbun\s+install\b/,/\byarn\s+(add|install)\b/,/\bpnpm\s+(add|install)\b/,/\bcargo\s+(install|add)\b/,/\bgo\s+(install|get)\b/,/\brustup\b/,/\basdf\b/,/\bvolta\b/,/\bdeno\s+install\b/];function v(t){if(t.tool_name!=="Bash")return[];let e=String(t.tool_input.command??"");if(!k.some(r=>r.test(e)))return[];let i=e.replace(/\bexport\s+(\w+)=\S*/g,"export $1=***");return[{type:"env",category:"env",data:s(i),priority:2}]}function x(t){if(t.tool_name!=="Skill")return[];let e=String(t.tool_input.skill??"");return[{type:"skill",category:"skill",data:s(e),priority:2}]}function w(t){if(!t.tool_response?.includes("Error")&&!t.tool_output?.isError)return[];let e=String(t.tool_response||""),o=[/not supported/i,/cannot/i,/does not support/i,/FAIL/i,/refused/i,/permission denied/i,/incompatible/i];for(let i of o){let r=e.match(i);if(r){let n=e.toLowerCase().indexOf(r[0].toLowerCase()),a=e.slice(Math.max(0,n-50),Math.min(e.length,n+200)).trim();return[{type:"constraint_discovered",category:"constraint",data:s(a),priority:2}]}}return[]}function R(t){if(t.tool_name!=="Agent")return[];let e=s(String(t.tool_input.prompt??t.tool_input.description??"")),o=t.tool_response?s(String(t.tool_response)):"",i=o.length>0;return[{type:i?"subagent_completed":"subagent_launched",category:"subagent",data:s(i?`[completed] ${e} \u2192 ${o}`:`[launched] ${e}`),priority:i?2:3}]}function T(t){let{tool_name:e,tool_input:o,tool_response:i}=t;if(!e.startsWith("mcp__"))return[];let r=e.split("__"),n=r[r.length-1]||e,a=Object.values(o).find(d=>typeof d=="string"),p=a?`: ${s(String(a))}`:"",u=i&&i.length>0?`
|
|
2
|
+
response: ${s(i)}`:"";return[{type:"mcp",category:"mcp",data:s(`${n}${p}${u}`),priority:3}]}var A=2048;function I(t,e){if(Buffer.byteLength(t,"utf8")<=e)return{value:t,truncated:!1};let o=Buffer.from(t,"utf8"),i=e;for(;i>0&&(o[i]&192)===128;)i--;return{value:o.subarray(0,i).toString("utf8"),truncated:!0}}var $=/(authorization|auth_token|access_token|refresh_token|bearer|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|set-cookie|signature|private[-_]?key|client[-_]?secret|x[-_]?api[-_]?key)/i,H="[REDACTED]";function g(t,e=new WeakSet){if(t==null||typeof t!="object")return t;if(e.has(t))return"[CIRCULAR]";e.add(t);let o;if(Array.isArray(t))o=t.map(i=>g(i,e));else{let i={};for(let[r,n]of Object.entries(t))$.test(r)?i[r]=H:i[r]=g(n,e);o=i}return e.delete(t),o}function N(t){let{tool_name:e,tool_input:o}=t;if(!e.startsWith("mcp__"))return[];let i=g(o??{}),r;try{r=JSON.stringify(i)}catch{r="{}"}let{value:n,truncated:a}=I(r,A),p=a?`{"tool_name":${JSON.stringify(e)},"params_raw":${JSON.stringify(n)},"truncated":true}`:`{"tool_name":${JSON.stringify(e)},"params":${n}}`;return[{type:"mcp_tool_call",category:"mcp_tool_call",data:s(p),priority:4}]}function C(t){if(t.tool_name!=="AskUserQuestion")return[];let e=t.tool_input.questions,o=Array.isArray(e)&&e.length>0?String(e[0].question??""):"",i=s(String(t.tool_response??"")),r=o?`Q: ${s(o)} \u2192 A: ${i}`:`answer: ${i}`;return[{type:"decision_question",category:"decision",data:s(r),priority:2}]}function P(t){if(t.tool_name!=="Agent")return[];if(!t.tool_response||t.tool_response.length===0)return[];let e=t.tool_response.length>500?t.tool_response.slice(0,500):t.tool_response;return[{type:"agent_finding",category:"agent-finding",data:s(e),priority:2}]}function O(t){let e=[f(t.tool_input),s(t.tool_response)].join(" ");if(e.length===0)return[];let o=new Set,i=e.match(/https?:\/\/[^\s)]+/g);if(i)for(let n of i)n=n.replace(/["'})\],;.]+$/,""),/localhost|127\.0\.0\.1/i.test(n)||o.add(n);let r=e.match(/(?<!\w)#(\d+)/g);if(r)for(let n of r)o.add(n);return o.size===0?[]:[{type:"external_ref",category:"external-ref",data:s(Array.from(o).join(", ")),priority:3}]}function B(t){if(t.tool_name!=="EnterWorktree")return[];let e=String(t.tool_input.name??"unnamed");return[{type:"worktree",category:"env",data:s(`entered worktree: ${e}`),priority:2}]}var L=[/\b(don'?t|do not|never|always|instead|rather|prefer)\b/i,/\b(use|switch to|go with|pick|choose)\s+\w+\s+(instead|over|not)\b/i,/\b(no,?\s+(use|do|try|make))\b/i,/\b(hayır|hayir|evet|böyle|boyle|degil|değil|yerine|kullan)\b/i];function j(t){return L.some(o=>o.test(t))?[{type:"decision",category:"decision",data:s(t),priority:2}]:[]}var D=[/\b(act as|you are|behave like|pretend|role of|persona)\b/i,/\b(senior|staff|principal|lead)\s+(engineer|developer|architect)\b/i,/\b(gibi davran|rolünde|olarak çalış)\b/i];function M(t){return D.some(o=>o.test(t))?[{type:"role",category:"role",data:s(t),priority:3}]:[]}var W=[{mode:"investigate",pattern:/\b(why|how does|explain|understand|what is|analyze|debug|look into)\b/i},{mode:"implement",pattern:/\b(create|add|build|implement|write|make|develop|fix)\b/i},{mode:"discuss",pattern:/\b(think about|consider|should we|what if|pros and cons|opinion)\b/i},{mode:"review",pattern:/\b(review|check|audit|verify|test|validate)\b/i}];function F(t){let e=W.find(({pattern:o})=>o.test(t));return e?[{type:"intent",category:"intent",data:s(e.mode),priority:4}]:[]}var U=[/\bblocked on\b/i,/\bwaiting for\b/i,/\bneed\s+\S+\s+before\b/i,/\bcan'?t proceed until\b/i,/\bdepends on\b/i,/\bblocked\b/i,/\bbekliyor\b/i,/\bbekliyorum\b/i],G=[/\bunblocked\b/i,/\bresolved\b/i,/\bgot the\s+\S+/i,/\bis ready now\b/i,/\bcan proceed\b/i];function J(t){let e=[];return G.some(r=>r.test(t))?(e.push({type:"blocker_resolved",category:"blocked-on",data:s(t),priority:2}),e):(U.some(r=>r.test(t))&&e.push({type:"blocker",category:"blocked-on",data:s(t),priority:2}),e)}function K(t){return t.length<=1024?[]:[{type:"data",category:"data",data:s(t),priority:4}]}var c=null;function q(t){let{tool_name:e,tool_response:o,tool_output:i}=t,r=String(o??""),n=i?.isError===!0;if(e==="Bash"&&/exit code [1-9]|error:|Error:|FAIL|failed/i.test(r)||n)return c={tool:e,error:r.slice(0,200),callsSince:0},[];if(!c)return[];if(c.callsSince++,c.callsSince>10)return c=null,[];let p=e===c.tool,u=c.tool==="Read"&&(e==="Edit"||e==="Write");if(p||u){let d={type:"error_resolved",category:"error-resolution",data:s(`Error in ${c.tool}: ${c.error} \u2192 Fixed`),priority:2};return c=null,[d]}return[]}function V(){c=null}var l=[];function z(t){return`${t.length}:${t.slice(0,20)}`}function Q(t){let{tool_name:e,tool_input:o}=t,i=z(JSON.stringify(o).slice(0,200));if(l.push({tool:e,inputHash:i}),l.length>50&&l.splice(0,l.length-50),l.length<3)return[];let r=0;for(let n=l.length-1;n>=0&&(l[n].tool===e&&l[n].inputHash===i);n--)r++;return r>=3?(l.splice(l.length-r),[{type:"retry_detected",category:"iteration-loop",data:s(`${e} called ${r} times with similar input`),priority:2}]):[]}function Y(){l.length=0}function X(t){try{let e=[];return e.push(...b(t)),e.push(...y(t)),e.push(...h(t)),e.push(...m(t)),e.push(...v(t)),e.push(...S(t)),e.push(...E(t)),e.push(...x(t)),e.push(...R(t)),e.push(...T(t)),e.push(...N(t)),e.push(...C(t)),e.push(...w(t)),e.push(...B(t)),e.push(...P(t)),e.push(...O(t)),e.push(...q(t)),e.push(...Q(t)),e}catch{return[]}}function Z(t){try{let e=[];return e.push(...j(t)),e.push(...M(t)),e.push(...F(t)),e.push(...J(t)),e.push(...K(t)),e}catch{return[]}}export{X as extractEvents,Z as extractUserEvents,V as resetErrorResolutionState,Y as resetIterationLoopState};
|
|
@@ -10,37 +10,85 @@
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { mkdirSync } from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
13
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Returns the worktree suffix for session path isolation.
|
|
18
18
|
* Mirrors the logic in src/server.ts — kept in sync manually since
|
|
19
19
|
* hooks run as plain .mjs (no TypeScript build step).
|
|
20
|
+
*
|
|
21
|
+
* Two-level cache:
|
|
22
|
+
* 1. In-process module cache — same hook fire calls this 3× (db,
|
|
23
|
+
* events, cleanup paths) so cache hits 2 of 3 cold within process.
|
|
24
|
+
* 2. Cross-process marker file in tmpdir keyed by sha256(cwd) — every
|
|
25
|
+
* Pre/PostToolUse hook is a fresh node fork; without this each fire
|
|
26
|
+
* pays 12-50ms for `git worktree list` on Linux/macOS, 50-150ms on
|
|
27
|
+
* Windows where fork+exec is heavier.
|
|
28
|
+
*
|
|
29
|
+
* Marker filename uses sha256(cwd) so it is alphanumeric — safe across
|
|
30
|
+
* Windows path/filename rules. tmpdir() resolves correctly on all 3 OS.
|
|
20
31
|
*/
|
|
32
|
+
let _wtCacheInProcess;
|
|
33
|
+
function workTreeMarkerPath(cwd) {
|
|
34
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
35
|
+
return join(tmpdir(), `cm-wt-${hash}.txt`);
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
function getWorktreeSuffix() {
|
|
22
39
|
const envSuffix = process.env.CONTEXT_MODE_SESSION_SUFFIX;
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
const cwd = process.cwd();
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
_wtCacheInProcess &&
|
|
44
|
+
_wtCacheInProcess.cwd === cwd &&
|
|
45
|
+
_wtCacheInProcess.envSuffix === envSuffix
|
|
46
|
+
) {
|
|
47
|
+
return _wtCacheInProcess.suffix;
|
|
25
48
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
|
|
50
|
+
let suffix;
|
|
51
|
+
if (envSuffix !== undefined) {
|
|
52
|
+
suffix = envSuffix ? `__${envSuffix}` : "";
|
|
53
|
+
} else {
|
|
54
|
+
// Try cross-process marker first.
|
|
55
|
+
const markerPath = workTreeMarkerPath(cwd);
|
|
56
|
+
try {
|
|
57
|
+
suffix = readFileSync(markerPath, "utf-8");
|
|
58
|
+
_wtCacheInProcess = { cwd, envSuffix, suffix };
|
|
59
|
+
return suffix;
|
|
60
|
+
} catch {
|
|
61
|
+
// marker missing → compute below
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
suffix = "";
|
|
65
|
+
try {
|
|
66
|
+
const mainWorktree = execFileSync(
|
|
67
|
+
"git",
|
|
68
|
+
["worktree", "list", "--porcelain"],
|
|
69
|
+
{ encoding: "utf-8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] },
|
|
70
|
+
)
|
|
71
|
+
.split(/\r?\n/)
|
|
72
|
+
.find((l) => l.startsWith("worktree "))
|
|
73
|
+
?.replace("worktree ", "")
|
|
74
|
+
?.trim();
|
|
75
|
+
if (mainWorktree && cwd !== mainWorktree) {
|
|
76
|
+
suffix = `__${createHash("sha256").update(cwd).digest("hex").slice(0, 8)}`;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// git not available or not a git repo — no suffix
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Best-effort write so subsequent hook forks short-circuit.
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(markerPath, suffix, "utf-8");
|
|
85
|
+
} catch {
|
|
86
|
+
// tmpdir not writable — degrade gracefully
|
|
39
87
|
}
|
|
40
|
-
} catch {
|
|
41
|
-
// git not available or not a git repo — no suffix
|
|
42
88
|
}
|
|
43
|
-
|
|
89
|
+
|
|
90
|
+
_wtCacheInProcess = { cwd, envSuffix, suffix };
|
|
91
|
+
return suffix;
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
/** Claude Code platform options (default). */
|
|
@@ -74,8 +74,14 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
|
|
|
74
74
|
workspaceRoots: Array.isArray(input.workspace_roots) ? input.workspace_roots : [],
|
|
75
75
|
lastKnownProjectDir,
|
|
76
76
|
});
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// Prefer bulk path (single transaction = single WAL commit). Falls back
|
|
78
|
+
// to per-event insert for older SessionDB instances that lack bulkInsertEvents.
|
|
79
|
+
if (typeof db.bulkInsertEvents === "function") {
|
|
80
|
+
db.bulkInsertEvents(sessionId, events, hookName, attributions);
|
|
81
|
+
} else {
|
|
82
|
+
for (let i = 0; i < events.length; i++) {
|
|
83
|
+
db.insertEvent(sessionId, events[i], hookName, attributions[i]);
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
return attributions;
|
|
81
87
|
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { buildAutoInjection } from "./auto-injection.mjs";
|
|
|
22
22
|
const toolNamer = createToolNamer("claude-code");
|
|
23
23
|
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
24
24
|
import { readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath, resolveConfigDir } from "./session-helpers.mjs";
|
|
25
|
-
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents
|
|
25
|
+
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "./session-directive.mjs";
|
|
26
26
|
import { createSessionLoaders } from "./session-loaders.mjs";
|
|
27
27
|
import { join, dirname } from "node:path";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
@@ -82,7 +82,13 @@ try {
|
|
|
82
82
|
const dbPath = getSessionDBPath();
|
|
83
83
|
const db = new SessionDB({ dbPath });
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// Filter events to the session being resumed. Falling back to
|
|
86
|
+
// getLatestSessionEvents(db) leaks events from any other session whose
|
|
87
|
+
// session_meta.started_at is more recent — observed cross-session bleed
|
|
88
|
+
// when a different worktree session started after this one and before
|
|
89
|
+
// the resume.
|
|
90
|
+
const sessionId = getSessionId(input);
|
|
91
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
86
92
|
if (events.length > 0) {
|
|
87
93
|
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath());
|
|
88
94
|
additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
|
|
@@ -17,7 +17,7 @@ import { createToolNamer } from "../core/tool-naming.mjs";
|
|
|
17
17
|
|
|
18
18
|
const toolNamer = createToolNamer("vscode-copilot");
|
|
19
19
|
const ROUTING_BLOCK = createRoutingBlock(toolNamer);
|
|
20
|
-
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents
|
|
20
|
+
import { writeSessionEventsFile, buildSessionDirective, getSessionEvents } from "../session-directive.mjs";
|
|
21
21
|
import {
|
|
22
22
|
readStdin, parseStdin, getSessionId, getSessionDBPath, getSessionEventsPath, getCleanupFlagPath,
|
|
23
23
|
getProjectDir, VSCODE_OPTS,
|
|
@@ -63,7 +63,12 @@ try {
|
|
|
63
63
|
const dbPath = getSessionDBPath(OPTS);
|
|
64
64
|
const db = new SessionDB({ dbPath });
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// Filter events to the session being resumed. Falling back to
|
|
67
|
+
// getLatestSessionEvents(db) leaks events from any other session whose
|
|
68
|
+
// session_meta.started_at is more recent — observed cross-session bleed
|
|
69
|
+
// when a different session started after this one and before the resume.
|
|
70
|
+
const sessionId = getSessionId(input, OPTS);
|
|
71
|
+
const events = sessionId ? getSessionEvents(db, sessionId) : [];
|
|
67
72
|
if (events.length > 0) {
|
|
68
73
|
const eventMeta = writeSessionEventsFile(events, getSessionEventsPath(OPTS));
|
|
69
74
|
additionalContext += buildSessionDirective("resume", eventMeta, toolNamer);
|
|
@@ -568,6 +568,7 @@ function generateCategoryInsights(c: CategoryAnalyticsData): Insight[] {
|
|
|
568
568
|
function Dashboard() {
|
|
569
569
|
const [data, setData] = useState<AnalyticsData | null>(null);
|
|
570
570
|
const [catData, setCatData] = useState<CategoryAnalyticsData | null>(null);
|
|
571
|
+
const [showAllInsights, setShowAllInsights] = useState(false);
|
|
571
572
|
useEffect(() => {
|
|
572
573
|
api.analytics().then(setData);
|
|
573
574
|
api.categoryAnalytics().then(setCatData).catch(() => {}); // graceful — catData stays null, sections hidden
|
|
@@ -580,7 +581,6 @@ function Dashboard() {
|
|
|
580
581
|
// Sort: critical first, then warning, positive, neutral
|
|
581
582
|
const SEV_ORDER = { critical: 0, warning: 1, positive: 2, neutral: 3 };
|
|
582
583
|
allInsights.sort((a, b) => (SEV_ORDER[a.severity] ?? 4) - (SEV_ORDER[b.severity] ?? 4));
|
|
583
|
-
const [showAllInsights, setShowAllInsights] = useState(false);
|
|
584
584
|
const insights = showAllInsights ? allInsights : allInsights.slice(0, 8);
|
|
585
585
|
|
|
586
586
|
// Compute derived values
|