context-mode 1.0.124 → 1.0.126

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 (52) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/README.md +3 -3
  6. package/build/adapters/claude-code/hooks.d.ts +22 -17
  7. package/build/adapters/claude-code/hooks.js +33 -24
  8. package/build/adapters/claude-code/index.d.ts +24 -1
  9. package/build/adapters/claude-code/index.js +67 -5
  10. package/build/adapters/codex/hooks.d.ts +13 -14
  11. package/build/adapters/codex/hooks.js +13 -14
  12. package/build/adapters/codex/index.js +19 -8
  13. package/build/adapters/types.d.ts +57 -0
  14. package/build/adapters/types.js +29 -0
  15. package/build/cli.js +38 -13
  16. package/build/db-base.d.ts +19 -2
  17. package/build/db-base.js +49 -15
  18. package/build/executor.js +40 -3
  19. package/build/runtime.d.ts +2 -1
  20. package/build/runtime.js +10 -0
  21. package/build/server.js +4 -2
  22. package/build/util/hook-config.d.ts +24 -1
  23. package/build/util/hook-config.js +39 -2
  24. package/build/util/plugin-cache-integrity.d.ts +37 -0
  25. package/build/util/plugin-cache-integrity.js +105 -0
  26. package/cli.bundle.mjs +141 -138
  27. package/configs/codex/hooks.json +1 -1
  28. package/hooks/core/routing.mjs +8 -4
  29. package/hooks/hooks.json +1 -1
  30. package/hooks/session-db.bundle.mjs +2 -2
  31. package/openclaw.plugin.json +1 -1
  32. package/package.json +2 -1
  33. package/scripts/plugin-cache-integrity.mjs +168 -0
  34. package/server.bundle.mjs +97 -94
  35. package/start.mjs +37 -0
  36. package/skills/UPSTREAM-CREDITS.md +0 -51
  37. package/skills/diagnose/SKILL.md +0 -122
  38. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  39. package/skills/grill-me/SKILL.md +0 -15
  40. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  41. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  42. package/skills/grill-with-docs/SKILL.md +0 -93
  43. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  44. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  45. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  46. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  47. package/skills/tdd/SKILL.md +0 -114
  48. package/skills/tdd/deep-modules.md +0 -33
  49. package/skills/tdd/interface-design.md +0 -31
  50. package/skills/tdd/mocking.md +0 -59
  51. package/skills/tdd/refactoring.md +0 -10
  52. package/skills/tdd/tests.md +0 -61
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PreToolUse": [
4
4
  {
5
- "matcher": "local_shell|shell|shell_command|exec_command|container.exec|functions\\.exec_command|Bash|Shell|apply_patch|functions\\.apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__.*__ctx_execute|mcp__.*__ctx_execute_file|mcp__.*__ctx_batch_execute|mcp__.*__ctx_fetch_and_index|mcp__.*__ctx_search|mcp__.*__ctx_index|mcp__(?!.*context-mode)",
5
+ "matcher": "local_shell|shell|shell_command|exec_command|Bash|Shell|apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__",
6
6
  "hooks": [
7
7
  { "type": "command", "command": "context-mode hook codex pretooluse" }
8
8
  ]
@@ -185,12 +185,16 @@ const SAFE_COMMAND_PATTERNS = [
185
185
  /^cd(?:\s+[^\r\n]+)?$/,
186
186
  /^mkdir(?:\s+[^\r\n]+)?$/,
187
187
  /^touch\s+[^\r\n]+$/,
188
- /^mv(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
189
- /^cp(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
190
- /^rm(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
188
+ // #517 follow-up: the original `(?!\s+-[a-zA-Z]*v\b)` required `v` to be
189
+ // the LAST alpha char in the flag bundle, so `-vs`, `-vfr`, `-rvf`,
190
+ // `-sfvr`, etc. silently slipped past the carve-out and flooded.
191
+ // `(?!\s+-[a-zA-Z]*v[a-zA-Z]*)` catches `v` anywhere in the bundle.
192
+ /^mv(?!\s+-[a-zA-Z]*v[a-zA-Z]*)(?!\s+--verbose\b)\s+[^\r\n]+$/,
193
+ /^cp(?!\s+-[a-zA-Z]*v[a-zA-Z]*)(?!\s+--verbose\b)\s+[^\r\n]+$/,
194
+ /^rm(?!\s+-[a-zA-Z]*v[a-zA-Z]*)(?!\s+--verbose\b)\s+[^\r\n]+$/,
191
195
  // ln (#517): silent on success — same `-v` / `--verbose` carve-out as
192
196
  // cp/mv/rm. Bulk symlink operations with -v flood one line per link.
193
- /^ln(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
197
+ /^ln(?!\s+-[a-zA-Z]*v[a-zA-Z]*)(?!\s+--verbose\b)\s+[^\r\n]+$/,
194
198
  // ls — refuse recursive (-R / --recursive) to keep output bounded.
195
199
  /^ls(?!\s+-[a-zA-Z]*R)(?!\s+--recursive)(?:\s+[^\r\n]+)?$/,
196
200
  // git read-only / status subcommands
package/hooks/hooks.json CHANGED
@@ -97,7 +97,7 @@
97
97
  ]
98
98
  },
99
99
  {
100
- "matcher": "mcp__(?!plugin_context-mode_)",
100
+ "matcher": "mcp__",
101
101
  "hooks": [
102
102
  {
103
103
  "type": "command",
@@ -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 u=e.trim();u&&this.#t.prepare(u).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(){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(process.platform==="linux"){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 J(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],u=Date.now();for(;Date.now()-u<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${e?.message}`)}function tt(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{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=Q();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(tt(o)){et(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 J(t)}cleanup(){v.delete(this.#e),D(this.#e),Z(this.#t)}};import{createHash as p}from"node:crypto";import{execFileSync as st}from"node:child_process";import{existsSync as f,realpathSync as nt,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=nt.native(n)}catch{}let e=g(t);return process.platform==="win32"||process.platform==="darwin"?e.toLowerCase():e}function j(n,t){return st("git",["-C",n,...t],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function rt(n){let t=j(n,["rev-parse","--show-toplevel"]);return t.length>0?g(t):null}function it(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 ot(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=rt(n),r=it(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 yt(){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 ht(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 u of["-wal","-shm"])try{C(a+u,r+u)}catch{}}catch{}return r}function ft(n){return at({...n,ext:".db"})}function at(n){let{projectDir:t,sessionsDir:e,ext:s}=n,r=n.suffix??ot(t),o=W(t),a=b(e,`${o}${r}${s}`);if(f(a))return a;let u=X(t);if(u===o)return a;let d=b(e,`${u}${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()}},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(`
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(),u=String(r?.projectDir??e.project_dir??"").trim(),d=String(r?.source??e.attribution_source??"unknown"),c=Number(r?.confidence??e.attribution_confidence??0),T=Number.isFinite(c)?Math.max(0,Math.min(1,c)):0,m=h(o?.bytesAvoided),L=h(o?.bytesReturned),R=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,u,d,T,m,L,s,a),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>R())}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,c)=>{let T=p("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),m=r?.[c],L=String(m?.projectDir??d.project_dir??"").trim(),R=String(m?.source??d.attribution_source??"unknown"),S=Number(m?.confidence??d.attribution_confidence??0),O=Number.isFinite(S)?Math.max(0,Math.min(1,S)):0,w=o?.[c],H=h(w?.bytesAvoided),$=h(w?.bytesReturned);return{event:d,dataHash:T,projectDir:L,attributionSource:R,attributionConfidence:O,bytesAvoided:H,bytesReturned:$}}),u=this.db.transaction(()=>{let d=this.stmt(i.getEventCount).get(t).cnt;for(let c of a)this.stmt(i.checkDuplicate).get(t,P,c.event.type,c.dataHash)||(d>=B?this.stmt(i.evictLowestPriority).run(t):d++,this.stmt(i.insertEvent).run(t,c.event.type,c.event.category,c.event.priority,c.event.data,c.projectDir,c.attributionSource,c.attributionConfidence,c.bytesAvoided,c.bytesReturned,s,c.dataHash));this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>u())}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,u=>"\\"+u),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,yt as _resetWorktreeSuffixCacheForTests,ot as getWorktreeSuffix,W as hashProjectDirCanonical,X as hashProjectDirLegacy,g as normalizeWorktreePath,ht as resolveContentStorePath,ft as resolveSessionDbPath,at 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??"").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};
@@ -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.124",
6
+ "version": "1.0.126",
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.124",
3
+ "version": "1.0.126",
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",
@@ -79,6 +79,7 @@
79
79
  "scripts/postinstall.mjs",
80
80
  "scripts/heal-better-sqlite3.mjs",
81
81
  "scripts/heal-installed-plugins.mjs",
82
+ "scripts/plugin-cache-integrity.mjs",
82
83
  "README.md",
83
84
  "LICENSE"
84
85
  ],
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Plugin cache integrity check (Algo-D4 + Algo-D5).
3
+ *
4
+ * Algorithmic defense against #550: a partial install (interrupted npm
5
+ * install, broken marketplace pull, half-finished /ctx-upgrade) leaves
6
+ * start.mjs spawnable but a critical sibling (server.bundle.mjs,
7
+ * cli.bundle.mjs, hooks/<event>.mjs, …) missing. The MCP child then
8
+ * dies silently downstream and the user sees an opaque "MCP server
9
+ * failed to start" with no actionable signal.
10
+ *
11
+ * The expected sibling tree is DERIVED from `package.json files[]` —
12
+ * the npm publish source of truth. Adding a new entry there auto-
13
+ * extends the integrity check; no parallel hardcoded list to maintain
14
+ * (the trap that bites every project that hand-rolls "list of files
15
+ * that must exist at runtime").
16
+ *
17
+ * Two consumers:
18
+ * 1. start.mjs at boot — calls assertPluginCacheIntegrity, on !ok
19
+ * writes a structured CONTEXT_MODE_PARTIAL_INSTALL stderr block
20
+ * and exits 2. Fail-fast — the alternative is a downstream stack
21
+ * trace from `import("./server.bundle.mjs")` that hides the
22
+ * actual root cause.
23
+ * 2. src/cli.ts ctx doctor (Algo-D5) — same helper, same answer,
24
+ * surfaced as a HealthCheck so users get the diagnostic without
25
+ * restarting the MCP server.
26
+ *
27
+ * Pure JS, Node.js built-ins only. Ships in package.json files[] so
28
+ * users running off the npm tarball get the same code path the
29
+ * developer ran during `pretest`.
30
+ */
31
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
32
+ import { join, relative } from "node:path";
33
+
34
+ /**
35
+ * Walk a directory recursively, returning a flat list of relative file
36
+ * paths (using `/` as separator inside the returned strings). Skips
37
+ * unreadable entries silently — the integrity check operates on what
38
+ * IS readable; missing entries are reported by the caller.
39
+ */
40
+ function listFilesRecursive(absDir, baseAbs) {
41
+ const out = [];
42
+ let entries;
43
+ try {
44
+ entries = readdirSync(absDir);
45
+ } catch {
46
+ return out; // unreadable — caller will report the parent as missing
47
+ }
48
+ for (const name of entries) {
49
+ const full = join(absDir, name);
50
+ let st;
51
+ try {
52
+ st = statSync(full);
53
+ } catch {
54
+ continue;
55
+ }
56
+ if (st.isDirectory()) {
57
+ out.push(...listFilesRecursive(full, baseAbs));
58
+ } else {
59
+ out.push(relative(baseAbs, full));
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ /**
66
+ * Compute the expected sibling tree for a given pluginRoot, derived
67
+ * from the supplied `package.json files[]` array.
68
+ *
69
+ * Algorithm:
70
+ * - Each entry in files[] is resolved against pluginRoot.
71
+ * - If it points to a directory → list every file inside recursively.
72
+ * - If it points to a file → kept as-is.
73
+ * - Entries that don't exist at probe-time are EXCLUDED from the
74
+ * manifest (they show up as `missing` in the assert step instead).
75
+ * This avoids the trap of "manifest contains paths that have never
76
+ * existed" — the manifest is a snapshot of WHAT IS, not WHAT WAS
77
+ * PUBLISHED.
78
+ *
79
+ * Returns relative paths (relative to pluginRoot). Used by both
80
+ * assertPluginCacheIntegrity and the doctor surface.
81
+ */
82
+ export function derivePluginManifest({ pkg, pluginRoot }) {
83
+ if (!pkg || !Array.isArray(pkg.files)) return [];
84
+ const manifest = new Set();
85
+ for (const entry of pkg.files) {
86
+ if (typeof entry !== "string" || !entry) continue;
87
+ const absEntry = join(pluginRoot, entry);
88
+ if (!existsSync(absEntry)) continue;
89
+ let st;
90
+ try {
91
+ st = statSync(absEntry);
92
+ } catch {
93
+ continue;
94
+ }
95
+ if (st.isDirectory()) {
96
+ for (const f of listFilesRecursive(absEntry, pluginRoot)) manifest.add(f);
97
+ } else {
98
+ manifest.add(entry);
99
+ }
100
+ }
101
+ return [...manifest];
102
+ }
103
+
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:
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.
119
+ */
120
+ const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
121
+ "server.bundle.mjs",
122
+ "cli.bundle.mjs",
123
+ join("hooks", "pretooluse.mjs"),
124
+ join("hooks", "posttooluse.mjs"),
125
+ join("hooks", "precompact.mjs"),
126
+ join("hooks", "sessionstart.mjs"),
127
+ join("hooks", "userpromptsubmit.mjs"),
128
+ ]);
129
+
130
+ /**
131
+ * Verify boot-critical siblings exist at pluginRoot.
132
+ *
133
+ * Returns `{ ok, missing }`. Pure — does NOT touch process.exit or
134
+ * stderr. The caller (start.mjs at boot, src/cli.ts at doctor) decides
135
+ * the failure surface (fail-fast exit 2 vs. doctor diagnostic).
136
+ *
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.
142
+ */
143
+ export function assertPluginCacheIntegrity({ pluginRoot }) {
144
+ const missing = [];
145
+ for (const rel of REQUIRED_RUNTIME_SIBLINGS) {
146
+ const abs = join(pluginRoot, rel);
147
+ if (!existsSync(abs)) missing.push(abs);
148
+ }
149
+ return { ok: missing.length === 0, missing };
150
+ }
151
+
152
+ /**
153
+ * Format the structured stderr block start.mjs emits when integrity
154
+ * fails. Marker line `CONTEXT_MODE_PARTIAL_INSTALL` lets external
155
+ * monitoring grep for the exact failure mode without parsing free-form
156
+ * text. Keep the format stable across versions.
157
+ */
158
+ export function formatPartialInstallReport({ pluginRoot, missing }) {
159
+ const lines = [
160
+ "CONTEXT_MODE_PARTIAL_INSTALL",
161
+ ` pluginRoot: ${pluginRoot}`,
162
+ " missing:",
163
+ ...missing.map((m) => ` - ${m}`),
164
+ " fix: rm -rf the install dir and re-pull (marketplace) or run `npm install -g context-mode` again.",
165
+ "",
166
+ ];
167
+ return lines.join("\n");
168
+ }