context-mode 1.0.135 → 1.0.137

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 (51) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/hooks.json +65 -0
  4. package/.codex-plugin/mcp.json +9 -0
  5. package/.codex-plugin/plugin.json +31 -0
  6. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  7. package/.openclaw-plugin/package.json +1 -1
  8. package/README.md +53 -24
  9. package/build/adapters/codex/index.js +24 -3
  10. package/build/adapters/opencode/index.d.ts +1 -0
  11. package/build/adapters/opencode/index.js +25 -0
  12. package/build/adapters/opencode/plugin.d.ts +22 -0
  13. package/build/adapters/opencode/plugin.js +52 -0
  14. package/build/adapters/pi/extension.js +20 -4
  15. package/build/adapters/pi/mcp-bridge.d.ts +39 -2
  16. package/build/adapters/pi/mcp-bridge.js +184 -24
  17. package/build/lifecycle.d.ts +2 -51
  18. package/build/lifecycle.js +3 -67
  19. package/build/server.d.ts +19 -0
  20. package/build/server.js +141 -58
  21. package/build/session/db.d.ts +6 -0
  22. package/build/session/db.js +17 -3
  23. package/build/session/extract.js +39 -1
  24. package/build/util/sibling-mcp.d.ts +0 -40
  25. package/build/util/sibling-mcp.js +11 -116
  26. package/cli.bundle.mjs +131 -129
  27. package/configs/kilo/kilo.json +3 -7
  28. package/configs/opencode/opencode.json +3 -7
  29. package/hooks/codex/platform.mjs +1 -0
  30. package/hooks/codex/posttooluse.mjs +1 -0
  31. package/hooks/codex/precompact.mjs +1 -0
  32. package/hooks/codex/pretooluse.mjs +1 -0
  33. package/hooks/codex/sessionstart.mjs +1 -0
  34. package/hooks/codex/stop.mjs +1 -0
  35. package/hooks/codex/userpromptsubmit.mjs +1 -0
  36. package/hooks/core/routing.mjs +112 -10
  37. package/hooks/ensure-deps.mjs +14 -3
  38. package/hooks/normalize-hooks.mjs +101 -19
  39. package/hooks/session-db.bundle.mjs +3 -3
  40. package/hooks/session-extract.bundle.mjs +2 -2
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +2 -1
  43. package/server.bundle.mjs +112 -110
  44. package/build/openclaw-plugin.d.ts +0 -130
  45. package/build/openclaw-plugin.js +0 -626
  46. package/build/opencode-plugin.d.ts +0 -122
  47. package/build/opencode-plugin.js +0 -372
  48. package/build/pi-extension.d.ts +0 -14
  49. package/build/pi-extension.js +0 -451
  50. package/build/util/db-lock.d.ts +0 -65
  51. package/build/util/db-lock.js +0 -166
@@ -1,10 +1,6 @@
1
1
  {
2
2
  "$schema": "https://app.kilo.ai/config.json",
3
- "mcp": {
4
- "context-mode": {
5
- "type": "local",
6
- "command": ["context-mode"]
7
- }
8
- },
9
- "plugin": ["context-mode"]
3
+ "plugin": [
4
+ "context-mode"
5
+ ]
10
6
  }
@@ -1,10 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "mcp": {
4
- "context-mode": {
5
- "type": "local",
6
- "command": ["context-mode"]
7
- }
8
- },
9
- "plugin": ["context-mode"]
3
+ "plugin": [
4
+ "context-mode"
5
+ ]
10
6
  }
@@ -0,0 +1 @@
1
+ process.env.CONTEXT_MODE_PLATFORM = "codex";
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  import "../ensure-deps.mjs";
4
5
  /**
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  import "../ensure-deps.mjs";
4
5
  /**
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  /**
4
5
  * Codex CLI preToolUse hook for context-mode.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  import "../ensure-deps.mjs";
4
5
  /**
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  import "../ensure-deps.mjs";
4
5
  /**
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./platform.mjs";
2
3
  import "../suppress-stderr.mjs";
3
4
  import "../ensure-deps.mjs";
4
5
  /**
@@ -17,7 +17,7 @@ import {
17
17
  } from "../routing-block.mjs";
18
18
  import { createToolNamer } from "./tool-naming.mjs";
19
19
  import { isMCPReady } from "./mcp-ready.mjs";
20
- import { existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, unlinkSync, openSync, closeSync, statSync, constants as fsConstants } from "node:fs";
20
+ import { existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, unlinkSync, openSync, closeSync, readFileSync, writeFileSync, statSync, constants as fsConstants } from "node:fs";
21
21
 
22
22
  /**
23
23
  * Guard for actions that redirect to MCP tools (#230).
@@ -29,7 +29,7 @@ function mcpRedirect(result) {
29
29
  if (!isMCPReady()) return null;
30
30
  return result;
31
31
  }
32
- import { tmpdir } from "node:os";
32
+ import { homedir, tmpdir } from "node:os";
33
33
  import { resolve } from "node:path";
34
34
 
35
35
  // Guidance throttle: show each advisory type at most once per session.
@@ -49,6 +49,32 @@ import { resolve } from "node:path";
49
49
  // invocations of the same logical session.
50
50
  const _guidanceShown = new Set();
51
51
 
52
+ // Periodic-guidance counters: how many times each (sessionId, type) pair has
53
+ // fired the periodic branch. Keyed by `${sessionId-or-ppid}::${type}`.
54
+ // File-backed for cross-process so hook invocations from the same logical
55
+ // session keep the counter coherent.
56
+ const _guidanceCounters = new Map();
57
+
58
+ // External-MCP nudge cadence — fire every N matching tool calls.
59
+ // Default 10: keeps the guidance fresh in long MCP-heavy sessions (e.g. a
60
+ // Jira/Slack/Notion run with 50+ tool calls — see #567 follow-up) without
61
+ // flooding context with repeat nudges. Bounds [1, 100]; invalid env values
62
+ // fall back to default. period=1 means "fire every call" (opt-in only).
63
+ const EXTERNAL_MCP_NUDGE_DEFAULT = 10;
64
+ const EXTERNAL_MCP_NUDGE_MIN = 1;
65
+ const EXTERNAL_MCP_NUDGE_MAX = 100;
66
+ const EXTERNAL_MCP_NUDGE_ENV = "CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY";
67
+
68
+ function getExternalMcpNudgeEvery() {
69
+ const raw = process.env[EXTERNAL_MCP_NUDGE_ENV];
70
+ if (raw == null || raw === "") return EXTERNAL_MCP_NUDGE_DEFAULT;
71
+ const parsed = Number.parseInt(raw, 10);
72
+ if (!Number.isFinite(parsed) || parsed < EXTERNAL_MCP_NUDGE_MIN || parsed > EXTERNAL_MCP_NUDGE_MAX) {
73
+ return EXTERNAL_MCP_NUDGE_DEFAULT;
74
+ }
75
+ return parsed;
76
+ }
77
+
52
78
  function defaultGuidanceId() {
53
79
  return process.env.VITEST_WORKER_ID
54
80
  ? `${process.ppid}-w${process.env.VITEST_WORKER_ID}`
@@ -85,6 +111,56 @@ function guidanceOnce(type, content, sessionId) {
85
111
  return { action: "context", additionalContext: content };
86
112
  }
87
113
 
114
+ /**
115
+ * Like guidanceOnce, but fires on a periodic cadence (calls 1, period+1,
116
+ * 2·period+1, …) rather than once per session.
117
+ *
118
+ * Motivation: external-MCP tool runs can span 50+ calls (e.g. a Jira/Slack
119
+ * search loop — see #567 follow-up). A single one-shot nudge gets lost
120
+ * after the model's context compaction kicks in, and subsequent large MCP
121
+ * payloads flood context unchecked. Re-firing the nudge every N calls
122
+ * keeps the guidance in the model's recent window without saturating it.
123
+ *
124
+ * Counter state is process-aware: in-memory Map for same-process callers,
125
+ * file-backed `<guidanceDir>/<type>.count` for cross-process hook
126
+ * invocations. On any IO/parse failure we fall back to firing — losing a
127
+ * counter is preferable to silently dropping the advisory.
128
+ */
129
+ function guidancePeriodic(type, content, sessionId, period) {
130
+ const safePeriod = Math.max(1, period | 0);
131
+ const id = sessionId ? `s-${sessionId}` : defaultGuidanceId();
132
+ const key = `${id}::${type}`;
133
+
134
+ // Read counter from memory first; fall through to disk on miss.
135
+ let count = _guidanceCounters.get(key);
136
+ const dir = guidanceDirFor(sessionId);
137
+ const counterPath = resolve(dir, `${type}.count`);
138
+
139
+ if (count == null) {
140
+ try {
141
+ const parsed = Number.parseInt(readFileSync(counterPath, "utf8"), 10);
142
+ count = Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
143
+ } catch {
144
+ count = 0;
145
+ }
146
+ }
147
+
148
+ const next = count + 1;
149
+ _guidanceCounters.set(key, next);
150
+
151
+ try {
152
+ mkdirSync(dir, { recursive: true });
153
+ writeFileSync(counterPath, String(next), "utf8");
154
+ } catch {
155
+ // Best-effort: cross-process counter may drift on FS failure, but we
156
+ // still return a decision based on the in-memory tick.
157
+ }
158
+
159
+ // Fire on the 1st, (period+1)th, (2·period+1)th… call.
160
+ if ((next - 1) % safePeriod !== 0) return null;
161
+ return { action: "context", additionalContext: content };
162
+ }
163
+
88
164
  /**
89
165
  * Robust recursive delete. On Windows, `fs.rmSync` on directories under a
90
166
  * tmpdir whose path contains non-ASCII characters (e.g. a Chinese / Japanese /
@@ -105,6 +181,7 @@ function rmSyncRobust(dir) {
105
181
 
106
182
  export function resetGuidanceThrottle(sessionId) {
107
183
  _guidanceShown.clear();
184
+ _guidanceCounters.clear();
108
185
  // Clear ppid-based dir (legacy / fallback callers) and the sessionId dir if given
109
186
  rmSyncRobust(guidanceDirFor());
110
187
  if (sessionId) {
@@ -499,6 +576,24 @@ function isExternalMcpTool(toolName) {
499
576
  return false;
500
577
  }
501
578
 
579
+ function getShellCommand(toolInput) {
580
+ if (!toolInput || typeof toolInput !== "object") return "";
581
+ if (typeof toolInput.command === "string") return toolInput.command;
582
+ if (typeof toolInput.cmd === "string") return toolInput.cmd;
583
+ return "";
584
+ }
585
+
586
+ function getCodexConfigDir(env = process.env) {
587
+ const codexHome = env.CODEX_HOME;
588
+ if (codexHome && codexHome.trim() !== "") return resolve(codexHome);
589
+ return resolve(homedir(), ".codex");
590
+ }
591
+
592
+ function getPlatformSettingsPath(platform) {
593
+ if (platform === "codex") return resolve(getCodexConfigDir(), "settings.json");
594
+ return undefined;
595
+ }
596
+
502
597
  /**
503
598
  * Route a PreToolUse event. Returns normalized decision object or null for passthrough.
504
599
  *
@@ -539,17 +634,18 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
539
634
 
540
635
  // Normalize platform-specific tool name to canonical
541
636
  const canonical = TOOL_ALIASES[toolName] ?? toolName;
637
+ const platformSettingsPath = getPlatformSettingsPath(platform);
542
638
 
543
639
  // ─── Bash: Stage 1 security check, then Stage 2 routing ───
544
640
  if (canonical === "Bash") {
545
- const command = toolInput.command ?? "";
641
+ const command = getShellCommand(toolInput);
546
642
 
547
643
  // Stage 1: Security check against user's deny/allow patterns.
548
644
  // Only act when an explicit pattern matched. When no pattern matches,
549
645
  // evaluateCommand returns { decision: "ask" } with no matchedPattern —
550
646
  // in that case fall through so other hooks and the platform's native engine can decide.
551
647
  if (security) {
552
- const policies = security.readBashPolicies(projectDir);
648
+ const policies = security.readBashPolicies(projectDir, platformSettingsPath);
553
649
  if (policies.length > 0) {
554
650
  const result = security.evaluateCommand(command, policies);
555
651
  if (result.decision === "deny") {
@@ -741,7 +837,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
741
837
  if (matchesContextModeTool(toolName, "ctx_execute", "execute")) {
742
838
  if (security && toolInput.language === "shell") {
743
839
  const code = toolInput.code ?? "";
744
- const policies = security.readBashPolicies(projectDir);
840
+ const policies = security.readBashPolicies(projectDir, platformSettingsPath);
745
841
  if (policies.length > 0) {
746
842
  const result = security.evaluateCommand(code, policies);
747
843
  if (result.decision === "deny") {
@@ -760,7 +856,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
760
856
  if (security) {
761
857
  // Check file path against Read deny patterns
762
858
  const filePath = toolInput.path ?? "";
763
- const denyGlobs = security.readToolDenyPatterns("Read", projectDir);
859
+ const denyGlobs = security.readToolDenyPatterns("Read", projectDir, platformSettingsPath);
764
860
  const evalResult = security.evaluateFilePath(filePath, denyGlobs);
765
861
  if (evalResult.denied) {
766
862
  return { action: "deny", reason: `Blocked by security policy: file path matches Read deny pattern ${evalResult.matchedPattern}` };
@@ -770,7 +866,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
770
866
  const lang = toolInput.language ?? "";
771
867
  const code = toolInput.code ?? "";
772
868
  if (lang === "shell") {
773
- const policies = security.readBashPolicies(projectDir);
869
+ const policies = security.readBashPolicies(projectDir, platformSettingsPath);
774
870
  if (policies.length > 0) {
775
871
  const result = security.evaluateCommand(code, policies);
776
872
  if (result.decision === "deny") {
@@ -789,7 +885,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
789
885
  if (matchesContextModeTool(toolName, "ctx_batch_execute", "batch_execute")) {
790
886
  if (security) {
791
887
  const commands = toolInput.commands ?? [];
792
- const policies = security.readBashPolicies(projectDir);
888
+ const policies = security.readBashPolicies(projectDir, platformSettingsPath);
793
889
  if (policies.length > 0) {
794
890
  for (const entry of commands) {
795
891
  const cmd = entry.command ?? "";
@@ -806,14 +902,20 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
806
902
  return null;
807
903
  }
808
904
 
809
- // ─── External MCP tools: one-shot guidance about routing large payloads ─── (#529)
905
+ // ─── External MCP tools: periodic guidance about routing large payloads ─── (#529, #567 follow-up)
810
906
  // hooks/hooks.json registers a `mcp__(?!plugin_context-mode_)` matcher so this
811
907
  // branch fires for slack/telegram/gdrive/notion-style MCPs whose results would
812
908
  // otherwise spill into context. We don't deny or modify — the agent still needs
813
909
  // the tool's output; we just nudge it to pipe large results through ctx_execute.
910
+ //
911
+ // Cadence: every N calls (default 10, tunable via CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY).
912
+ // The original one-shot nudge (#529) was lost after context compaction in
913
+ // MCP-heavy sessions (e.g. 50+ Jira calls in #567 follow-up), letting later
914
+ // payloads flood context unchecked. Re-firing periodically keeps the guidance
915
+ // in the model's recent window without saturating it.
814
916
  if (isExternalMcpTool(toolName)) {
815
917
  const externalMcpGuidance = platform ? createExternalMcpGuidance(t) : EXTERNAL_MCP_GUIDANCE;
816
- return guidanceOnce("external-mcp", externalMcpGuidance, sessionId);
918
+ return guidancePeriodic("external-mcp", externalMcpGuidance, sessionId, getExternalMcpNudgeEvery());
817
919
  }
818
920
 
819
921
  // Unknown tool — pass through
@@ -19,7 +19,7 @@
19
19
  * @see https://github.com/mksglu/context-mode/issues/203
20
20
  */
21
21
 
22
- import { existsSync, copyFileSync } from "node:fs";
22
+ import { existsSync, copyFileSync, renameSync, unlinkSync } from "node:fs";
23
23
  import { execSync } from "node:child_process";
24
24
  import { resolve, dirname } from "node:path";
25
25
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -127,6 +127,18 @@ function probeNativeInProcess(pluginRoot) {
127
127
  }
128
128
  }
129
129
 
130
+ function replaceActiveNativeBinaryFromCache(abiCachePath, binaryPath) {
131
+ const tmpPath = `${binaryPath}.staging-${process.pid}-${Date.now()}`;
132
+ try {
133
+ copyFileSync(abiCachePath, tmpPath);
134
+ codesignBinary(tmpPath);
135
+ renameSync(tmpPath, binaryPath);
136
+ } catch (err) {
137
+ try { unlinkSync(tmpPath); } catch { /* best effort cleanup */ }
138
+ throw err;
139
+ }
140
+ }
141
+
130
142
  export function ensureNativeCompat(pluginRoot) {
131
143
  // Pre-compute paths regardless of runtime — the Bun branch below uses
132
144
  // them to seed the ABI cache (#543) so the next /ctx-upgrade boot (under
@@ -161,8 +173,7 @@ export function ensureNativeCompat(pluginRoot) {
161
173
 
162
174
  // Fast path: cached binary for this ABI already exists — swap in
163
175
  if (existsSync(abiCachePath)) {
164
- copyFileSync(abiCachePath, binaryPath);
165
- codesignBinary(binaryPath);
176
+ replaceActiveNativeBinaryFromCache(abiCachePath, binaryPath);
166
177
  if (skipProbe) return; // Trust the cached binary — skip SIGSEGV-prone probe
167
178
  // Validate via child process — dlopen cache is per-process, so in-process
168
179
  // require() can't detect a swapped binary on disk (#148)
@@ -18,18 +18,70 @@ import { resolve } from "node:path";
18
18
 
19
19
  const PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
20
20
 
21
+ // #604: matches a cache path segment `context-mode/context-mode/<version>`.
22
+ // Capture group is the X.Y.Z version. Used to detect command paths frozen on a
23
+ // previous-version dir that Claude Code's native plugin manager has since
24
+ // cleaned up. `/g` so a single content blob with multiple stale references is
25
+ // fully covered. Forward-slash only — callers convert beforehand.
26
+ const CACHE_VERSION_RE =
27
+ /context-mode\/context-mode\/([0-9]+\.[0-9]+\.[0-9]+)(?=\/)/g;
28
+
21
29
  /** Convert any path string to forward slashes (MSYS-safe). */
22
30
  function fwd(p) {
23
31
  return String(p).replace(/\\/g, "/");
24
32
  }
25
33
 
26
34
  /**
27
- * Pure detection: does this content contain an unresolved CLAUDE_PLUGIN_ROOT
28
- * placeholder that should be normalized?
35
+ * Extract the X.Y.Z version segment from a pluginRoot under the context-mode
36
+ * cache layout. Returns null when running from npm-global, a dev checkout, or
37
+ * any layout that does not match the `<…>/context-mode/context-mode/<v>(/…)?`
38
+ * pattern — callers must treat null as "no stale-path check is possible".
39
+ */
40
+ function pluginRootVersion(pluginRoot) {
41
+ if (!pluginRoot) return null;
42
+ const m =
43
+ /context-mode\/context-mode\/([0-9]+\.[0-9]+\.[0-9]+)(?:\/|$)/.exec(
44
+ fwd(pluginRoot),
45
+ );
46
+ return m ? m[1] : null;
47
+ }
48
+
49
+ /**
50
+ * Does `content` reference any context-mode cache version segment that differs
51
+ * from `currentVersion`? Detects the #604 ratchet: already-normalized hooks.json
52
+ * / plugin.json carrying a previous version's absolute paths forward into a
53
+ * newer version's cache directory after Claude Code's auto-update.
54
+ */
55
+ function hasStaleCacheVersionSegment(content, currentVersion) {
56
+ if (!currentVersion || !content || typeof content !== "string") return false;
57
+ const safe = fwd(content);
58
+ CACHE_VERSION_RE.lastIndex = 0;
59
+ let m;
60
+ while ((m = CACHE_VERSION_RE.exec(safe)) !== null) {
61
+ if (m[1] !== currentVersion) return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Pure detection: does this content need to be (re-)normalized?
68
+ *
69
+ * Two triggers:
70
+ * 1. Fresh content still containing the `${CLAUDE_PLUGIN_ROOT}` placeholder
71
+ * — the original #378 first-boot path on any host.
72
+ * 2. (#604) Already-resolved content whose absolute paths point at a
73
+ * different version of the context-mode cache than the current
74
+ * `pluginRoot`. Breaks the ratchet that previously froze stale paths
75
+ * after Claude Code's native plugin manager copied a previous version's
76
+ * hooks.json forward.
77
+ *
78
+ * `pluginRoot` is optional for backwards compatibility with single-arg
79
+ * callers; without it, only the placeholder check runs.
29
80
  */
30
- export function needsHookNormalization(content) {
81
+ export function needsHookNormalization(content, pluginRoot) {
31
82
  if (!content || typeof content !== "string") return false;
32
- return content.includes(PLACEHOLDER);
83
+ if (content.includes(PLACEHOLDER)) return true;
84
+ return hasStaleCacheVersionSegment(content, pluginRootVersion(pluginRoot));
33
85
  }
34
86
 
35
87
  /**
@@ -41,10 +93,11 @@ export function needsHookNormalization(content) {
41
93
  * Idempotent — leaves already-normalized content unchanged.
42
94
  */
43
95
  export function normalizeHooksJson(content, nodePath, pluginRoot) {
44
- if (!needsHookNormalization(content)) return content;
96
+ if (!needsHookNormalization(content, pluginRoot)) return content;
45
97
 
46
98
  const safeNode = fwd(nodePath);
47
99
  const safeRoot = fwd(pluginRoot);
100
+ const currentVersion = pluginRootVersion(pluginRoot);
48
101
 
49
102
  let parsed;
50
103
  try {
@@ -65,12 +118,30 @@ export function normalizeHooksJson(content, nodePath, pluginRoot) {
65
118
  if (!Array.isArray(inner)) continue;
66
119
  for (const h of inner) {
67
120
  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}" `);
121
+
122
+ const hasPlaceholder = h.command.includes(PLACEHOLDER);
123
+ // #604: also rewrite when the command holds a stale absolute path under
124
+ // a previous-version cache dir (Claude Code's auto-update ratchet).
125
+ const hasStale = hasStaleCacheVersionSegment(h.command, currentVersion);
126
+ if (!hasPlaceholder && !hasStale) continue;
127
+
128
+ let next = h.command;
129
+ if (hasPlaceholder) {
130
+ // Replace placeholder with absolute root (forward-slash).
131
+ next = next.replaceAll(PLACEHOLDER, safeRoot);
132
+ // Replace bare `node ` prefix with quoted execPath. Match both
133
+ // `node ` and `node\t` at start, with optional surrounding whitespace.
134
+ next = next.replace(/^\s*node\s+/, `"${safeNode}" `);
135
+ }
136
+ if (hasStale) {
137
+ // Re-point every `context-mode/context-mode/<old-version>/…` segment
138
+ // to the current pluginRoot's version. Operates on the forward-slash
139
+ // form so MSYS-mangled paths heal as well.
140
+ next = fwd(next).replace(
141
+ CACHE_VERSION_RE,
142
+ `context-mode/context-mode/${currentVersion}`,
143
+ );
144
+ }
74
145
  h.command = next;
75
146
  mutated = true;
76
147
  }
@@ -92,10 +163,11 @@ export function normalizeHooksJson(content, nodePath, pluginRoot) {
92
163
  * Idempotent.
93
164
  */
94
165
  export function normalizePluginJson(content, nodePath, pluginRoot) {
95
- if (!needsHookNormalization(content)) return content;
166
+ if (!needsHookNormalization(content, pluginRoot)) return content;
96
167
 
97
168
  const safeNode = fwd(nodePath);
98
169
  const safeRoot = fwd(pluginRoot);
170
+ const currentVersion = pluginRootVersion(pluginRoot);
99
171
 
100
172
  let parsed;
101
173
  try {
@@ -114,11 +186,21 @@ export function normalizePluginJson(content, nodePath, pluginRoot) {
114
186
 
115
187
  if (Array.isArray(srv.args)) {
116
188
  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
- );
189
+ const after = before.map((a) => {
190
+ if (typeof a !== "string") return a;
191
+ let next = a;
192
+ if (next.includes(PLACEHOLDER)) {
193
+ next = next.replaceAll(PLACEHOLDER, safeRoot);
194
+ }
195
+ // #604: same auto-update ratchet hits plugin.json args (see #523).
196
+ if (hasStaleCacheVersionSegment(next, currentVersion)) {
197
+ next = fwd(next).replace(
198
+ CACHE_VERSION_RE,
199
+ `context-mode/context-mode/${currentVersion}`,
200
+ );
201
+ }
202
+ return next;
203
+ });
122
204
  if (after.some((v, i) => v !== before[i])) {
123
205
  srv.args = after;
124
206
  mutated = true;
@@ -158,7 +240,7 @@ export function normalizeHooksOnStartup({ pluginRoot, nodePath, platform }) {
158
240
  const hooksPath = resolve(pluginRoot, "hooks", "hooks.json");
159
241
  if (existsSync(hooksPath)) {
160
242
  const original = readFileSync(hooksPath, "utf-8");
161
- if (needsHookNormalization(original)) {
243
+ if (needsHookNormalization(original, pluginRoot)) {
162
244
  const next = normalizeHooksJson(original, nodePath, pluginRoot);
163
245
  if (next !== original) {
164
246
  writeFileSync(hooksPath, next, "utf-8");
@@ -174,7 +256,7 @@ export function normalizeHooksOnStartup({ pluginRoot, nodePath, platform }) {
174
256
  const pluginPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
175
257
  if (existsSync(pluginPath)) {
176
258
  const original = readFileSync(pluginPath, "utf-8");
177
- if (needsHookNormalization(original)) {
259
+ if (needsHookNormalization(original, pluginRoot)) {
178
260
  const next = normalizePluginJson(original, nodePath, pluginRoot);
179
261
  if (next !== original) {
180
262
  writeFileSync(pluginPath, next, "utf-8");
@@ -1,4 +1,4 @@
1
- import{createRequire as Y}from"node:module";import{existsSync as G,unlinkSync as x,renameSync as q}from"node:fs";import{tmpdir as z}from"node:os";import{join as K}from"node:path";var N=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()}},A=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()}},l=null;function V(n){let t=null;try{return t=new n(":memory:"),t.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{t?.close()}catch{}}}function Q(n,t){let e=t!==void 0?t:globalThis.Bun;if(typeof e<"u"&&e!==null)return!0;let s=n??process.versions,[r,o]=(s.node??"0.0.0").split("."),a=Number(r),c=Number(o);return!Number.isFinite(a)||!Number.isFinite(c)?!1:a>22||a===22&&c>=5}function J(){if(!l){let n=Y(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;l=function(s,r){let o=new t(s,{readonly:r?.readonly,create:!0}),a=new N(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(Q()){let t=null;try{({DatabaseSync:t}=n(["node","sqlite"].join(":")))}catch{t=null}t&&V(t)?l=function(s,r){let o=new t(s,{readOnly:r?.readonly??!1});return new A(o)}:l=n("better-sqlite3")}else l=n("better-sqlite3")}return l}function I(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function U(n){if(!G(n))for(let t of["-wal","-shm"])try{x(n+t)}catch{}}function Z(n){for(let t of["","-wal","-shm"])try{x(n+t)}catch{}}function D(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function M(n="context-mode"){return K(z(),`${n}-${process.pid}.db`)}function tt(n,t=[100,500,2e3]){let e;for(let s=0;s<=t.length;s++)try{return n()}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 et(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function st(n){let t=Date.now();for(let e of["","-wal","-shm"])try{q(n+e,`${n}${e}.corrupt-${t}`)}catch{}}var _=Symbol.for("__context_mode_live_dbs_v3__"),v=(()=>{let n=globalThis;return n[_]||(n[_]=new Set,process.on("exit",()=>{for(let t of n[_])D(t);n[_].clear()})),n[_]})(),y=class{#t;#e;constructor(t){let e=J();this.#t=t,U(t);let s;try{s=new e(t,{timeout:3e4}),I(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(et(o)){st(t),U(t);try{s=new e(t,{timeout:3e4}),I(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,v.add(this.#e),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){v.delete(this.#e),D(this.#e)}withRetry(t){return tt(t)}cleanup(){v.delete(this.#e),D(this.#e),Z(this.#t)}};import{createHash as p}from"node:crypto";import{execFileSync as nt}from"node:child_process";import{existsSync as f,realpathSync as rt,renameSync as C}from"node:fs";import{join as b}from"node:path";var E;function g(n){let t=n.replace(/\\/g,"/");return/^\/+$/.test(t)?"/":/^[A-Za-z]:\/+$/.test(t)?`${t.slice(0,2)}/`:t.replace(/\/+$/,"")}function F(n){let t=n;try{t=rt.native(n)}catch{}let e=g(t);return process.platform==="win32"||process.platform==="darwin"?e.toLowerCase():e}function j(n,t){return nt("git",["-C",n,...t],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function it(n){let t=j(n,["rev-parse","--show-toplevel"]);return t.length>0?g(t):null}function ot(n){let t=j(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();return t?g(t):null}function at(n=process.cwd()){let t=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(E&&E.projectDir===n&&E.envSuffix===t)return E.suffix;let e="";if(t!==void 0)e=t?`__${t}`:"";else try{let s=it(n),r=ot(n);if(s&&r){let o=F(s),a=F(r);o!==a&&(e=`__${p("sha256").update(o).digest("hex").slice(0,8)}`)}}catch{}return E={projectDir:n,envSuffix:t,suffix:e},e}function ht(){E=void 0}function X(n){return p("sha256").update(g(n)).digest("hex").slice(0,16)}function W(n){let t=g(n),e=process.platform==="darwin"||process.platform==="win32"?t.toLowerCase():t;return p("sha256").update(e).digest("hex").slice(0,16)}function ft(n){let{projectDir:t,contentDir:e}=n,s=W(t),r=b(e,`${s}.db`);if(f(r))return r;let o=X(t);if(o===s)return r;let a=b(e,`${o}.db`);if(f(a))try{C(a,r);for(let c of["-wal","-shm"])try{C(a+c,r+c)}catch{}}catch{}return r}function bt(n){return ct({...n,ext:".db"})}function ct(n){let{projectDir:t,sessionsDir:e,ext:s}=n,r=n.suffix??at(t),o=W(t),a=b(e,`${o}${r}${s}`);if(f(a))return a;let c=X(t);if(c===o)return a;let d=b(e,`${c}${r}${s}`);if(f(d))try{C(d,a)}catch{}return a}var B=1e3,P=5;function h(n){let t=Number(n);return!Number.isFinite(t)||t<=0?0:Math.floor(t)}var i={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",getEventBytesSummary:"getEventBytesSummary"},k=class extends y{constructor(t){super(t?.dbPath??M("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 Y}from"node:module";import{existsSync as G,unlinkSync as x,renameSync as q}from"node:fs";import{tmpdir as z}from"node:os";import{join as K}from"node:path";var N=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()}},D=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()}},l=null;function V(n){let t=null;try{return t=new n(":memory:"),t.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{t?.close()}catch{}}}function Q(n,t){let e=t!==void 0?t:globalThis.Bun;if(typeof e<"u"&&e!==null)return!0;let s=n??process.versions,[r,o]=(s.node??"0.0.0").split("."),a=Number(r),c=Number(o);return!Number.isFinite(a)||!Number.isFinite(c)?!1:a>22||a===22&&c>=5}function J(){if(!l){let n=Y(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;l=function(s,r){let o=new t(s,{readonly:r?.readonly,create:!0}),a=new N(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(Q()){let t=null;try{({DatabaseSync:t}=n(["node","sqlite"].join(":")))}catch{t=null}t&&V(t)?l=function(s,r){let o=new t(s,{readOnly:r?.readonly??!1});return new D(o)}:l=n("better-sqlite3")}else l=n("better-sqlite3")}return l}function U(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function I(n){if(!G(n))for(let t of["-wal","-shm"])try{x(n+t)}catch{}}function Z(n){for(let t of["","-wal","-shm"])try{x(n+t)}catch{}}function A(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function M(n="context-mode"){return K(z(),`${n}-${process.pid}.db`)}function tt(n,t=[100,500,2e3]){let e;for(let s=0;s<=t.length;s++)try{return n()}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 et(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function st(n){let t=Date.now();for(let e of["","-wal","-shm"])try{q(n+e,`${n}${e}.corrupt-${t}`)}catch{}}var _=Symbol.for("__context_mode_live_dbs_v3__"),v=(()=>{let n=globalThis;return n[_]||(n[_]=new Set,process.on("exit",()=>{for(let t of n[_])A(t);n[_].clear()})),n[_]})(),y=class{#t;#e;constructor(t){let e=J();this.#t=t,I(t);let s;try{s=new e(t,{timeout:3e4}),U(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(et(o)){st(t),I(t);try{s=new e(t,{timeout:3e4}),U(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,v.add(this.#e),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){v.delete(this.#e),A(this.#e)}withRetry(t){return tt(t)}cleanup(){v.delete(this.#e),A(this.#e),Z(this.#t)}};import{createHash as p}from"node:crypto";import{execFileSync as nt}from"node:child_process";import{existsSync as f,realpathSync as rt,renameSync as C}from"node:fs";import{join as b}from"node:path";var E;function g(n){let t=n.replace(/\\/g,"/");return/^\/+$/.test(t)?"/":/^[A-Za-z]:\/+$/.test(t)?`${t.slice(0,2)}/`:t.replace(/\/+$/,"")}function F(n){let t=n;try{t=rt.native(n)}catch{}let e=g(t);return process.platform==="win32"||process.platform==="darwin"?e.toLowerCase():e}function k(n,t){return nt("git",["-C",n,...t],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function it(n){let t=k(n,["rev-parse","--show-toplevel"]);return t.length>0?g(t):null}function ot(n){let t=k(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();return t?g(t):null}function at(n=process.cwd()){let t=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(E&&E.projectDir===n&&E.envSuffix===t)return E.suffix;let e="";if(t!==void 0)e=t?`__${t}`:"";else try{let s=it(n),r=ot(n);if(s&&r){let o=F(s),a=F(r);o!==a&&(e=`__${p("sha256").update(o).digest("hex").slice(0,8)}`)}}catch{}return E={projectDir:n,envSuffix:t,suffix:e},e}function ht(){E=void 0}function X(n){return p("sha256").update(g(n)).digest("hex").slice(0,16)}function W(n){let t=g(n),e=process.platform==="darwin"||process.platform==="win32"?t.toLowerCase():t;return p("sha256").update(e).digest("hex").slice(0,16)}function ft(n){let{projectDir:t,contentDir:e}=n,s=W(t),r=b(e,`${s}.db`);if(f(r))return r;let o=X(t);if(o===s)return r;let a=b(e,`${o}.db`);if(f(a))try{C(a,r);for(let c of["-wal","-shm"])try{C(a+c,r+c)}catch{}}catch{}return r}function bt(n){return ct({...n,ext:".db"})}function ct(n){let{projectDir:t,sessionsDir:e,ext:s}=n,r=n.suffix??at(t),o=W(t),a=b(e,`${o}${r}${s}`);if(f(a))return a;let c=X(t);if(c===o)return a;let d=b(e,`${c}${r}${s}`);if(f(d))try{C(d,a)}catch{}return a}var B=1e3,P=5;function h(n){let t=Number(n);return!Number.isFinite(t)||t<=0?0:Math.floor(t)}var i={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",getEventBytesSummary:"getEventBytesSummary"},j=class extends y{constructor(t){super(t?.dbPath??M("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,
@@ -102,7 +102,7 @@ import{createRequire as Y}from"node:module";import{existsSync as G,unlinkSync as
102
102
  )
103
103
  RETURNING session_id, snapshot`),t(i.deleteEvents,"DELETE FROM session_events WHERE session_id = ?"),t(i.deleteMeta,"DELETE FROM session_meta WHERE session_id = ?"),t(i.deleteResume,"DELETE FROM session_resume WHERE session_id = ?"),t(i.searchEvents,`SELECT id, session_id, category, type, data, created_at
104
104
  FROM session_events
105
- WHERE project_dir = ?
105
+ WHERE (project_dir = ? OR project_dir = '')
106
106
  AND (data LIKE '%' || ? || '%' ESCAPE '\\' OR category LIKE '%' || ? || '%' ESCAPE '\\')
107
107
  AND (? IS NULL OR category = ?)
108
108
  ORDER BY id ASC
@@ -116,4 +116,4 @@ import{createRequire as Y}from"node:module";import{existsSync as G,unlinkSync as
116
116
  FROM tool_calls WHERE session_id = ?`),t(i.getToolCallByTool,`SELECT tool, calls, bytes_returned
117
117
  FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),t(i.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
118
118
  COALESCE(SUM(bytes_returned), 0) AS bytes_returned
119
- FROM session_events WHERE session_id = ?`)}insertEvent(t,e,s="PostToolUse",r,o){let a=p("sha256").update(e.data).digest("hex").slice(0,16).toUpperCase(),c=String(r?.projectDir??e.project_dir??"").trim(),d=String(r?.source??e.attribution_source??"unknown"),u=Number(r?.confidence??e.attribution_confidence??0),T=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,m=h(o?.bytesAvoided),L=h(o?.bytesReturned),S=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,P,e.type,a))return;this.stmt(i.getEventCount).get(t).cnt>=B&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,e.type,e.category,e.priority,e.data,c,d,T,m,L,s,a),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>S())}bulkInsertEvents(t,e,s="PostToolUse",r,o){if(!e||e.length===0)return;if(e.length===1){this.insertEvent(t,e[0],s,r?.[0],o?.[0]);return}let a=e.map((d,u)=>{let T=p("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),m=r?.[u],L=String(m?.projectDir??d.project_dir??"").trim(),S=String(m?.source??d.attribution_source??"unknown"),R=Number(m?.confidence??d.attribution_confidence??0),O=Number.isFinite(R)?Math.max(0,Math.min(1,R)):0,w=o?.[u],H=h(w?.bytesAvoided),$=h(w?.bytesReturned);return{event:d,dataHash:T,projectDir:L,attributionSource:S,attributionConfidence:O,bytesAvoided:H,bytesReturned:$}}),c=this.db.transaction(()=>{let d=this.stmt(i.getEventCount).get(t).cnt;for(let u of a)this.stmt(i.checkDuplicate).get(t,P,u.event.type,u.dataHash)||(d>=B?this.stmt(i.evictLowestPriority).run(t):d++,this.stmt(i.insertEvent).run(t,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,s,u.dataHash));this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>c())}getEvents(t,e){let s=e?.limit??1e3,r=e?.type,o=e?.minPriority;return r&&o!==void 0?this.stmt(i.getEventsByTypeAndPriority).all(t,r,o,s):r?this.stmt(i.getEventsByType).all(t,r,s):o!==void 0?this.stmt(i.getEventsByPriority).all(t,o,s):this.stmt(i.getEvents).all(t,s)}getEventCount(t){return this.stmt(i.getEventCount).get(t).cnt}getEventBytesSummary(t){let e=this.stmt(i.getEventBytesSummary).get(t);return{bytesAvoided:Number(e?.bytes_avoided??0),bytesReturned:Number(e?.bytes_returned??0)}}getLatestAttributedProjectDir(t){return this.stmt(i.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(i.searchEvents).all(s,o,o,a,a,e)}catch{return[]}}ensureSession(t,e){this.stmt(i.ensureSession).run(t,e)}getSessionStats(t){return this.stmt(i.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(i.incrementCompactCount).run(t)}upsertResume(t,e,s){this.stmt(i.upsertResume).run(t,e,s??0)}getResume(t){return this.stmt(i.getResume).get(t)??null}markResumeConsumed(t){this.stmt(i.markResumeConsumed).run(t)}claimLatestUnconsumedResume(t){let e=this.stmt(i.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(i.incrementToolCall).run(t,e,r)}catch{}}getToolCallStats(t){try{let e=this.stmt(i.getToolCallTotals).get(t),s=this.stmt(i.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(i.deleteEvents).run(t),this.stmt(i.deleteResume).run(t),this.stmt(i.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let e=`-${t}`,s=this.stmt(i.getOldSessions).all(e);for(let{session_id:r}of s)this.deleteSession(r);return s.length}};export{k as SessionDB,ht as _resetWorktreeSuffixCacheForTests,at as getWorktreeSuffix,W as hashProjectDirCanonical,X as hashProjectDirLegacy,g as normalizeWorktreePath,ft as resolveContentStorePath,bt as resolveSessionDbPath,ct as resolveSessionPath};
119
+ FROM session_events WHERE session_id = ?`)}insertEvent(t,e,s="PostToolUse",r,o){let a=p("sha256").update(e.data).digest("hex").slice(0,16).toUpperCase(),c=String(r?.projectDir??e.project_dir??this._getSessionProjectDir(t)).trim(),d=String(r?.source??e.attribution_source??"unknown"),u=Number(r?.confidence??e.attribution_confidence??0),T=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,m=h(o?.bytesAvoided),S=h(o?.bytesReturned),L=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,P,e.type,a))return;this.stmt(i.getEventCount).get(t).cnt>=B&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,e.type,e.category,e.priority,e.data,c,d,T,m,S,s,a),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>L())}bulkInsertEvents(t,e,s="PostToolUse",r,o){if(!e||e.length===0)return;if(e.length===1){this.insertEvent(t,e[0],s,r?.[0],o?.[0]);return}let a=e.map((d,u)=>{let T=p("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),m=r?.[u],S=String(m?.projectDir??d.project_dir??this._getSessionProjectDir(t)??"").trim(),L=String(m?.source??d.attribution_source??"unknown"),R=Number(m?.confidence??d.attribution_confidence??0),O=Number.isFinite(R)?Math.max(0,Math.min(1,R)):0,w=o?.[u],H=h(w?.bytesAvoided),$=h(w?.bytesReturned);return{event:d,dataHash:T,projectDir:S,attributionSource:L,attributionConfidence:O,bytesAvoided:H,bytesReturned:$}}),c=this.db.transaction(()=>{let d=this.stmt(i.getEventCount).get(t).cnt;for(let u of a)this.stmt(i.checkDuplicate).get(t,P,u.event.type,u.dataHash)||(d>=B?this.stmt(i.evictLowestPriority).run(t):d++,this.stmt(i.insertEvent).run(t,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,s,u.dataHash));this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>c())}getEvents(t,e){let s=e?.limit??1e3,r=e?.type,o=e?.minPriority;return r&&o!==void 0?this.stmt(i.getEventsByTypeAndPriority).all(t,r,o,s):r?this.stmt(i.getEventsByType).all(t,r,s):o!==void 0?this.stmt(i.getEventsByPriority).all(t,o,s):this.stmt(i.getEvents).all(t,s)}getEventCount(t){return this.stmt(i.getEventCount).get(t).cnt}getEventBytesSummary(t){let e=this.stmt(i.getEventBytesSummary).get(t);return{bytesAvoided:Number(e?.bytes_avoided??0),bytesReturned:Number(e?.bytes_returned??0)}}getLatestAttributedProjectDir(t){return this.stmt(i.getLatestAttributedProject).get(t)?.project_dir||null}_getSessionProjectDir(t){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(t)?.project_dir||""}catch{return""}}searchEvents(t,e,s,r){try{let o=t.replace(/[%_]/g,c=>"\\"+c),a=r??null;return this.stmt(i.searchEvents).all(s,o,o,a,a,e)}catch{return[]}}ensureSession(t,e){this.stmt(i.ensureSession).run(t,e)}getSessionStats(t){return this.stmt(i.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(i.incrementCompactCount).run(t)}upsertResume(t,e,s){this.stmt(i.upsertResume).run(t,e,s??0)}getResume(t){return this.stmt(i.getResume).get(t)??null}markResumeConsumed(t){this.stmt(i.markResumeConsumed).run(t)}claimLatestUnconsumedResume(t){let e=this.stmt(i.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(i.incrementToolCall).run(t,e,r)}catch{}}getToolCallStats(t){try{let e=this.stmt(i.getToolCallTotals).get(t),s=this.stmt(i.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(i.deleteEvents).run(t),this.stmt(i.deleteResume).run(t),this.stmt(i.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let e=`-${t}`,s=this.stmt(i.getOldSessions).all(e);for(let{session_id:r}of s)this.deleteSession(r);return s.length}};export{j as SessionDB,ht as _resetWorktreeSuffixCacheForTests,at as getWorktreeSuffix,W as hashProjectDirCanonical,X as hashProjectDirLegacy,g as normalizeWorktreePath,ft as resolveContentStorePath,bt as resolveSessionDbPath,ct as resolveSessionPath};