context-mode 1.0.126 → 1.0.128

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.
@@ -249,39 +249,88 @@ let securityInitFailed = false;
249
249
  /**
250
250
  * @returns {boolean} true if security module loaded successfully.
251
251
  *
252
- * Loud fail: if `build/security.js` is missing or fails to import, log a
253
- * clear stderr warning instead of swallowing the error silently. Without
254
- * this, user-configured `permissions.deny` patterns (#466) become no-ops
255
- * with no indication that policy enforcement is disabled — a fail-open
256
- * security regression.
252
+ * Loud fail: if neither the esbuild bundle nor `build/security.js` is
253
+ * importable, log a clear stderr warning instead of swallowing the error
254
+ * silently. Without this, user-configured `permissions.deny` patterns
255
+ * (#466) become no-ops with no indication that policy enforcement is
256
+ * disabled — a fail-open security regression.
257
+ *
258
+ * ─── Resolution order (#558) ───────────────────────────────────────────
259
+ *
260
+ * 1. `hooks/security.bundle.mjs` — esbuild output, sibling of routing.mjs's
261
+ * parent. Marketplace installs (`git clone` install path) ship this
262
+ * bundle via CI's `git add -f`, so it's the only artifact reliably
263
+ * present across BOTH `npm install` (build/ generated by tsc) AND
264
+ * marketplace install (build/ excluded by .gitignore, never built).
265
+ *
266
+ * 2. `<buildDir>/security.js` — tsc output. Present after `npm run build`.
267
+ * Kept as a fallback so source checkouts that bypass `npm run bundle`
268
+ * still degrade gracefully to the tsc-emitted module.
269
+ *
270
+ * Bundle path is computed from `import.meta.url` (sibling layout:
271
+ * `hooks/core/routing.mjs` → `hooks/security.bundle.mjs`).
272
+ * `CONTEXT_MODE_SECURITY_BUNDLE_PATH` is a test seam — it lets
273
+ * subprocess-based tests stage a bundle in tmpdir without polluting the
274
+ * repo's hooks/ directory.
257
275
  */
258
276
  export async function initSecurity(buildDir) {
259
- try {
260
- const { existsSync } = await import("node:fs");
261
- const { resolve } = await import("node:path");
262
- const { pathToFileURL } = await import("node:url");
263
- const secPath = resolve(buildDir, "security.js");
264
- if (!existsSync(secPath)) {
277
+ const { existsSync } = await import("node:fs");
278
+ const { resolve, dirname } = await import("node:path");
279
+ const { fileURLToPath, pathToFileURL } = await import("node:url");
280
+
281
+ // Default: <hooks/core/ dir>/../security.bundle.mjs → hooks/security.bundle.mjs.
282
+ const defaultBundlePath = resolve(
283
+ dirname(fileURLToPath(import.meta.url)),
284
+ "..",
285
+ "security.bundle.mjs",
286
+ );
287
+ const bundlePath = process.env.CONTEXT_MODE_SECURITY_BUNDLE_PATH || defaultBundlePath;
288
+ const secPath = resolve(buildDir, "security.js");
289
+
290
+ // Bundle-first: marketplace installs ship the bundle, never the build/ dir.
291
+ if (existsSync(bundlePath)) {
292
+ try {
293
+ security = await import(pathToFileURL(bundlePath).href);
294
+ return true;
295
+ } catch (err) {
265
296
  if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
266
297
  process.stderr.write(
267
- `[context-mode] WARNING: ${secPath} not found security deny patterns will NOT be enforced. ` +
268
- `Run \`npm run build\` to generate it. Set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1 to silence.\n`,
298
+ `[context-mode] WARNING: failed to load security bundle (${bundlePath}) — deny patterns NOT enforced: ${err?.message ?? err}\n`,
269
299
  );
270
300
  }
271
301
  securityInitFailed = true;
272
302
  return false;
273
303
  }
274
- security = await import(pathToFileURL(secPath).href);
275
- return true;
276
- } catch (err) {
277
- if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
278
- process.stderr.write(
279
- `[context-mode] WARNING: failed to load security module deny patterns NOT enforced: ${err?.message ?? err}\n`,
280
- );
304
+ }
305
+
306
+ // Fallback: tsc-emitted build/security.js (source checkout + `npm run build`).
307
+ if (existsSync(secPath)) {
308
+ try {
309
+ security = await import(pathToFileURL(secPath).href);
310
+ return true;
311
+ } catch (err) {
312
+ if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
313
+ process.stderr.write(
314
+ `[context-mode] WARNING: failed to load security module — deny patterns NOT enforced: ${err?.message ?? err}\n`,
315
+ );
316
+ }
317
+ securityInitFailed = true;
318
+ return false;
281
319
  }
282
- securityInitFailed = true;
283
- return false;
284
320
  }
321
+
322
+ // Neither artifact present — preserve fail-open with an actionable warning
323
+ // that mentions BOTH paths so users on either install model can self-diagnose.
324
+ if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
325
+ process.stderr.write(
326
+ `[context-mode] WARNING: security module not found — security deny patterns will NOT be enforced.\n` +
327
+ ` Searched: ${bundlePath} (bundle) and ${secPath} (build).\n` +
328
+ ` Marketplace installs ship hooks/security.bundle.mjs via CI; for source checkouts run \`npm run bundle\` (or \`npm run build\`).\n` +
329
+ ` Set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1 to silence.\n`,
330
+ );
331
+ }
332
+ securityInitFailed = true;
333
+ return false;
285
334
  }
286
335
 
287
336
  /** @returns {boolean} true if a previous initSecurity() call failed to load the module. */
@@ -289,6 +338,49 @@ export function isSecurityInitFailed() {
289
338
  return securityInitFailed;
290
339
  }
291
340
 
341
+ /**
342
+ * Build the agent-facing additionalContext block surfacing the security
343
+ * init failure (#558).
344
+ *
345
+ * Pre-558 the only signal of a fail-open security regression was a
346
+ * stderr WARNING line that adapters typically suppress / discard. The
347
+ * user had no in-band signal that `permissions.deny` was no-op'd.
348
+ *
349
+ * Returns a structured XML-ish block when initSecurity() has failed,
350
+ * `null` otherwise. SessionStart hooks append the block to their
351
+ * additionalContext so the agent (and through the agent, the user)
352
+ * sees the warning the next time they view the session — not just in
353
+ * suppressed stderr.
354
+ *
355
+ * The block format intentionally mirrors the `<context_guidance>`
356
+ * shape used elsewhere in routing so existing prompt-template
357
+ * scaffolding picks it up without special-casing.
358
+ */
359
+ export function buildSecurityWarningContext() {
360
+ if (!securityInitFailed) return null;
361
+ return [
362
+ "<context_mode_security_warning>",
363
+ " <severity>HIGH</severity>",
364
+ " <issue>",
365
+ " The context-mode security module failed to load.",
366
+ " User-configured `permissions.deny` patterns are NOT being enforced.",
367
+ " Bash commands and file operations bypass the deny gate (fail-open).",
368
+ " </issue>",
369
+ " <root_cause>",
370
+ " `hooks/security.bundle.mjs` (and `build/security.js`) are absent or unloadable.",
371
+ " Common on marketplace installs where `build/` is gitignored and the",
372
+ " bundle was missing prior to v1.0.127.",
373
+ " </root_cause>",
374
+ " <fix>",
375
+ " Run `npm run bundle` from the context-mode source checkout, OR",
376
+ " upgrade context-mode to v1.0.127+ (which ships hooks/security.bundle.mjs",
377
+ " via CI). To opt in to fail-CLOSED instead, set CONTEXT_MODE_REQUIRE_SECURITY=1.",
378
+ " To silence this warning while you investigate, set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1.",
379
+ " </fix>",
380
+ "</context_mode_security_warning>",
381
+ ].join("\n");
382
+ }
383
+
292
384
  /**
293
385
  * Normalize platform-specific tool names to canonical (Claude Code) names.
294
386
  *
@@ -25,7 +25,7 @@ import { createSessionLoaders } from "../session-loaders.mjs";
25
25
  import { join, dirname } from "node:path";
26
26
  import { readFileSync, unlinkSync } from "node:fs";
27
27
  import { homedir } from "node:os";
28
- import { fileURLToPath, pathToFileURL } from "node:url";
28
+ import { fileURLToPath } from "node:url";
29
29
 
30
30
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
31
31
  const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
@@ -88,11 +88,13 @@ try {
88
88
  const sessionId = getSessionId(input, OPTS);
89
89
  db.ensureSession(sessionId, projectDir);
90
90
 
91
- // Auto-write GEMINI.md on startup if missing or not merged yet
92
- try {
93
- const { GeminiCLIAdapter } = await import(pathToFileURL(join(HOOK_DIR, "..", "..", "build", "adapters", "gemini-cli", "index.js")).href);
94
- new GeminiCLIAdapter().writeRoutingInstructions(projectDir, join(HOOK_DIR, "..", ".."));
95
- } catch { /* best effort don't block session start */ }
91
+ // NOTE (#558): excised the old GEMINI.md auto-write block. It loaded an
92
+ // adapter from build/ (gitignored, missing on marketplace installs) and
93
+ // called a method that was deleted from every adapter in commit 6dae20c.
94
+ // Both layers were silently no-op'd by the surrounding try/catch on every
95
+ // install path for many releases. If routing-instruction auto-write is
96
+ // reintroduced it must come with its own PRD, method spec, and format
97
+ // tests — out of scope for the security regression fix.
96
98
 
97
99
  const ruleFilePaths = [
98
100
  join(homedir(), ".gemini", "GEMINI.md"),
@@ -0,0 +1 @@
1
+ import{readFileSync as S,realpathSync as R}from"node:fs";import{resolve as u}from"node:path";import{resolve as a}from"node:path";import{homedir as d}from"node:os";import{createRequire as C}from"node:module";function v(e=process.env){let s=e.CLAUDE_CONFIG_DIR;return s&&s.trim()!==""?s.startsWith("~")?a(d(),s.replace(/^~[/\\]?/,"")):a(s):a(d(),".claude")}function E(e=process.env){return a(v(e),"settings.json")}function h(e=process.env){let s=[],t=null,n=null;try{let r=C(import.meta.url)("../adapters/detect.js");t=r.detectPlatform(),n=r.getSessionDirSegments}catch{}if(t&&n&&t.platform!=="claude-code"){let o=n(t.platform);o&&o.length>0&&s.push(a(d(),...o,"settings.json"))}let i=E(e);return s.includes(i)||s.push(i),s}function w(e){let s=e.match(/^Bash\((.+)\)$/);return s?s[1]:null}function k(e){let s=e.match(/^(\w+)\((.+)\)$/);return s?{tool:s[1],glob:s[2]}:null}function $(e){return e.replace(/[.*+?^${}()|[\]\\\/\-]/g,"\\$&")}function P(e){return e.replace(/[.+?^${}()|[\]\\\/\-]/g,"\\$&").replace(/\*/g,".*")}function A(e,s=!1){let t,n=e.indexOf(":");if(n!==-1){let i=e.slice(0,n),o=e.slice(n+1),r=$(i),l=P(o);t=`^${r}(\\s${l})?$`}else t=`^${P(e)}$`;return new RegExp(t,s?"i":"")}function G(e,s=!1){let t="",n=0;for(;n<e.length;)e[n]==="*"&&e[n+1]==="*"?n+2<e.length&&e[n+2]==="/"?(t+="(.*/)?",n+=3):(t+=".*",n+=2):e[n]==="*"?(t+="[^/]*",n++):e[n]==="?"?(t+="[^/]",n++):(t+=e[n].replace(/[.+^${}()|[\]\\\/\-]/g,"\\$&"),n++);return new RegExp(`^${t}$`,s?"i":"")}function f(e,s,t=!1){for(let n of s){let i=w(n);if(i&&A(i,t).test(e))return n}return null}function b(e){let s=[],t="",n=!1,i=!1,o=!1;for(let r=0;r<e.length;r++){let l=e[r],c=r>0?e[r-1]:"";l==="'"&&!i&&!o&&c!=="\\"?(n=!n,t+=l):l==='"'&&!n&&!o&&c!=="\\"?(i=!i,t+=l):l==="`"&&!n&&!i&&c!=="\\"?(o=!o,t+=l):!n&&!i&&!o?l===";"?(s.push(t.trim()),t=""):l==="|"&&e[r+1]==="|"||l==="&"&&e[r+1]==="&"?(s.push(t.trim()),t="",r++):l==="|"?(s.push(t.trim()),t=""):t+=l:t+=l}return t.trim()&&s.push(t.trim()),s.filter(r=>r.length>0)}function m(e){let s;try{s=S(e,"utf-8")}catch{return null}let t;try{t=JSON.parse(s)}catch{return null}let n=t?.permissions;if(!n||typeof n!="object")return null;let i=o=>Array.isArray(o)?o.filter(r=>typeof r=="string"&&w(r)!==null):[];return{allow:i(n.allow),deny:i(n.deny),ask:i(n.ask)}}function L(e,s){let t=[];if(e){let i=u(e,".claude","settings.local.json"),o=m(i);o&&t.push(o);let r=u(e,".claude","settings.json"),l=m(r);l&&t.push(l)}let n=s!==void 0?[s]:h();for(let i of n){let o=m(i);o&&t.push(o)}return t}function M(e,s,t){let n=[],i=r=>{let l;try{l=S(r,"utf-8")}catch{return null}let c;try{c=JSON.parse(l)}catch{return null}let g=c?.permissions?.deny;if(!Array.isArray(g))return[];let y=[];for(let x of g){if(typeof x!="string")continue;let p=k(x);p&&p.tool===e&&y.push(p.glob)}return y};if(s){let r=i(u(s,".claude","settings.local.json"));r!==null&&n.push(r);let l=i(u(s,".claude","settings.json"));l!==null&&n.push(l)}let o=t!==void 0?[t]:h();for(let r of o){let l=i(r);l!==null&&n.push(l)}return n}function q(e,s,t=process.platform==="win32"){let n=b(e);for(let i of n)for(let o of s){let r=f(i,o.deny,t);if(r)return{decision:"deny",matchedPattern:r}}for(let i of s){let o=f(e,i.ask,t);if(o)return{decision:"ask",matchedPattern:o};let r=f(e,i.allow,t);if(r)return{decision:"allow",matchedPattern:r}}return{decision:"ask"}}function z(e,s,t=process.platform==="win32"){let n=b(e);for(let i of n)for(let o of s){let r=f(i,o.deny,t);if(r)return{decision:"deny",matchedPattern:r}}return{decision:"allow"}}function I(e,s,t=process.platform==="win32",n){let i=r=>r.replace(/\\/g,"/"),o=new Set;if(o.add(i(e)),n){let r=u(n,e);o.add(i(r));try{o.add(i(R(r)))}catch{}}for(let r of s)for(let l of r){let c=G(i(l),t);for(let g of o)if(c.test(g))return{denied:!0,matchedPattern:l}}return{denied:!1}}var _={python:[/os\.system\(\s*(['"])(.*?)\1\s*\)/g,/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*(['"])(.*?)\1/g],javascript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],typescript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],ruby:[/system\(\s*(['"])(.*?)\1/g,/`(.*?)`/g],go:[/exec\.Command\(\s*(['"`])(.*?)\1/g],php:[/shell_exec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])exec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])system\(\s*(['"`])(.*?)\1/g,/passthru\(\s*(['"`])(.*?)\1/g,/proc_open\(\s*(['"`])(.*?)\1/g],rust:[/Command::new\(\s*(['"`])(.*?)\1/g]};function F(e){let s=[],t=/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*\[([^\]]+)\]/g,n;for(;(n=t.exec(e))!==null;){let o=[...n[1].matchAll(/(['"])(.*?)\1/g)].map(r=>r[2]);o.length>0&&s.push(o.join(" "))}return s}function H(e,s){let t=_[s];if(!t&&s!=="python")return[];let n=[];if(t)for(let i of t){i.lastIndex=0;let o;for(;(o=i.exec(e))!==null;){let r=o[o.length-1];r&&n.push(r)}}return s==="python"&&n.push(...F(e)),n}export{q as evaluateCommand,z as evaluateCommandDenyOnly,I as evaluateFilePath,H as extractShellCommands,G as fileGlobToRegex,A as globToRegex,f as matchesAnyPattern,w as parseBashPattern,k as parseToolPattern,L as readBashPolicies,M as readToolDenyPatterns,b as splitChainedCommands};
@@ -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__"),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 et}from"node:module";import{existsSync as st,unlinkSync as X,renameSync as nt}from"node:fs";import{tmpdir as rt}from"node:os";import{join as it}from"node:path";import{writeFileSync as A,readFileSync as M,unlinkSync as J}from"node:fs";import{tmpdir as Z}from"node:os";var l=class extends Error{pid;dbPath;constructor(t,e){super(`Another context-mode server is already running (PID: ${t}). Stop it before starting a new instance.`),this.name="DatabaseLockedError",this.pid=t,this.dbPath=e}};function tt(n){try{return process.kill(n,0),!0}catch{return!1}}function F(n){return`${n}.lock`}function P(n){let t=Z();return n===t||n.startsWith(t+"/")||n.startsWith(t+"\\")}function k(n){let{dbPath:t}=n;if(P(t))return{skipped:!0};let e=F(t),s=String(process.pid);try{return A(e,s,{flag:"wx"}),{skipped:!1}}catch(c){if(c?.code!=="EEXIST")throw c}let r;try{r=M(e,"utf-8").trim()}catch{try{return A(e,s,{flag:"wx"}),{skipped:!1}}catch{throw new l(0,t)}}let o=Number.parseInt(r,10);if(Number.isFinite(o)&&o>0&&tt(o))throw new l(o,t);A(e,s,{flag:"w"});let a;try{a=Number.parseInt(M(e,"utf-8").trim(),10)}catch{throw new l(0,t)}if(a!==process.pid)throw new l(a,t);return{skipped:!1}}function E(n){let{dbPath:t}=n;if(!P(t))try{J(F(t))}catch{}}var C=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()}},O=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()}},m=null;function ot(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 at(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 ct(){if(!m){let n=et(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;m=function(s,r){let o=new t(s,{readonly:r?.readonly,create:!0}),a=new C(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(at()){let t=null;try{({DatabaseSync:t}=n(["node","sqlite"].join(":")))}catch{t=null}t&&ot(t)?m=function(s,r){let o=new t(s,{readOnly:r?.readonly??!1});return new O(o)}:m=n("better-sqlite3")}else m=n("better-sqlite3")}return m}function B(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}try{n.pragma("locking_mode = EXCLUSIVE")}catch{}}function j(n){if(!st(n))for(let t of["-wal","-shm"])try{X(n+t)}catch{}}function ut(n){for(let t of["","-wal","-shm"])try{X(n+t)}catch{}}function w(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function W(n="context-mode"){return it(rt(),`${n}-${process.pid}.db`)}function dt(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 lt(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 Et(n){let t=Date.now();for(let e of["","-wal","-shm"])try{nt(n+e,`${n}${e}.corrupt-${t}`)}catch{}}var _=Symbol.for("__context_mode_live_dbs_v2__"),D=(()=>{let n=globalThis;return n[_]||(n[_]=new Map,process.on("exit",()=>{for(let[t,e]of n[_])w(t),E({dbPath:e});n[_].clear()})),n[_]})(),f=class{#t;#e;constructor(t){let e=ct();this.#t=t,k({dbPath:t}),j(t);let s;try{s=new e(t,{timeout:3e4}),B(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(lt(o)){Et(t),j(t);try{s=new e(t,{timeout:3e4}),B(s)}catch(a){throw E({dbPath:t}),new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw E({dbPath:t}),r}this.#e=s,D.set(this.#e,t),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){D.delete(this.#e),w(this.#e),E({dbPath:this.#t})}withRetry(t){return dt(t)}cleanup(){D.delete(this.#e),w(this.#e),ut(this.#t),E({dbPath:this.#t})}};import{createHash as y}from"node:crypto";import{execFileSync as mt}from"node:child_process";import{existsSync as S,realpathSync as pt,renameSync as I}from"node:fs";import{join as L}from"node:path";var p;function T(n){let t=n.replace(/\\/g,"/");return/^\/+$/.test(t)?"/":/^[A-Za-z]:\/+$/.test(t)?`${t.slice(0,2)}/`:t.replace(/\/+$/,"")}function H(n){let t=n;try{t=pt.native(n)}catch{}let e=T(t);return process.platform==="win32"||process.platform==="darwin"?e.toLowerCase():e}function q(n,t){return mt("git",["-C",n,...t],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function gt(n){let t=q(n,["rev-parse","--show-toplevel"]);return t.length>0?T(t):null}function _t(n){let t=q(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();return t?T(t):null}function yt(n=process.cwd()){let t=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(p&&p.projectDir===n&&p.envSuffix===t)return p.suffix;let e="";if(t!==void 0)e=t?`__${t}`:"";else try{let s=gt(n),r=_t(n);if(s&&r){let o=H(s),a=H(r);o!==a&&(e=`__${y("sha256").update(o).digest("hex").slice(0,8)}`)}}catch{}return p={projectDir:n,envSuffix:t,suffix:e},e}function xt(){p=void 0}function V(n){return y("sha256").update(T(n)).digest("hex").slice(0,16)}function z(n){let t=T(n),e=process.platform==="darwin"||process.platform==="win32"?t.toLowerCase():t;return y("sha256").update(e).digest("hex").slice(0,16)}function Ut(n){let{projectDir:t,contentDir:e}=n,s=z(t),r=L(e,`${s}.db`);if(S(r))return r;let o=V(t);if(o===s)return r;let a=L(e,`${o}.db`);if(S(a))try{I(a,r);for(let c of["-wal","-shm"])try{I(a+c,r+c)}catch{}}catch{}return r}function Mt(n){return Tt({...n,ext:".db"})}function Tt(n){let{projectDir:t,sessionsDir:e,ext:s}=n,r=n.suffix??yt(t),o=z(t),a=L(e,`${o}${r}${s}`);if(S(a))return a;let c=V(t);if(c===o)return a;let d=L(e,`${c}${r}${s}`);if(S(d))try{I(d,a)}catch{}return a}var $=1e3,Y=5;function b(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"},G=class extends f{constructor(t){super(t?.dbPath??W("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,
@@ -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=y("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),h=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,g=b(o?.bytesAvoided),R=b(o?.bytesReturned),v=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,Y,e.type,a))return;this.stmt(i.getEventCount).get(t).cnt>=$&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,e.type,e.category,e.priority,e.data,c,d,h,g,R,s,a),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>v())}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 h=y("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),g=r?.[u],R=String(g?.projectDir??d.project_dir??"").trim(),v=String(g?.source??d.attribution_source??"unknown"),N=Number(g?.confidence??d.attribution_confidence??0),x=Number.isFinite(N)?Math.max(0,Math.min(1,N)):0,U=o?.[u],K=b(U?.bytesAvoided),Q=b(U?.bytesReturned);return{event:d,dataHash:h,projectDir:R,attributionSource:v,attributionConfidence:x,bytesAvoided:K,bytesReturned:Q}}),c=this.db.transaction(()=>{let d=this.stmt(i.getEventCount).get(t).cnt;for(let u of a)this.stmt(i.checkDuplicate).get(t,Y,u.event.type,u.dataHash)||(d>=$?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{G as SessionDB,xt as _resetWorktreeSuffixCacheForTests,yt as getWorktreeSuffix,z as hashProjectDirCanonical,V as hashProjectDirLegacy,T as normalizeWorktreePath,Ut as resolveContentStorePath,Mt as resolveSessionDbPath,Tt as resolveSessionPath};
@@ -53,6 +53,24 @@ await runHook(async () => {
53
53
 
54
54
  let additionalContext = ROUTING_BLOCK;
55
55
 
56
+ // ─── #558: surface security init failure as agent-facing context ───
57
+ //
58
+ // Pre-558 the only signal of a fail-open security regression was a
59
+ // stderr WARNING line (suppressed/discarded by most adapters). The
60
+ // SessionStart additionalContext block is the in-band channel — the
61
+ // agent reads it, the user sees it. Idempotent by virtue of
62
+ // SessionStart's once-per-session lifecycle.
63
+ try {
64
+ const { initSecurity, isSecurityInitFailed, buildSecurityWarningContext } =
65
+ await import("./core/routing.mjs");
66
+ const { resolve: _resolve } = await import("node:path");
67
+ await initSecurity(_resolve(HOOK_DIR, "..", "build"));
68
+ if (isSecurityInitFailed()) {
69
+ const warning = buildSecurityWarningContext();
70
+ if (warning) additionalContext = warning + "\n\n" + additionalContext;
71
+ }
72
+ } catch { /* security probe is best-effort — never block session start */ }
73
+
56
74
  try {
57
75
  const raw = await readStdin();
58
76
  const input = parseStdin(raw);
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.126",
6
+ "version": "1.0.128",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.126",
3
+ "version": "1.0.128",
4
4
  "type": "module",
5
5
  "description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -85,9 +85,9 @@
85
85
  ],
86
86
  "scripts": {
87
87
  "build": "tsc && node -e \"if(process.platform!=='win32'){require('fs').chmodSync('build/cli.js',0o755)}\" && npm run bundle && npm run assert-bundle && npm run assert-asymmetric-drift",
88
- "assert-bundle": "node scripts/assert-bundle.mjs server.bundle.mjs cli.bundle.mjs hooks/session-extract.bundle.mjs hooks/session-snapshot.bundle.mjs hooks/session-db.bundle.mjs",
88
+ "assert-bundle": "node scripts/assert-bundle.mjs server.bundle.mjs cli.bundle.mjs hooks/session-extract.bundle.mjs hooks/session-snapshot.bundle.mjs hooks/session-db.bundle.mjs hooks/security.bundle.mjs",
89
89
  "assert-asymmetric-drift": "node scripts/assert-asymmetric-drift.mjs",
90
- "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=cli.bundle.mjs --external:better-sqlite3 --minify && esbuild src/session/extract.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-extract.bundle.mjs --minify && esbuild src/session/snapshot.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-snapshot.bundle.mjs --minify && esbuild src/session/db.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-db.bundle.mjs --external:better-sqlite3 --minify",
90
+ "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=cli.bundle.mjs --external:better-sqlite3 --minify && esbuild src/session/extract.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-extract.bundle.mjs --minify && esbuild src/session/snapshot.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-snapshot.bundle.mjs --minify && esbuild src/session/db.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-db.bundle.mjs --external:better-sqlite3 --minify && esbuild src/security.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/security.bundle.mjs --minify",
91
91
  "version-sync": "node scripts/version-sync.mjs",
92
92
  "version": "node scripts/version-sync.mjs && git add package.json .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json .codex-plugin/plugin.json .openclaw-plugin/openclaw.plugin.json .openclaw-plugin/package.json openclaw.plugin.json .pi/extensions/context-mode/package.json",
93
93
  "prepublishOnly": "npm run build",
@@ -29,7 +29,7 @@
29
29
  * developer ran during `pretest`.
30
30
  */
31
31
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
32
- import { join, relative } from "node:path";
32
+ import { join, relative, sep } from "node:path";
33
33
 
34
34
  /**
35
35
  * Walk a directory recursively, returning a flat list of relative file
@@ -102,22 +102,15 @@ export function derivePluginManifest({ pkg, pluginRoot }) {
102
102
  }
103
103
 
104
104
  /**
105
- * REQUIRED_RUNTIME_SIBLINGS — the minimum set of files start.mjs must
106
- * find at boot. These are the files start.mjs actively `import()`s or
107
- * needs to re-symlink against. The check is intentionally narrower
108
- * than the full manifest:
105
+ * LEGACY_FALLBACK — the v1.0.126 hardcoded REQUIRED_RUNTIME_SIBLINGS,
106
+ * preserved verbatim. Forms the union seed for the algorithmic set so
107
+ * the post-558 contract is strictly additive over the pre-558 contract
108
+ * (no required sibling ever silently disappears).
109
109
  *
110
- * - server.bundle.mjs / cli.bundle.mjs are produced by `npm run
111
- * bundle`. Without server.bundle.mjs the server can't start;
112
- * without cli.bundle.mjs `context-mode doctor` can't run.
113
- * - hooks/{5 hook scripts}.mjs are spawned per Claude Code event.
114
- * Missing any one produces a silent hook failure.
115
- *
116
- * Other files in package.json files[] (insight/, configs/, README, …)
117
- * are not boot-critical, so missing them is a "warn"-class issue
118
- * surfaced only via the doctor — never enough to fail-fast at boot.
110
+ * Also acts as a safety net when `package.json` is unreadable — the
111
+ * boot gate stays loud even if the publish manifest is corrupted.
119
112
  */
120
- const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
113
+ const LEGACY_FALLBACK = Object.freeze([
121
114
  "server.bundle.mjs",
122
115
  "cli.bundle.mjs",
123
116
  join("hooks", "pretooluse.mjs"),
@@ -127,6 +120,94 @@ const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
127
120
  join("hooks", "userpromptsubmit.mjs"),
128
121
  ]);
129
122
 
123
+ /**
124
+ * SOFT_FALLBACK_BUNDLES — bundles that already implement
125
+ * bundle-first / build-fallback resolution (via session-loaders.mjs or
126
+ * session-helpers.mjs). Their absence on a published install is
127
+ * gracefully recoverable, so they MUST NOT join the fail-fast boot
128
+ * gate — the gate would refuse to start a working install.
129
+ *
130
+ * The security bundle is intentionally NOT here: its absence creates a
131
+ * silent fail-OPEN regression (#558), so it IS boot-critical.
132
+ */
133
+ const SOFT_FALLBACK_BUNDLES = new Set([
134
+ "hooks/session-extract.bundle.mjs",
135
+ "hooks/session-snapshot.bundle.mjs",
136
+ "hooks/session-db.bundle.mjs",
137
+ "hooks/session-attribution.bundle.mjs",
138
+ ]);
139
+
140
+ /**
141
+ * Algorithmically extract every esbuild output path from
142
+ * `package.json scripts.bundle`. The bundle script is the SINGLE
143
+ * SOURCE OF TRUTH for "what bundles this build produces" — parsing
144
+ * its `--outfile=…` arguments avoids the parallel-list trap that
145
+ * bit Algo-D4 v1.0.126 (the hardcoded REQUIRED list lagged the
146
+ * actual bundle output).
147
+ *
148
+ * Returns POSIX-style relative paths (forward slashes) for stable
149
+ * comparison with SOFT_FALLBACK_BUNDLES. Caller normalizes to
150
+ * `path.join` shape before pluginRoot-relative resolution.
151
+ */
152
+ function extractBundleOutfiles(pkg) {
153
+ const script = pkg?.scripts?.bundle;
154
+ if (typeof script !== "string") return [];
155
+ const out = new Set();
156
+ // Match every `--outfile=<path>` token (path is whitespace-delimited
157
+ // because the script chains commands with `&&`).
158
+ const re = /--outfile=(\S+)/g;
159
+ let m;
160
+ while ((m = re.exec(script)) !== null) {
161
+ out.add(m[1]);
162
+ }
163
+ return [...out];
164
+ }
165
+
166
+ /**
167
+ * Algorithmic — derive the boot-critical sibling set as the union of:
168
+ * 1. LEGACY_FALLBACK (the v1.0.126 contract, preserved verbatim).
169
+ * 2. Every esbuild output path from `package.json scripts.bundle`
170
+ * that is NOT in SOFT_FALLBACK_BUNDLES.
171
+ *
172
+ * Why algorithmic instead of hardcoded:
173
+ *
174
+ * v1.0.126 shipped Algo-D4 with a hardcoded REQUIRED_RUNTIME_SIBLINGS
175
+ * array that omitted `hooks/security.bundle.mjs` (the bundle didn't
176
+ * ship until v1.0.127). The hardcoded list would need manual
177
+ * extension every time a runtime bundle is added — the same trap
178
+ * would re-bite the next bundle. Deriving from `scripts.bundle`
179
+ * closes the trap: any new bundle output is auto-gated unless it
180
+ * joins the soft-fallback whitelist (which is itself an explicit
181
+ * architectural decision, not a maintenance burden). (#558)
182
+ *
183
+ * Returns OS-native-separator relative paths (suitable for
184
+ * `path.join(pluginRoot, …)`).
185
+ *
186
+ * If `package.json` is unreadable, returns LEGACY_FALLBACK as a
187
+ * safety net so the boot gate never goes silent due to a parse
188
+ * error in the publish manifest.
189
+ */
190
+ export function getRequiredRuntimeSiblings(pluginRoot) {
191
+ let pkg;
192
+ try {
193
+ pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf-8"));
194
+ } catch {
195
+ return [...LEGACY_FALLBACK];
196
+ }
197
+ const required = new Set(LEGACY_FALLBACK);
198
+ for (const outfile of extractBundleOutfiles(pkg)) {
199
+ // Normalize to POSIX for soft-fallback membership check —
200
+ // scripts.bundle is hand-authored with forward slashes already,
201
+ // but be defensive in case a Windows-authored package.json ever
202
+ // reaches us.
203
+ const posix = outfile.split(sep).join("/");
204
+ if (SOFT_FALLBACK_BUNDLES.has(posix)) continue;
205
+ // Convert back to OS-native sep for downstream filesystem ops.
206
+ required.add(posix.split("/").join(sep));
207
+ }
208
+ return [...required];
209
+ }
210
+
130
211
  /**
131
212
  * Verify boot-critical siblings exist at pluginRoot.
132
213
  *
@@ -134,15 +215,14 @@ const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
134
215
  * stderr. The caller (start.mjs at boot, src/cli.ts at doctor) decides
135
216
  * the failure surface (fail-fast exit 2 vs. doctor diagnostic).
136
217
  *
137
- * Uses package.json (read from pluginRoot) only as a source-of-truth
138
- * cross-check; the actual REQUIRED list is hardcoded above to keep the
139
- * runtime contract independent of package.json being readable. If
140
- * package.json IS readable AND files[] omits something we require, the
141
- * check fails — that's the "drift between contract and tarball" trap.
218
+ * Required-set is computed by `getRequiredRuntimeSiblings()`
219
+ * algorithmically derived from `package.json files[]` filtered to the
220
+ * RUNTIME_CRITICAL_PATTERN. Drift between publish manifest and runtime
221
+ * contract becomes architecturally impossible (#558).
142
222
  */
143
223
  export function assertPluginCacheIntegrity({ pluginRoot }) {
144
224
  const missing = [];
145
- for (const rel of REQUIRED_RUNTIME_SIBLINGS) {
225
+ for (const rel of getRequiredRuntimeSiblings(pluginRoot)) {
146
226
  const abs = join(pluginRoot, rel);
147
227
  if (!existsSync(abs)) missing.push(abs);
148
228
  }