context-mode 1.0.107 → 1.0.109

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 (48) 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 +22 -18
  6. package/build/adapters/claude-code/index.js +26 -9
  7. package/build/adapters/opencode/index.js +5 -5
  8. package/build/cli.js +92 -12
  9. package/build/server.js +45 -3
  10. package/build/session/analytics.d.ts +7 -0
  11. package/build/session/analytics.js +75 -15
  12. package/build/session/db.d.ts +3 -1
  13. package/build/session/persist-tool-calls.d.ts +54 -0
  14. package/build/session/persist-tool-calls.js +105 -0
  15. package/build/session/project-attribution.d.ts +1 -1
  16. package/cli.bundle.mjs +123 -122
  17. package/hooks/ensure-deps.mjs +28 -12
  18. package/hooks/posttooluse.mjs +90 -80
  19. package/hooks/precompact.mjs +56 -46
  20. package/hooks/pretooluse.mjs +161 -167
  21. package/hooks/routing-block.mjs +2 -2
  22. package/hooks/run-hook.mjs +82 -0
  23. package/hooks/session-db.bundle.mjs +2 -2
  24. package/hooks/sessionstart.mjs +187 -155
  25. package/hooks/userpromptsubmit.mjs +69 -58
  26. package/openclaw.plugin.json +1 -1
  27. package/package.json +2 -1
  28. package/scripts/heal-better-sqlite3.mjs +108 -0
  29. package/scripts/postinstall.mjs +27 -0
  30. package/server.bundle.mjs +88 -88
  31. package/skills/UPSTREAM-CREDITS.md +51 -0
  32. package/skills/context-mode-ops/SKILL.md +147 -0
  33. package/skills/diagnose/SKILL.md +122 -0
  34. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  35. package/skills/grill-me/SKILL.md +15 -0
  36. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  37. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  38. package/skills/grill-with-docs/SKILL.md +93 -0
  39. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  40. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  41. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  42. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  43. package/skills/tdd/SKILL.md +114 -0
  44. package/skills/tdd/deep-modules.md +33 -0
  45. package/skills/tdd/interface-design.md +31 -0
  46. package/skills/tdd/mocking.md +59 -0
  47. package/skills/tdd/refactoring.md +10 -0
  48. package/skills/tdd/tests.md +61 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./suppress-stderr.mjs";
3
2
  /**
4
3
  * Unified PreToolUse hook for context-mode (Claude Code)
5
4
  * Redirects data-fetching tools to context-mode MCP tools
@@ -9,190 +8,185 @@ import "./suppress-stderr.mjs";
9
8
  * Routing is delegated to core/routing.mjs (shared across platforms).
10
9
  * This file retains the Claude Code-specific self-heal block and
11
10
  * uses core/formatters.mjs for Claude Code output format.
11
+ *
12
+ * Crash-resilience: wrapped via runHook (#414) — module loads happen
13
+ * dynamically inside the wrapper.
14
+ *
15
+ * #415: the destructive settings.json mutation block (which removed
16
+ * context-mode hook entries when hooks.json was present) was deleted.
17
+ * It deleted user-written hook configs without consent and was the
18
+ * documented cause of the regression.
12
19
  */
13
20
 
14
- import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, copyFileSync, readdirSync } from "node:fs";
15
- import { resolve, dirname, basename } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
- import { homedir, tmpdir } from "node:os";
18
- import { readStdin } from "./core/stdin.mjs";
19
- import { routePreToolUse, initSecurity } from "./core/routing.mjs";
20
- import { formatDecision } from "./core/formatters.mjs";
21
- import { parseStdin, getSessionId, resolveConfigDir } from "./session-helpers.mjs";
22
-
23
- // ─── Manual recursive copy (avoids cpSync libuv crash on non-ASCII paths, Windows + Node 24) ───
24
- function copyDirSync(src, dest) {
25
- mkdirSync(dest, { recursive: true });
26
- for (const entry of readdirSync(src, { withFileTypes: true })) {
27
- const srcPath = resolve(src, entry.name);
28
- const destPath = resolve(dest, entry.name);
29
- if (entry.isDirectory()) copyDirSync(srcPath, destPath);
30
- else copyFileSync(srcPath, destPath);
31
- }
32
- }
33
-
34
- // ─── Self-heal: rename dir to correct version, fix registry + hooks ───
35
- try {
36
- const hookDir = dirname(fileURLToPath(import.meta.url));
37
- const myRoot = resolve(hookDir, "..");
38
- const myPkg = JSON.parse(readFileSync(resolve(myRoot, "package.json"), "utf-8"));
39
- const myVersion = myPkg.version ?? "unknown";
40
- const myDirName = basename(myRoot);
41
- const cacheParent = dirname(myRoot);
42
- const marker = resolve(tmpdir(), `context-mode-healed-${myVersion}`);
43
-
44
- // Only self-heal inside plugin cache dirs — skip in dev/CI environments
45
- const isInPluginCache = myRoot.includes("/plugins/cache/") || myRoot.includes("\\plugins\\cache\\");
46
- if (myVersion !== "unknown" && isInPluginCache && !existsSync(marker)) {
47
- // 1. If dir name doesn't match version (e.g. "0.7.0" but code is "0.9.12"),
48
- // create correct dir, copy files, update registry + hooks
49
- const correctDir = resolve(cacheParent, myVersion);
50
- if (myDirName !== myVersion && !existsSync(correctDir)) {
51
- copyDirSync(myRoot, correctDir);
52
-
53
- // Create start.mjs in new dir if missing
54
- const startMjs = resolve(correctDir, "start.mjs");
55
- if (!existsSync(startMjs)) {
56
- writeFileSync(startMjs, [
57
- '#!/usr/bin/env node',
58
- 'import { existsSync } from "node:fs";',
59
- 'import { dirname, resolve } from "node:path";',
60
- 'import { fileURLToPath } from "node:url";',
61
- 'const __dirname = dirname(fileURLToPath(import.meta.url));',
62
- 'process.chdir(__dirname);',
63
- 'if (!process.env.CLAUDE_PROJECT_DIR) process.env.CLAUDE_PROJECT_DIR = process.cwd();',
64
- 'if (existsSync(resolve(__dirname, "server.bundle.mjs"))) {',
65
- ' await import("./server.bundle.mjs");',
66
- '} else if (existsSync(resolve(__dirname, "build", "server.js"))) {',
67
- ' await import("./build/server.js");',
68
- '}',
69
- ].join("\n"), "utf-8");
70
- }
21
+ import { runHook } from "./run-hook.mjs";
22
+
23
+ await runHook(async () => {
24
+ const { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } = await import("node:fs");
25
+ const { resolve, dirname, basename } = await import("node:path");
26
+ const { fileURLToPath } = await import("node:url");
27
+ const { tmpdir } = await import("node:os");
28
+ const { readStdin } = await import("./core/stdin.mjs");
29
+ const { routePreToolUse, initSecurity } = await import("./core/routing.mjs");
30
+ const { formatDecision } = await import("./core/formatters.mjs");
31
+ const { parseStdin, getSessionId, resolveConfigDir } = await import("./session-helpers.mjs");
32
+
33
+ // ─── Manual recursive copy (avoids cpSync libuv crash on non-ASCII paths, Windows + Node 24) ───
34
+ function copyDirSync(src, dest) {
35
+ mkdirSync(dest, { recursive: true });
36
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
37
+ const srcPath = resolve(src, entry.name);
38
+ const destPath = resolve(dest, entry.name);
39
+ if (entry.isDirectory()) copyDirSync(srcPath, destPath);
40
+ else copyFileSync(srcPath, destPath);
71
41
  }
42
+ }
72
43
 
73
- const targetDir = existsSync(correctDir) ? correctDir : myRoot;
74
-
75
- // 2. Update installed_plugins.json → point to correct version dir
76
- // Skip if not present (e.g. CI / non-Claude-Code environments)
77
- const ipPath = resolve(resolveConfigDir(), "plugins", "installed_plugins.json");
78
- if (existsSync(ipPath)) {
79
- const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
80
- for (const [key, entries] of Object.entries(ip.plugins || {})) {
81
- if (!key.toLowerCase().includes("context-mode")) continue;
82
- for (const entry of entries) {
83
- entry.installPath = targetDir;
84
- entry.version = myVersion;
85
- entry.lastUpdated = new Date().toISOString();
44
+ // ─── Self-heal: rename dir to correct version, fix registry + hooks ───
45
+ try {
46
+ const hookDir = dirname(fileURLToPath(import.meta.url));
47
+ const myRoot = resolve(hookDir, "..");
48
+ const myPkg = JSON.parse(readFileSync(resolve(myRoot, "package.json"), "utf-8"));
49
+ const myVersion = myPkg.version ?? "unknown";
50
+ const myDirName = basename(myRoot);
51
+ const cacheParent = dirname(myRoot);
52
+ const marker = resolve(tmpdir(), `context-mode-healed-${myVersion}`);
53
+
54
+ // Only self-heal inside plugin cache dirs — skip in dev/CI environments
55
+ const isInPluginCache = myRoot.includes("/plugins/cache/") || myRoot.includes("\\plugins\\cache\\");
56
+ if (myVersion !== "unknown" && isInPluginCache && !existsSync(marker)) {
57
+ // 1. If dir name doesn't match version (e.g. "0.7.0" but code is "0.9.12"),
58
+ // create correct dir, copy files, update registry + hooks
59
+ const correctDir = resolve(cacheParent, myVersion);
60
+ if (myDirName !== myVersion && !existsSync(correctDir)) {
61
+ copyDirSync(myRoot, correctDir);
62
+
63
+ // Create start.mjs in new dir if missing
64
+ const startMjs = resolve(correctDir, "start.mjs");
65
+ if (!existsSync(startMjs)) {
66
+ writeFileSync(startMjs, [
67
+ '#!/usr/bin/env node',
68
+ 'import { existsSync } from "node:fs";',
69
+ 'import { dirname, resolve } from "node:path";',
70
+ 'import { fileURLToPath } from "node:url";',
71
+ 'const __dirname = dirname(fileURLToPath(import.meta.url));',
72
+ 'process.chdir(__dirname);',
73
+ 'if (!process.env.CLAUDE_PROJECT_DIR) process.env.CLAUDE_PROJECT_DIR = process.cwd();',
74
+ 'if (existsSync(resolve(__dirname, "server.bundle.mjs"))) {',
75
+ ' await import("./server.bundle.mjs");',
76
+ '} else if (existsSync(resolve(__dirname, "build", "server.js"))) {',
77
+ ' await import("./build/server.js");',
78
+ '}',
79
+ ].join("\n"), "utf-8");
86
80
  }
87
81
  }
88
- writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
89
- }
90
82
 
91
- // 3. Update hook paths + matcher in settings.json for ALL hook types (#187)
92
- // Previously only fixed PreToolUse — SessionStart, PostToolUse, PreCompact,
93
- // UserPromptSubmit paths remained stale after marketplace auto-update.
94
- const settingsPath = resolve(resolveConfigDir(), "settings.json");
95
- try {
96
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
- const allHooks = settings.hooks || {};
98
- let changed = false;
99
-
100
- // If hooks.json is present, the plugin system owns hook registration.
101
- // Remove any settings.json context-mode entries to prevent duplicate concurrent
102
- // hook processes that cause "non-blocking hook error" on every tool call.
103
- const hooksJsonPath = resolve(myRoot, "hooks", "hooks.json");
104
- if (existsSync(hooksJsonPath)) {
105
- for (const hookType of Object.keys(allHooks)) {
106
- const entries = allHooks[hookType];
107
- if (!Array.isArray(entries)) continue;
108
- const filtered = entries.filter(
109
- (entry) =>
110
- !entry.hooks?.some(
111
- (h) => h.command?.includes(".mjs") && h.command?.includes("context-mode"),
112
- ),
113
- );
114
- if (filtered.length !== entries.length) {
115
- allHooks[hookType] = filtered;
116
- changed = true;
83
+ const targetDir = existsSync(correctDir) ? correctDir : myRoot;
84
+
85
+ // 2. Update installed_plugins.json point to correct version dir
86
+ // Skip if not present (e.g. CI / non-Claude-Code environments)
87
+ const ipPath = resolve(resolveConfigDir(), "plugins", "installed_plugins.json");
88
+ if (existsSync(ipPath)) {
89
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
90
+ for (const [key, entries] of Object.entries(ip.plugins || {})) {
91
+ if (!key.toLowerCase().includes("context-mode")) continue;
92
+ for (const entry of entries) {
93
+ entry.installPath = targetDir;
94
+ entry.version = myVersion;
95
+ entry.lastUpdated = new Date().toISOString();
117
96
  }
118
97
  }
119
- } else {
120
- // Legacy: hooks.json absent — rewrite stale paths to current version dir.
121
- for (const hookType of Object.keys(allHooks)) {
122
- const entries = allHooks[hookType];
123
- if (!Array.isArray(entries)) continue;
98
+ writeFileSync(ipPath, JSON.stringify(ip, null, 2) + "\n", "utf-8");
99
+ }
124
100
 
125
- for (const entry of entries) {
126
- // Fix deprecated Task-only matcher (PreToolUse only)
127
- if (hookType === "PreToolUse" && entry.matcher?.includes("Task") && !entry.matcher.includes("Agent")) {
128
- entry.matcher = entry.matcher.replace("Task", "Agent|Task");
129
- changed = true;
130
- }
131
- // Rewrite stale context-mode hook paths to point to current version
132
- for (const h of (entry.hooks || [])) {
133
- if (h.command && h.command.includes(".mjs") && h.command.includes("context-mode") && !h.command.includes(targetDir)) {
134
- // Extract the script filename (e.g., sessionstart.mjs, pretooluse.mjs)
135
- const scriptMatch = h.command.match(/([a-z]+\.mjs)\s*"?\s*$/);
136
- if (scriptMatch) {
137
- h.command = "node " + resolve(targetDir, "hooks", scriptMatch[1]);
138
- changed = true;
101
+ // 3. Legacy: hooks.json absent — rewrite stale paths in settings.json to current version dir.
102
+ // The previous "if hooks.json present, delete settings.json entries" block was REMOVED (#415):
103
+ // it destroyed user-written hook configs without consent. Plugin-system + settings.json
104
+ // coexistence is now Claude Code's responsibility, not ours.
105
+ const settingsPath = resolve(resolveConfigDir(), "settings.json");
106
+ try {
107
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
108
+ const allHooks = settings.hooks || {};
109
+ let changed = false;
110
+
111
+ const hooksJsonPath = resolve(myRoot, "hooks", "hooks.json");
112
+ if (!existsSync(hooksJsonPath)) {
113
+ // Legacy: hooks.json absent rewrite stale paths to current version dir.
114
+ for (const hookType of Object.keys(allHooks)) {
115
+ const entries = allHooks[hookType];
116
+ if (!Array.isArray(entries)) continue;
117
+
118
+ for (const entry of entries) {
119
+ // Fix deprecated Task-only matcher (PreToolUse only)
120
+ if (hookType === "PreToolUse" && entry.matcher?.includes("Task") && !entry.matcher.includes("Agent")) {
121
+ entry.matcher = entry.matcher.replace("Task", "Agent|Task");
122
+ changed = true;
123
+ }
124
+ // Rewrite stale context-mode hook paths to point to current version
125
+ for (const h of (entry.hooks || [])) {
126
+ if (h.command && h.command.includes(".mjs") && h.command.includes("context-mode") && !h.command.includes(targetDir)) {
127
+ // Extract the script filename (e.g., sessionstart.mjs, pretooluse.mjs)
128
+ const scriptMatch = h.command.match(/([a-z]+\.mjs)\s*"?\s*$/);
129
+ if (scriptMatch) {
130
+ h.command = "node " + resolve(targetDir, "hooks", scriptMatch[1]);
131
+ changed = true;
132
+ }
139
133
  }
140
134
  }
141
135
  }
142
136
  }
143
137
  }
144
- }
145
138
 
146
- if (changed) writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
147
- } catch { /* skip settings update */ }
139
+ if (changed) writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
140
+ } catch { /* skip settings update */ }
148
141
 
149
- // Old version dirs are cleaned lazily by sessionstart.mjs (age-gated >1h)
150
- // to avoid breaking active sessions that still reference them (#181).
142
+ // Old version dirs are cleaned lazily by sessionstart.mjs (age-gated >1h)
143
+ // to avoid breaking active sessions that still reference them (#181).
151
144
 
152
- writeFileSync(marker, Date.now().toString(), "utf-8");
153
- }
154
- } catch { /* best effort — don't block hook */ }
155
-
156
- // ─── Init security from compiled build ───
157
- const __hookDir = dirname(fileURLToPath(import.meta.url));
158
- await initSecurity(resolve(__hookDir, "..", "build"));
159
-
160
- // ─── Read stdin ───
161
- const raw = await readStdin();
162
- const input = parseStdin(raw);
163
- const tool = input.tool_name ?? "";
164
- const toolInput = input.tool_input ?? {};
165
-
166
- // ─── Route and format response ───
167
- const decision = routePreToolUse(tool, toolInput, process.env.CLAUDE_PROJECT_DIR, "claude-code", getSessionId(input));
168
- const response = formatDecision("claude-code", decision);
169
-
170
- // ─── Write latency marker for cross-hook timing (Category 27) ───
171
- // Marker writes MUST happen before stdout write — stdout is the last action
172
- // so the process can exit immediately after, avoiding CI test timeouts.
173
- try {
174
- const sessionId = getSessionId(input);
175
- if (tool) {
176
- const markerPath = resolve(tmpdir(), `context-mode-latency-${sessionId}-${tool}.txt`);
177
- writeFileSync(markerPath, String(Date.now()), "utf-8");
178
- }
179
- } catch { /* latency tracking is best-effort — never block hook */ }
145
+ writeFileSync(marker, Date.now().toString(), "utf-8");
146
+ }
147
+ } catch { /* best effort — don't block hook */ }
148
+
149
+ // ─── Init security from compiled build ───
150
+ const __hookDir = dirname(fileURLToPath(import.meta.url));
151
+ await initSecurity(resolve(__hookDir, "..", "build"));
152
+
153
+ // ─── Read stdin ───
154
+ const raw = await readStdin();
155
+ const input = parseStdin(raw);
156
+ const tool = input.tool_name ?? "";
157
+ const toolInput = input.tool_input ?? {};
158
+
159
+ // ─── Route and format response ───
160
+ const decision = routePreToolUse(tool, toolInput, process.env.CLAUDE_PROJECT_DIR, "claude-code", getSessionId(input));
161
+ const response = formatDecision("claude-code", decision);
180
162
 
181
- // ─── Write rejected-approach marker for PostToolUse to pick up ───
182
- // PreToolUse cannot safely load SessionDB (native module loading breaks hook stdout).
183
- // Write a marker file instead; PostToolUse reads it and writes the event.
184
- if (decision && (decision.action === "deny" || decision.action === "modify")) {
163
+ // ─── Write latency marker for cross-hook timing (Category 27) ───
164
+ // Marker writes MUST happen before stdout write stdout is the last action
165
+ // so the process can exit immediately after, avoiding CI test timeouts.
185
166
  try {
186
167
  const sessionId = getSessionId(input);
187
- const reason = decision.action === "deny"
188
- ? (decision.reason || "denied")
189
- : "Redirected to context-mode sandbox";
190
- const markerPath = resolve(tmpdir(), `context-mode-rejected-${sessionId}.txt`);
191
- writeFileSync(markerPath, `${tool}:${reason}`, "utf-8");
192
- } catch { /* best-effort — never block hook */ }
193
- }
194
-
195
- // ─── stdout write is the LAST action process exits immediately after ───
196
- if (response !== null) {
197
- process.stdout.write(JSON.stringify(response) + "\n");
198
- }
168
+ if (tool) {
169
+ const markerPath = resolve(tmpdir(), `context-mode-latency-${sessionId}-${tool}.txt`);
170
+ writeFileSync(markerPath, String(Date.now()), "utf-8");
171
+ }
172
+ } catch { /* latency tracking is best-effort — never block hook */ }
173
+
174
+ // ─── Write rejected-approach marker for PostToolUse to pick up ───
175
+ // PreToolUse cannot safely load SessionDB (native module loading breaks hook stdout).
176
+ // Write a marker file instead; PostToolUse reads it and writes the event.
177
+ if (decision && (decision.action === "deny" || decision.action === "modify")) {
178
+ try {
179
+ const sessionId = getSessionId(input);
180
+ const reason = decision.action === "deny"
181
+ ? (decision.reason || "denied")
182
+ : "Redirected to context-mode sandbox";
183
+ const markerPath = resolve(tmpdir(), `context-mode-rejected-${sessionId}.txt`);
184
+ writeFileSync(markerPath, `${tool}:${reason}`, "utf-8");
185
+ } catch { /* best-effort — never block hook */ }
186
+ }
187
+
188
+ // ─── stdout write is the LAST action — process exits immediately after ───
189
+ if (response !== null) {
190
+ process.stdout.write(JSON.stringify(response) + "\n");
191
+ }
192
+ });
@@ -36,11 +36,11 @@ export function createRoutingBlock(t, options = {}) {
36
36
 
37
37
  <forbidden_actions>
38
38
  - NO Bash for commands producing >20 lines output.
39
- - NO Read for analysis — use execute_file. Read IS correct for files you intend to Edit.
39
+ - NO Read for analysis — use ${t("ctx_execute_file")}. Read IS correct for files you intend to Edit.
40
40
  - NO WebFetch — use ${t("ctx_fetch_and_index")}.
41
41
  - Bash ONLY for git/mkdir/rm/mv/navigation.
42
42
  - NO ${t("ctx_execute")} or ${t("ctx_execute_file")} for file creation/modification.
43
- ctx_execute is for analysis, processing, computation only.
43
+ ${t("ctx_execute")} is for analysis, processing, computation only.
44
44
  </forbidden_actions>
45
45
 
46
46
  <file_writing_policy>
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-hook.mjs — Universal crash-resilient wrapper for context-mode hook entries (#414).
4
+ *
5
+ * Why this exists:
6
+ * - hooks/hooks.json declares commands as `node "${CLAUDE_PLUGIN_ROOT}/hooks/X.mjs"`.
7
+ * On Windows shells (Git Bash, cmd.exe) the placeholder may mangle and resolution
8
+ * can fail with `cjs/loader:1479 MODULE_NOT_FOUND` — silent ghost hooks.
9
+ * - Top-level `import "./suppress-stderr.mjs"` style side-effects throw at
10
+ * parse time. A `try {}` inside the same file CANNOT catch a parse-time
11
+ * import failure, and `process.on('uncaughtException')` is also installed
12
+ * too late. The fix is to dynamic-import the side-effects from inside this
13
+ * wrapper, where the handler is guaranteed to be live.
14
+ *
15
+ * Contract:
16
+ * - logs every failure to ~/.claude/context-mode/hook-errors.log
17
+ * - never propagates a non-zero exit (Claude Code surfaces non-zero as a
18
+ * "non-blocking hook error" on every tool call, which spams the user)
19
+ * - one-liner adoption for new hooks:
20
+ * import { runHook } from "./run-hook.mjs";
21
+ * await runHook(async () => { ...body... });
22
+ */
23
+
24
+ import { homedir } from "node:os";
25
+ import { resolve } from "node:path";
26
+ import { existsSync, mkdirSync, appendFileSync } from "node:fs";
27
+
28
+ function logError(err) {
29
+ try {
30
+ const dir = resolve(homedir(), ".claude", "context-mode");
31
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
32
+ const line = `[${new Date().toISOString()}] pid=${process.pid} ${err?.stack || err?.message || String(err)}\n`;
33
+ appendFileSync(resolve(dir, "hook-errors.log"), line);
34
+ } catch {
35
+ /* never fail logging */
36
+ }
37
+ }
38
+
39
+ // Install process-level safety nets BEFORE any user code runs.
40
+ // Caveat: these only catch failures inside dynamically-loaded modules
41
+ // (which is precisely what runHook does). Static top-level imports in
42
+ // THIS file would still bypass these — keep this file's imports minimal
43
+ // and fail-safe (only node: built-ins above).
44
+ process.on("uncaughtException", (err) => {
45
+ logError(err);
46
+ process.exit(0);
47
+ });
48
+ process.on("unhandledRejection", (err) => {
49
+ logError(err);
50
+ process.exit(0);
51
+ });
52
+
53
+ /**
54
+ * Run a hook handler with full crash-resilience.
55
+ *
56
+ * Order of operations:
57
+ * 1. Dynamic-import suppress-stderr.mjs (best-effort — non-fatal)
58
+ * 2. Dynamic-import ensure-deps.mjs (best-effort — non-fatal)
59
+ * 3. Invoke handler — any throw is logged and we exit 0
60
+ *
61
+ * @param {() => Promise<void> | void} handler
62
+ */
63
+ export async function runHook(handler) {
64
+ try {
65
+ await import("./suppress-stderr.mjs");
66
+ } catch (e) {
67
+ logError(e);
68
+ /* continue — non-fatal */
69
+ }
70
+ try {
71
+ await import("./ensure-deps.mjs");
72
+ } catch (e) {
73
+ logError(e);
74
+ /* continue — handler may still work */
75
+ }
76
+ try {
77
+ await handler();
78
+ } catch (e) {
79
+ logError(e);
80
+ process.exit(0);
81
+ }
82
+ }
@@ -1,4 +1,4 @@
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 s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){let e="",s=null;for(let o=0;o<t.length;o++){let a=t[o];if(s)e+=a,a===s&&(s=null);else if(a==="'"||a==='"')e+=a,s=a;else if(a===";"){let c=e.trim();c&&this.#t.prepare(c).run(),e=""}else e+=a}let r=e.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>{let r=e.get(...s);return r===null?void 0:r},all:(...s)=>e.all(...s),iterate:(...s)=>e.iterate(...s)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},h=class{#t;constructor(t){this.#t=t}pragma(t){let s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){return this.#t.exec(t),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>e.get(...s),all:(...s)=>e.all(...s),iterate:(...s)=>typeof e.iterate=="function"?e.iterate(...s):e.all(...s)[Symbol.iterator]()}}transaction(t){return(...e)=>{this.#t.exec("BEGIN");try{let s=t(...e);return this.#t.exec("COMMIT"),s}catch(s){throw this.#t.exec("ROLLBACK"),s}}}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(s,r){let o=new t(s,{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(s,r){let o=new t(s,{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 e;for(let s=0;s<=t.length;s++)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(e=r instanceof Error?r:new Error(o),s<t.length){let a=t[s],c=Date.now();for(;Date.now()-c<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${e?.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 e of["","-wal","-shm"])try{M(i+e,`${i}${e}.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 e=B();this.#t=t,N(t);let s;try{s=new e(t,{timeout:3e4}),b(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(j(o)){X(t),N(t);try{s=new e(t,{timeout:3e4}),b(s)}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=s,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 e="";if(i!==void 0)e=i?`__${i}`:"";else try{let s=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();s&&t!==s&&(e=`__${f("sha256").update(t).digest("hex").slice(0,8)}`)}catch{}return l={cwd:t,envSuffix:i,suffix:e},e}function J(){l=void 0}var D=1e3,O=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",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",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 e=this.db.pragma("table_xinfo(session_events)").find(s=>s.name==="data_hash");e&&e.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
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 s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){let e="",s=null;for(let o=0;o<t.length;o++){let a=t[o];if(s)e+=a,a===s&&(s=null);else if(a==="'"||a==='"')e+=a,s=a;else if(a===";"){let c=e.trim();c&&this.#t.prepare(c).run(),e=""}else e+=a}let r=e.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>{let r=e.get(...s);return r===null?void 0:r},all:(...s)=>e.all(...s),iterate:(...s)=>e.iterate(...s)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},h=class{#t;constructor(t){this.#t=t}pragma(t){let s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){return this.#t.exec(t),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>e.get(...s),all:(...s)=>e.all(...s),iterate:(...s)=>typeof e.iterate=="function"?e.iterate(...s):e.all(...s)[Symbol.iterator]()}}transaction(t){return(...e)=>{this.#t.exec("BEGIN");try{let s=t(...e);return this.#t.exec("COMMIT"),s}catch(s){throw this.#t.exec("ROLLBACK"),s}}}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(s,r){let o=new t(s,{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(s,r){let o=new t(s,{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 e;for(let s=0;s<=t.length;s++)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(e=r instanceof Error?r:new Error(o),s<t.length){let a=t[s],c=Date.now();for(;Date.now()-c<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${e?.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 e of["","-wal","-shm"])try{M(i+e,`${i}${e}.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 e=B();this.#t=t,N(t);let s;try{s=new e(t,{timeout:3e4}),b(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(j(o)){X(t),N(t);try{s=new e(t,{timeout:3e4}),b(s)}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=s,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 e="";if(i!==void 0)e=i?`__${i}`:"";else try{let s=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();s&&t!==s&&(e=`__${f("sha256").update(t).digest("hex").slice(0,8)}`)}catch{}return l={cwd:t,envSuffix:i,suffix:e},e}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",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",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 e=this.db.pragma("table_xinfo(session_events)").find(s=>s.name==="data_hash");e&&e.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,
@@ -107,4 +107,4 @@ import{createRequire as I}from"node:module";import{existsSync as U,unlinkSync as
107
107
  updated_at = datetime('now')`),t(n.getToolCallTotals,`SELECT COALESCE(SUM(calls), 0) AS calls,
108
108
  COALESCE(SUM(bytes_returned), 0) AS bytes_returned
109
109
  FROM tool_calls WHERE session_id = ?`),t(n.getToolCallByTool,`SELECT tool, calls, bytes_returned
110
- FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`)}insertEvent(t,e,s="PostToolUse",r){let o=f("sha256").update(e.data).digest("hex").slice(0,16).toUpperCase(),a=String(r?.projectDir??e.project_dir??"").trim(),c=String(r?.source??e.attribution_source??"unknown"),u=Number(r?.confidence??e.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,O,e.type,o))return;this.stmt(n.getEventCount).get(t).cnt>=D&&this.stmt(n.evictLowestPriority).run(t),this.stmt(n.insertEvent).run(t,e.type,e.category,e.priority,e.data,a,c,_,s,o),this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>E())}bulkInsertEvents(t,e,s="PostToolUse",r){if(!e||e.length===0)return;if(e.length===1){this.insertEvent(t,e[0],s,r?.[0]);return}let o=e.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,O,u.event.type,u.dataHash)||(c>=D?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,s,u.dataHash));this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>a())}getEvents(t,e){let s=e?.limit??1e3,r=e?.type,o=e?.minPriority;return r&&o!==void 0?this.stmt(n.getEventsByTypeAndPriority).all(t,r,o,s):r?this.stmt(n.getEventsByType).all(t,r,s):o!==void 0?this.stmt(n.getEventsByPriority).all(t,o,s):this.stmt(n.getEvents).all(t,s)}getEventCount(t){return this.stmt(n.getEventCount).get(t).cnt}getLatestAttributedProjectDir(t){return this.stmt(n.getLatestAttributedProject).get(t)?.project_dir||null}searchEvents(t,e,s,r){try{let o=t.replace(/[%_]/g,c=>"\\"+c),a=r??null;return this.stmt(n.searchEvents).all(s,o,o,a,a,e)}catch{return[]}}ensureSession(t,e){this.stmt(n.ensureSession).run(t,e)}getSessionStats(t){return this.stmt(n.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(n.incrementCompactCount).run(t)}upsertResume(t,e,s){this.stmt(n.upsertResume).run(t,e,s??0)}getResume(t){return this.stmt(n.getResume).get(t)??null}markResumeConsumed(t){this.stmt(n.markResumeConsumed).run(t)}claimLatestUnconsumedResume(t){let e=this.stmt(n.claimLatestUnconsumedResume).get(t);return e?{sessionId:e.session_id,snapshot:e.snapshot}:null}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,e,s=0){let r=Number.isFinite(s)&&s>0?Math.round(s):0;try{this.stmt(n.incrementToolCall).run(t,e,r)}catch{}}getToolCallStats(t){try{let e=this.stmt(n.getToolCallTotals).get(t),s=this.stmt(n.getToolCallByTool).all(t),r={};for(let o of s)r[o.tool]={calls:o.calls,bytesReturned:o.bytes_returned};return{totalCalls:e?.calls??0,totalBytesReturned:e?.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 e=`-${t}`,s=this.stmt(n.getOldSessions).all(e);for(let{session_id:r}of s)this.deleteSession(r);return s.length}};export{A as SessionDB,J as _resetWorktreeSuffixCacheForTests,z as getWorktreeSuffix};
110
+ FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`)}insertEvent(t,e,s="PostToolUse",r){let o=f("sha256").update(e.data).digest("hex").slice(0,16).toUpperCase(),a=String(r?.projectDir??e.project_dir??"").trim(),c=String(r?.source??e.attribution_source??"unknown"),u=Number(r?.confidence??e.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,e.type,o))return;this.stmt(n.getEventCount).get(t).cnt>=O&&this.stmt(n.evictLowestPriority).run(t),this.stmt(n.insertEvent).run(t,e.type,e.category,e.priority,e.data,a,c,_,s,o),this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>E())}bulkInsertEvents(t,e,s="PostToolUse",r){if(!e||e.length===0)return;if(e.length===1){this.insertEvent(t,e[0],s,r?.[0]);return}let o=e.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,s,u.dataHash));this.stmt(n.updateMetaLastEvent).run(t)});this.withRetry(()=>a())}getEvents(t,e){let s=e?.limit??1e3,r=e?.type,o=e?.minPriority;return r&&o!==void 0?this.stmt(n.getEventsByTypeAndPriority).all(t,r,o,s):r?this.stmt(n.getEventsByType).all(t,r,s):o!==void 0?this.stmt(n.getEventsByPriority).all(t,o,s):this.stmt(n.getEvents).all(t,s)}getEventCount(t){return this.stmt(n.getEventCount).get(t).cnt}getLatestAttributedProjectDir(t){return this.stmt(n.getLatestAttributedProject).get(t)?.project_dir||null}searchEvents(t,e,s,r){try{let o=t.replace(/[%_]/g,c=>"\\"+c),a=r??null;return this.stmt(n.searchEvents).all(s,o,o,a,a,e)}catch{return[]}}ensureSession(t,e){this.stmt(n.ensureSession).run(t,e)}getSessionStats(t){return this.stmt(n.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(n.incrementCompactCount).run(t)}upsertResume(t,e,s){this.stmt(n.upsertResume).run(t,e,s??0)}getResume(t){return this.stmt(n.getResume).get(t)??null}markResumeConsumed(t){this.stmt(n.markResumeConsumed).run(t)}claimLatestUnconsumedResume(t){let e=this.stmt(n.claimLatestUnconsumedResume).get(t);return e?{sessionId:e.session_id,snapshot:e.snapshot}:null}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,e,s=0){let r=Number.isFinite(s)&&s>0?Math.round(s):0;try{this.stmt(n.incrementToolCall).run(t,e,r)}catch{}}getToolCallStats(t){try{let e=this.stmt(n.getToolCallTotals).get(t),s=this.stmt(n.getToolCallByTool).all(t),r={};for(let o of s)r[o.tool]={calls:o.calls,bytesReturned:o.bytes_returned};return{totalCalls:e?.calls??0,totalBytesReturned:e?.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 e=`-${t}`,s=this.stmt(n.getOldSessions).all(e);for(let{session_id:r}of s)this.deleteSession(r);return s.length}};export{A as SessionDB,J as _resetWorktreeSuffixCacheForTests,z as getWorktreeSuffix};