@mxalbert/context-mode 2.0.1 → 2.0.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "context-mode for Antigravity CLI (agy): sandboxed code execution, FTS5 knowledge base, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "context-mode",
3
3
  "description": "context-mode for GitHub Copilot CLI: sandboxed code execution in 11 languages, an FTS5 knowledge base with BM25 ranking, and session capture. Saves your context window by keeping raw bytes out of the conversation.",
4
- "version": "2.0.1",
4
+ "version": "2.0.3",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "context-window",
@@ -685,6 +685,52 @@ function getPlatformSettingsPath(platform) {
685
685
  return undefined;
686
686
  }
687
687
 
688
+ /**
689
+ * Platforms whose PreToolUse response can surface a real "ask" confirmation
690
+ * prompt to the user (hooks/core/formatters.mjs emits permissionDecision:"ask"
691
+ * or the platform equivalent).
692
+ *
693
+ * Everywhere else: an `ask` match on an ordinary Bash command is NOT returned
694
+ * from routePreToolUse (see the Stage-1 comment inside — the host's own
695
+ * permission system decides), while the ctx_* sandbox branches deny with an
696
+ * actionable reason via sandboxAskDecision — the gemini-cli/codex/kimi/kiro
697
+ * formatters silently DROP ask, which would fail the sandbox open. Keep in
698
+ * sync with the per-platform `ask` formatters; claude-code also covers
699
+ * qwen-code, which shares the claude-code hook script and wire protocol
700
+ * (src/cli.ts HOOK_MAP).
701
+ */
702
+ const ASK_CAPABLE_PLATFORMS = new Set([
703
+ "claude-code", // + qwen-code (shared hook script / wire protocol)
704
+ "vscode-copilot",
705
+ "jetbrains-copilot",
706
+ "copilot-cli",
707
+ "cursor",
708
+ "antigravity-cli",
709
+ ]);
710
+
711
+ /**
712
+ * Resolve an `ask` policy match inside the ctx_* sandbox tools for platforms
713
+ * with no confirmation surface. Unlike ordinary Bash calls — where the host's
714
+ * own permission system backs the decision up — nothing re-gates sandbox
715
+ * execution, and several formatters (gemini-cli, codex, kimi return null;
716
+ * kiro exits 0) would silently drop an `ask` decision and run the code
717
+ * unconfirmed. Deny with an actionable reason instead: fail-closed,
718
+ * consistent with the plugin adapters (OpenCode/KiloCode/OpenClaw) that
719
+ * block on ask.
720
+ */
721
+ function sandboxAskDecision(platform, describe, matchedPattern) {
722
+ if (ASK_CAPABLE_PLATFORMS.has(platform || "claude-code")) {
723
+ return { action: "ask" };
724
+ }
725
+ return {
726
+ action: "deny",
727
+ reason:
728
+ `Blocked by security policy: ${describe} matches ask pattern ${matchedPattern} — ` +
729
+ "this platform has no interactive confirmation path for sandbox execution. " +
730
+ "Move the pattern to allow (or deny) in your settings to make the decision explicit.",
731
+ };
732
+ }
733
+
688
734
  /**
689
735
  * Route a PreToolUse event. Returns normalized decision object or null for passthrough.
690
736
  *
@@ -749,9 +795,25 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
749
795
  return { action: "deny", reason: `Blocked by security policy: matches deny pattern ${result.matchedPattern}` };
750
796
  }
751
797
  if (result.decision === "ask" && result.matchedPattern) {
752
- return { action: "ask" };
798
+ // An ask pattern expresses "confirm with me" — a prompt, not a
799
+ // block. Only surface the ask decision on platforms whose
800
+ // PreToolUse response can render a real confirmation prompt
801
+ // (ASK_CAPABLE_PLATFORMS). Elsewhere the decision degraded into
802
+ // a hard block — the OpenCode/KiloCode plugins throw and OpenClaw
803
+ // blocks on ask — silently converting a confirmation intent into
804
+ // a denial: `Bash(git commit:*)` ask-listed for Claude Code's
805
+ // prompt hard-blocked `git commit` on OpenCode with no way to
806
+ // proceed. Fall through to Stage 2 and let the host's own
807
+ // permission system decide. The ctx_* sandbox branches below
808
+ // keep ask enforcement fail-safe instead — prompt where the
809
+ // platform can render one, deny with an actionable reason
810
+ // elsewhere — because no host permission system backs those
811
+ // calls.
812
+ if (ASK_CAPABLE_PLATFORMS.has(platform || "claude-code")) {
813
+ return { action: "ask" };
814
+ }
753
815
  }
754
- // "allow" or no match → fall through to Stage 2
816
+ // "allow" or no match (or ask on a prompt-incapable platform) → fall through to Stage 2
755
817
  }
756
818
  }
757
819
 
@@ -965,7 +1027,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
965
1027
  return { action: "deny", reason: `Blocked by security policy: shell code matches deny pattern ${result.matchedPattern}` };
966
1028
  }
967
1029
  if (result.decision === "ask" && result.matchedPattern) {
968
- return { action: "ask" };
1030
+ return sandboxAskDecision(platform, "shell code", result.matchedPattern);
969
1031
  }
970
1032
  }
971
1033
  }
@@ -997,7 +1059,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
997
1059
  return { action: "deny", reason: `Blocked by security policy: shell code matches deny pattern ${result.matchedPattern}` };
998
1060
  }
999
1061
  if (result.decision === "ask" && result.matchedPattern) {
1000
- return { action: "ask" };
1062
+ return sandboxAskDecision(platform, "shell code", result.matchedPattern);
1001
1063
  }
1002
1064
  }
1003
1065
  }
@@ -1018,7 +1080,7 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
1018
1080
  return { action: "deny", reason: `Blocked by security policy: batch command "${entry.label ?? cmd}" matches deny pattern ${result.matchedPattern}` };
1019
1081
  }
1020
1082
  if (result.decision === "ask" && result.matchedPattern) {
1021
- return { action: "ask" };
1083
+ return sandboxAskDecision(platform, `batch command "${entry.label ?? cmd}"`, result.matchedPattern);
1022
1084
  }
1023
1085
  }
1024
1086
  }
@@ -1048,3 +1110,104 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform, sessi
1048
1110
  // Unknown tool — pass through
1049
1111
  return null;
1050
1112
  }
1113
+
1114
+ /**
1115
+ * Route an OpenCode v2 permission "evaluate" event (ctx.permission.hook).
1116
+ *
1117
+ * v2's permission system asserts every core-tool action before execution and
1118
+ * lets plugins mutate the decision: `event.effect` ("allow" | "deny" | "ask")
1119
+ * and `event.message`. This maps context-mode's security policies onto that
1120
+ * surface, restoring ask/confirmation semantics that the execute.before bridge
1121
+ * cannot express (an ask there can only throw = hard block):
1122
+ *
1123
+ * - deny match → { effect: "deny" } — blocked with a reason. Normally
1124
+ * pre-empted by the execute.before throw (which fires first and is
1125
+ * unaffected by --auto); kept as defense-in-depth for any assert path
1126
+ * execute.before misses.
1127
+ * - ask match → { effect: "ask" } — an interactive confirmation in
1128
+ * TUI runs, restoring the user's ask intent even when host allow rules
1129
+ * would have auto-approved (verified live: a project-tier allow cannot
1130
+ * mask a global-tier ask). Under `--auto` the host auto-approves the
1131
+ * resulting ask too — an explicit opt-in (verified live).
1132
+ * - allow match → { effect: "allow" } — skips the host's default-ask
1133
+ * prompt for commands the user's own global rules pre-approve.
1134
+ * - no match → null — NO OPINION: the host decision (its own rules,
1135
+ * default ask, or --auto) stays untouched.
1136
+ *
1137
+ * Asymmetry is deliberate: deny/ask are honored from ALL policy tiers
1138
+ * (project + global — enforcement direction), while allow is honored from
1139
+ * GLOBAL tiers only. A project-shipped .claude/settings.json can harden a
1140
+ * repo's agent (deny/ask) but must not be able to weaken the host's
1141
+ * confirmation by pre-approving commands in a freshly cloned repo.
1142
+ *
1143
+ * Only the v2 "shell" action carries Bash-policy semantics — resources are the
1144
+ * parsed command statements of one shell invocation. Other actions (read,
1145
+ * write, edit, …) have no context-mode policy mapping (routePreToolUse has no
1146
+ * security branch for them either — parity) and return null.
1147
+ *
1148
+ * Fail-open on a missing security module, mirroring routePreToolUse Stage 1.
1149
+ *
1150
+ * @param {string} action - v2 permission action name ("shell" for the shell tool)
1151
+ * @param {string[]} resources - parsed command statement strings
1152
+ * @param {string} [projectDir] - project directory for policy lookup
1153
+ * @param {string} [platform] - platform ID for the adapter settings path
1154
+ * @returns {{ effect: "allow" | "deny" | "ask", message?: string } | null}
1155
+ */
1156
+ export function routePermissionEvaluate(action, resources, projectDir, platform) {
1157
+ if (action !== "shell") return null;
1158
+ if (!Array.isArray(resources) || resources.length === 0) return null;
1159
+ if (!security) return null;
1160
+
1161
+ const platformSettingsPath = getPlatformSettingsPath(platform);
1162
+ // Full policies (project + global) drive deny/ask — enforcement direction.
1163
+ const policies = security.readBashPolicies(projectDir, platformSettingsPath);
1164
+ if (policies.length === 0) return null;
1165
+
1166
+ const commands = resources.filter((r) => typeof r === "string" && r.length > 0);
1167
+ if (commands.length === 0) return null;
1168
+
1169
+ const results = commands.map((command) => security.evaluateCommand(command, policies));
1170
+
1171
+ // Deny wins across all statements (chain semantics: one denied segment
1172
+ // blocks the whole invocation — same as evaluateCommand).
1173
+ const denied = results.find((r) => r.decision === "deny");
1174
+ if (denied) {
1175
+ return {
1176
+ effect: "deny",
1177
+ message: `Blocked by security policy: matches deny pattern ${denied.matchedPattern}`,
1178
+ };
1179
+ }
1180
+
1181
+ // Ask: evaluated per tier INDEPENDENTLY. evaluateCommand on the combined
1182
+ // policy list returns the FIRST definitive result, so a project-tier allow
1183
+ // would mask a later global-tier ask — and since OpenCode v2's default
1184
+ // shell permission is allow, the user's global ask would silently vanish.
1185
+ // Per-policy calls keep evaluateCommand's chain/subshell parsing while
1186
+ // honoring an explicit ask from EVERY tier.
1187
+ for (const command of commands) {
1188
+ for (const policy of policies) {
1189
+ const result = security.evaluateCommand(command, [policy]);
1190
+ if (result.decision === "ask" && result.matchedPattern) {
1191
+ return {
1192
+ effect: "ask",
1193
+ message: `Confirmation required: matches ask pattern ${result.matchedPattern}`,
1194
+ };
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ // Allow: global tiers only (the user's own files) — a project cannot
1200
+ // pre-approve. All statements must be explicitly allowed, mirroring
1201
+ // evaluateCommand's "allowed iff every segment explicitly allowed".
1202
+ const globalPolicies = security.readBashPolicies(undefined, platformSettingsPath);
1203
+ if (globalPolicies.length > 0) {
1204
+ const allAllowed = commands.every((command) => {
1205
+ const result = security.evaluateCommand(command, globalPolicies);
1206
+ return result.decision === "allow" && result.matchedPattern;
1207
+ });
1208
+ if (allAllowed) return { effect: "allow" };
1209
+ }
1210
+
1211
+ // No explicit match — no opinion; the host decides.
1212
+ return null;
1213
+ }
@@ -1,7 +1,7 @@
1
- import{createRequire as le}from"node:module";import{existsSync as ge,unlinkSync as H,renameSync as Ee}from"node:fs";import{tmpdir as me}from"node:os";import{join as _e}from"node:path";var I=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let i=0;i<e.length;i++){let a=e[i];if(r)t+=a,a===r&&(r=null);else if(a==="'"||a==='"')t+=a,r=a;else if(a===";"){let u=t.trim();u&&this.#e.prepare(u).run(),t=""}else t+=a}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},M=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},y=null;function pe(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function Se(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,i]=(r.node??"0.0.0").split("."),a=Number(s),u=Number(i);return!Number.isFinite(a)||!Number.isFinite(u)?!1:a>22||a===22&&u>=5}function fe(){if(!y){let n=le(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;y=function(r,s){let i=new e(r,{readonly:s?.readonly,create:!0}),a=new I(i);return s?.timeout&&a.pragma(`busy_timeout = ${s.timeout}`),a}}else if(Se()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&pe(e)?y=function(r,s){let i=new e(r,{readOnly:s?.readonly??!1}),a=new M(i);return s?.timeout&&a.pragma(`busy_timeout = ${s.timeout}`),a}:y=n("better-sqlite3")}else y=n("better-sqlite3")}return y}function F(n,e=process.env){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");let t=ye(e);if(t!==null)try{n.pragma(`mmap_size = ${t}`)}catch{}}function ye(n=process.env){let e=n.CONTEXT_MODE_DB_MMAP_SIZE;if(e==null)return null;let t=String(e).trim();if(t==="")return null;let r=Number(t);return!Number.isFinite(r)||!Number.isInteger(r)||r<0?null:r}function B(n){if(!ge(n))for(let e of["-wal","-shm"])try{H(n+e)}catch{}}function he(n){for(let e of["","-wal","-shm"])try{H(n+e)}catch{}}function U(n){try{n.close()}catch{}}function $(n="context-mode"){return _e(me(),`${n}-${process.pid}.db`)}function W(n){if(n instanceof Error){let e=n.code;return typeof e=="string"?`${e} ${n.message}`:n.message}if(typeof n=="string")return n;if(n!==null&&typeof n=="object"){let e=n.code,t=n.message,r=[typeof e=="string"?e:"",typeof t=="string"?t:""].filter(Boolean);return r.length>0?r.join(" "):String(n)}return String(n)}function Te(n){let e=W(n);return e.includes("SQLITE_BUSY")||e.includes("database is locked")||e.includes("SQLITE_IOERR")||/disk i\/o error/i.test(e)}function ve(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){if(!Te(s))throw s;if(t=s instanceof Error?s:new Error(W(s)),r<e.length){let i=e[r],a=Date.now();for(;Date.now()-a<i;);}}throw new Error(`SQLITE_BUSY/SQLITE_IOERR: transient SQLite error after ${e.length} retries. Original error: ${t?.message}`)}function Re(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 be(n){let e=Date.now();for(let t of["","-wal","-shm"])try{Ee(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var De="[context-mode:db]",j=3e4,V=256,m=new Map;function Le(n){if(n instanceof Error){let e=n.code;return typeof e=="string"?e:""}if(n!==null&&typeof n=="object"){let e=n.code;return typeof e=="string"?e:""}return""}function Ne(n){if(n instanceof Error)return n.message;if(typeof n=="string")return n;if(n!==null&&typeof n=="object"){let e=n.message;if(typeof e=="string"&&e)return e}return String(n)}function l(n,e,t,r=Date.now()){try{let s=Le(e),i=Ne(e),a=t?` (${t})`:"",u=`${n}|${s}|${i}${a}`,d=m.get(u);if(d!==void 0&&r-d<j)return;if(m.set(u,r),m.size>V){for(let[E,L]of m)r-L>=j&&m.delete(E);for(;m.size>V;){let E=m.keys().next().value;if(E===void 0)break;m.delete(E)}}let c=process.env.OPENCODE_DEBUG,p=c!==void 0&&c!==""&&c!=="0"&&c!=="false"&&e instanceof Error&&e.stack?`
2
- ${e.stack}`:"",f=s?` [${s}]`:"";console.error(`${De} ${n}${f}: ${i}${a}${p}`)}catch{}}var R=Symbol.for("__context_mode_live_dbs_v3__"),x=(()=>{let n=globalThis;return n[R]||(n[R]=new Set,process.on("exit",()=>{for(let e of n[R])U(e);n[R].clear()})),n[R]})(),N=class{#e;#t;constructor(e){let t=fe();this.#e=e,B(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let i=s instanceof Error?s.message:String(s);if(Re(i)){l("SQLiteBase.open",s,e),be(e),B(e);try{r=new t(e,{timeout:3e4}),F(r)}catch(a){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw s}this.#t=r,x.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){x.delete(this.#t),U(this.#t)}withRetry(e){return ve(e)}cleanup(){x.delete(this.#t),U(this.#t),he(this.#e)}};import{createHash as b}from"node:crypto";import{execFileSync as Ce}from"node:child_process";import{accessSync as Oe,constants as Ae,existsSync as A,mkdirSync as we,realpathSync as xe,renameSync as k}from"node:fs";import{homedir as Q}from"node:os";import{dirname as Ie,isAbsolute as J,join as _,resolve as T}from"node:path";var g="CONTEXT_MODE_DIR",Z="sessions",X="content",D=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=g,s,i,a={}){super(i??Fe(e,t,a),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=a.ignoredEnvVar,this.ignoredReason=a.ignoredReason}},O=new Map;function rt(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):_(Me(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function Me(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?q(r.trim()):q(n,Q())}function q(n,e){return n.startsWith("~")?T(Q(),n.replace(/^~[/\\]?/,"")):J(n)?T(n):e?T(e,n):T(n)}function Ue(n,e,t){return new D(n,e,g,void 0,[`Invalid ${g} for context-mode ${n} directory: ${t}`,re()].join(`
3
- `))}function ee(n){let e=process.env[g];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:g,ignoredReason:"empty"};if(!J(t))throw Ue(n,t,`${g} must be an absolute path.`);return{kind:"override",root:T(t)}}function ke(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function te(n,e){let t=ee(n);return t.kind!=="override"?null:{kind:n,path:_(t.root,e),envVar:g,source:"override"}}function Pe(n,e,t){return{kind:n,path:T(e()),envVar:null,source:"default",...t}}function ne(n){let e=ee("session");return e.kind==="override"?{kind:"session",path:_(e.root,Z),envVar:g,source:"override"}:Pe("session",n,ke(e))}function st(n){let e=te("content",X);if(e)return e;let t=ne(n);return{kind:"content",path:_(Ie(t.path),X),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function ot(n){let e=te("stats",Z);if(e)return e;let t=ne(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function it(n){return n.message}function at(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function ct(){O.clear()}function ut(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=O.get(e);if(t instanceof D)throw t;if(t===n.path)return t;try{return we(n.path,{recursive:!0}),Oe(n.path,Ae.W_OK),O.set(e,n.path),n.path}catch(r){let s=new D(n.kind,je(r)??n.path,g,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw O.set(e,s),s}}function Fe(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Be(t),re()].filter(Boolean).join(`
4
- `)}function Be(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function re(){return`Set ${g} to a writable absolute path.`}function je(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var h;function S(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function G(n){let e=n;try{e=xe.native(n)}catch{}let t=S(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function se(n,e){return Ce("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function Ve(n){let e=se(n,["rev-parse","--show-toplevel"]);return e.length>0?S(e):null}function He(n){let e=se(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?S(e):null}function $e(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(h&&h.projectDir===n&&h.envSuffix===e)return h.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=Ve(n),s=He(n);if(r&&s){let i=G(r),a=G(s);i!==a&&(t=`__${b("sha256").update(i).digest("hex").slice(0,8)}`)}}catch{}return h={projectDir:n,envSuffix:e,suffix:t},t}function dt(){h=void 0}function oe(n){return b("sha256").update(S(n)).digest("hex").slice(0,16)}function ie(n){let e=S(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return b("sha256").update(t).digest("hex").slice(0,16)}function lt(n){let{projectDir:e,contentDir:t}=n,r=ie(e),s=_(t,`${r}.db`);if(A(s))return s;let i=oe(e);if(i===r)return s;let a=_(t,`${i}.db`);if(A(a))try{k(a,s);for(let u of["-wal","-shm"])try{k(a+u,s+u)}catch{}}catch{}return s}function gt(n){return We({...n,ext:".db"})}function We(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??$e(e),i=ie(e),a=_(t,`${i}${s}${r}`);if(A(a))return a;let u=oe(e);if(u===i)return a;let d=_(t,`${u}${s}${r}`);if(A(d))try{k(d,a)}catch{}return a}var Y=1e3,K=5;function C(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",getLatestCommitMessage:"getLatestCommitMessage",incrementCompactCount:"incrementCompactCount",getUsageCursor:"getUsageCursor",setUsageCursor:"setUsageCursor",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"},Xe=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ae(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,i]of Xe)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${i}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function Et(n,e){let t=null;try{t=new e(n),ae(t)}catch(r){l("ensureSessionEventsSchema",r,n)}finally{try{t?.close()}catch{}}}var z=class extends N{constructor(e){super(e?.dbPath??$("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=(this.db.pragma("table_xinfo(session_events)")??[]).find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch(e){l("SessionDB.initSchema.dataHashMigration",e,this.dbPath)}this.db.exec(`
1
+ import{createRequire as ge}from"node:module";import{existsSync as Ee,unlinkSync as $,renameSync as me}from"node:fs";import{tmpdir as _e}from"node:os";import{join as pe}from"node:path";var I=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){let t="",r=null;for(let a=0;a<e.length;a++){let i=e[a];if(r)t+=i,i===r&&(r=null);else if(i==="'"||i==='"')t+=i,r=i;else if(i===";"){let c=t.trim();c&&this.#e.prepare(c).run(),t=""}else t+=i}let s=t.trim();return s&&this.#e.prepare(s).run(),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>{let s=t.get(...r);return s===null?void 0:s},all:(...r)=>t.all(...r),iterate:(...r)=>t.iterate(...r)}}transaction(e){return this.#e.transaction(e)}close(){this.#e.close()}},M=class{#e;constructor(e){this.#e=e}pragma(e){let r=this.#e.prepare(`PRAGMA ${e}`).all();if(!r||r.length===0)return;if(r.length>1)return r;let s=Object.values(r[0]);return s.length===1?s[0]:r[0]}exec(e){return this.#e.exec(e),this}prepare(e){let t=this.#e.prepare(e);return{run:(...r)=>t.run(...r),get:(...r)=>t.get(...r),all:(...r)=>t.all(...r),iterate:(...r)=>typeof t.iterate=="function"?t.iterate(...r):t.all(...r)[Symbol.iterator]()}}transaction(e){return(...t)=>{this.#e.exec("BEGIN");try{let r=e(...t);return this.#e.exec("COMMIT"),r}catch(r){throw this.#e.exec("ROLLBACK"),r}}}close(){this.#e.close()}},y=null;function fe(n){let e=null;try{return e=new n(":memory:"),e.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{e?.close()}catch{}}}function Se(n,e){let t=e!==void 0?e:globalThis.Bun;if(typeof t<"u"&&t!==null)return!0;let r=n??process.versions,[s,a]=(r.node??"0.0.0").split("."),i=Number(s),c=Number(a);return!Number.isFinite(i)||!Number.isFinite(c)?!1:i>22||i===22&&c>=5}function ye(){if(!y){let n=ge(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;y=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new I(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(Se()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&fe(e)?y=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new M(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:y=n("better-sqlite3")}else y=n("better-sqlite3")}return y}function B(n,e=process.env){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");let t=he(e);if(t!==null)try{n.pragma(`mmap_size = ${t}`)}catch{}}function he(n=process.env){let e=n.CONTEXT_MODE_DB_MMAP_SIZE;if(e==null)return null;let t=String(e).trim();if(t==="")return null;let r=Number(t);return!Number.isFinite(r)||!Number.isInteger(r)||r<0?null:r}function j(n){if(!Ee(n))for(let e of["-wal","-shm"])try{$(n+e)}catch{}}function Te(n){for(let e of["","-wal","-shm"])try{$(n+e)}catch{}}function U(n){try{n.close()}catch{}}function W(n="context-mode"){return pe(_e(),`${n}-${process.pid}.db`)}function X(n){if(n instanceof Error){let e=n.code;return typeof e=="string"?`${e} ${n.message}`:n.message}if(typeof n=="string")return n;if(n!==null&&typeof n=="object"){let e=n.code,t=n.message,r=[typeof e=="string"?e:"",typeof t=="string"?t:""].filter(Boolean);return r.length>0?r.join(" "):String(n)}return String(n)}function ve(n){let e=X(n);return e.includes("SQLITE_BUSY")||e.includes("database is locked")||e.includes("SQLITE_IOERR")||/disk i\/o error/i.test(e)}function Re(n,e=[100,500,2e3]){let t;for(let a=0;a<=e.length;a++)try{return n()}catch(i){if(!ve(i))throw i;if(t=i instanceof Error?i:new Error(X(i)),!(i instanceof Error)){let c=k(i);c&&(t.code=c)}if(a<e.length){let c=e[a],d=Date.now();for(;Date.now()-d<c;);}}let r=new Error(`SQLITE_BUSY/SQLITE_IOERR: transient SQLite error after ${e.length} retries. Original error: ${t?.message}`),s=t?k(t):"";throw s&&(r.code=s),r}function be(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 De(n){let e=Date.now();for(let t of["","-wal","-shm"])try{me(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var Le="[context-mode:db]",V=3e4,H=256,m=new Map;function k(n){if(n instanceof Error){let e=n.code;return typeof e=="string"?e:""}if(n!==null&&typeof n=="object"){let e=n.code;return typeof e=="string"?e:""}return""}function Ne(n){if(n instanceof Error)return n.message;if(typeof n=="string")return n;if(n!==null&&typeof n=="object"){let e=n.message;if(typeof e=="string"&&e)return e}return String(n)}function l(n,e,t,r=Date.now()){try{let s=k(e),a=Ne(e),i=t?` (${t})`:"",c=`${n}|${s}|${a}${i}`,d=m.get(c);if(d!==void 0&&r-d<V)return;if(m.set(c,r),m.size>H){for(let[E,L]of m)r-L>=V&&m.delete(E);for(;m.size>H;){let E=m.keys().next().value;if(E===void 0)break;m.delete(E)}}let u=process.env.OPENCODE_DEBUG,p=u!==void 0&&u!==""&&u!=="0"&&u!=="false"&&e instanceof Error&&e.stack?`
2
+ ${e.stack}`:"",S=s?` [${s}]`:"";console.error(`${Le} ${n}${S}: ${a}${i}${p}`)}catch{}}var R=Symbol.for("__context_mode_live_dbs_v3__"),x=(()=>{let n=globalThis;return n[R]||(n[R]=new Set,process.on("exit",()=>{for(let e of n[R])U(e);n[R].clear()})),n[R]})(),N=class{#e;#t;constructor(e){let t=ye();this.#e=e,j(e);let r;try{r=new t(e,{timeout:3e4}),B(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(be(a)){l("SQLiteBase.open",s,e),De(e),j(e);try{r=new t(e,{timeout:3e4}),B(r)}catch(i){throw new Error(`Failed to create fresh DB after renaming corrupt file: ${i instanceof Error?i.message:String(i)}`)}}else throw s}this.#t=r,x.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){x.delete(this.#t),U(this.#t)}withRetry(e){return Re(e)}cleanup(){x.delete(this.#t),U(this.#t),Te(this.#e)}};import{createHash as b}from"node:crypto";import{execFileSync as Ce}from"node:child_process";import{accessSync as Oe,constants as Ae,existsSync as A,mkdirSync as we,realpathSync as xe,renameSync as P}from"node:fs";import{homedir as J}from"node:os";import{dirname as Ie,isAbsolute as Z,join as _,resolve as T}from"node:path";var g="CONTEXT_MODE_DIR",ee="sessions",q="content",D=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=g,s,a,i={}){super(a??Fe(e,t,i),{cause:s}),this.name="StorageDirectoryError",this.kind=e,this.path=t,this.overrideEnvVar=r,this.ignoredEnvVar=i.ignoredEnvVar,this.ignoredReason=i.ignoredReason}},O=new Map;function rt(n){let e=n.env??process.env,t=n.legacySessionDirEnv,r=t?e[t]?.trim():void 0;return r&&t?(n.onLegacySessionDir?.(t,r),r):_(Me(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function Me(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?G(r.trim()):G(n,J())}function G(n,e){return n.startsWith("~")?T(J(),n.replace(/^~[/\\]?/,"")):Z(n)?T(n):e?T(e,n):T(n)}function Ue(n,e,t){return new D(n,e,g,void 0,[`Invalid ${g} for context-mode ${n} directory: ${t}`,se()].join(`
3
+ `))}function te(n){let e=process.env[g];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:g,ignoredReason:"empty"};if(!Z(t))throw Ue(n,t,`${g} must be an absolute path.`);return{kind:"override",root:T(t)}}function ke(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function ne(n,e){let t=te(n);return t.kind!=="override"?null:{kind:n,path:_(t.root,e),envVar:g,source:"override"}}function Pe(n,e,t){return{kind:n,path:T(e()),envVar:null,source:"default",...t}}function re(n){let e=te("session");return e.kind==="override"?{kind:"session",path:_(e.root,ee),envVar:g,source:"override"}:Pe("session",n,ke(e))}function st(n){let e=ne("content",q);if(e)return e;let t=re(n);return{kind:"content",path:_(Ie(t.path),q),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function ot(n){let e=ne("stats",ee);if(e)return e;let t=re(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function it(n){return n.message}function at(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function ct(){O.clear()}function ut(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=O.get(e);if(t instanceof D)throw t;if(t===n.path)return t;try{return we(n.path,{recursive:!0}),Oe(n.path,Ae.W_OK),O.set(e,n.path),n.path}catch(r){let s=new D(n.kind,je(r)??n.path,g,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw O.set(e,s),s}}function Fe(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Be(t),se()].filter(Boolean).join(`
4
+ `)}function Be(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function se(){return`Set ${g} to a writable absolute path.`}function je(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var h;function f(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function Y(n){let e=n;try{e=xe.native(n)}catch{}let t=f(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function oe(n,e){return Ce("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function Ve(n){let e=oe(n,["rev-parse","--show-toplevel"]);return e.length>0?f(e):null}function He(n){let e=oe(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?f(e):null}function $e(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(h&&h.projectDir===n&&h.envSuffix===e)return h.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=Ve(n),s=He(n);if(r&&s){let a=Y(r),i=Y(s);a!==i&&(t=`__${b("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return h={projectDir:n,envSuffix:e,suffix:t},t}function dt(){h=void 0}function ie(n){return b("sha256").update(f(n)).digest("hex").slice(0,16)}function ae(n){let e=f(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return b("sha256").update(t).digest("hex").slice(0,16)}function lt(n){let{projectDir:e,contentDir:t}=n,r=ae(e),s=_(t,`${r}.db`);if(A(s))return s;let a=ie(e);if(a===r)return s;let i=_(t,`${a}.db`);if(A(i))try{P(i,s);for(let c of["-wal","-shm"])try{P(i+c,s+c)}catch{}}catch{}return s}function gt(n){return We({...n,ext:".db"})}function We(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??$e(e),a=ae(e),i=_(t,`${a}${s}${r}`);if(A(i))return i;let c=ie(e);if(c===a)return i;let d=_(t,`${c}${s}${r}`);if(A(d))try{P(d,i)}catch{}return i}var K=1e3,z=5;function C(n){let e=Number(n);return!Number.isFinite(e)||e<=0?0:Math.floor(e)}var o={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",getSessionRollup:"getSessionRollup",getMaxFileEdits:"getMaxFileEdits",getLatestCommitMessage:"getLatestCommitMessage",incrementCompactCount:"incrementCompactCount",getUsageCursor:"getUsageCursor",setUsageCursor:"setUsageCursor",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"},Xe=[["project_dir","TEXT NOT NULL DEFAULT ''"],["attribution_source","TEXT NOT NULL DEFAULT 'unknown'"],["attribution_confidence","REAL NOT NULL DEFAULT 0"],["bytes_avoided","INTEGER NOT NULL DEFAULT 0"],["bytes_returned","INTEGER NOT NULL DEFAULT 0"]];function ce(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Xe)t.has(s)||(n.exec(`ALTER TABLE session_events ADD COLUMN ${s} ${a}`),r=!0);return r&&n.exec("CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(session_id, project_dir)"),r}function Et(n,e){let t=null;try{t=new e(n),ce(t)}catch(r){l("ensureSessionEventsSchema",r,n)}finally{try{t?.close()}catch{}}}var Q=class extends N{constructor(e){super(e?.dbPath??W("session"))}stmt(e){return this.stmts.get(e)}initSchema(){try{let t=(this.db.pragma("table_xinfo(session_events)")??[]).find(r=>r.name==="data_hash");t&&t.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch(e){l("SessionDB.initSchema.dataHashMigration",e,this.dbPath)}this.db.exec(`
5
5
  CREATE TABLE IF NOT EXISTS session_events (
6
6
  id INTEGER PRIMARY KEY AUTOINCREMENT,
7
7
  session_id TEXT NOT NULL,
@@ -51,7 +51,7 @@ ${e.stack}`:"",f=s?` [${s}]`:"";console.error(`${De} ${n}${f}: ${i}${a}${p}`)}ca
51
51
  );
52
52
 
53
53
  CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
54
- `);try{ae(this.db)}catch(e){l("SessionDB.initSchema.migrateColumns",e,this.dbPath)}try{(this.db.pragma("table_xinfo(session_meta)")??[]).some(t=>t.name==="usage_cursor")||this.db.exec("ALTER TABLE session_meta ADD COLUMN usage_cursor TEXT")}catch(e){l("SessionDB.initSchema.usageCursorMigration",e,this.dbPath)}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
54
+ `);try{ce(this.db)}catch(e){l("SessionDB.initSchema.migrateColumns",e,this.dbPath)}try{(this.db.pragma("table_xinfo(session_meta)")??[]).some(t=>t.name==="usage_cursor")||this.db.exec("ALTER TABLE session_meta ADD COLUMN usage_cursor TEXT")}catch(e){l("SessionDB.initSchema.usageCursorMigration",e,this.dbPath)}}prepareStatements(){this.stmts=new Map;let e=(t,r)=>{this.stmts.set(t,this.db.prepare(r))};e(o.insertEvent,`INSERT INTO session_events (
55
55
  session_id, type, category, priority, data,
56
56
  project_dir, attribution_source, attribution_confidence,
57
57
  bytes_avoided, bytes_returned,
@@ -140,6 +140,6 @@ ${e.stack}`:"",f=s?` [${s}]`:"";console.error(`${De} ${n}${f}: ${i}${a}${p}`)}ca
140
140
  FROM tool_calls WHERE session_id = ?`),e(o.getToolCallByTool,`SELECT tool, calls, bytes_returned
141
141
  FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),e(o.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
142
142
  COALESCE(SUM(bytes_returned), 0) AS bytes_returned
143
- FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,i){let a=b("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),u=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),c=Number(s?.confidence??t.attribution_confidence??0),v=Number.isFinite(c)?Math.max(0,Math.min(1,c)):0,p=C(i?.bytesAvoided),f=C(i?.bytesReturned),E=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,K,t.type,a))return;this.stmt(o.getEventCount).get(e).cnt>=Y&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,u,d,v,p,f,r,a),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>E())}bulkInsertEvents(e,t,r="PostToolUse",s,i){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],i?.[0]);return}let a=t.map((d,c)=>{let v=b("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),p=s?.[c],f=String(p?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),E=f===""?"":S(f),L=String(p?.source??d.attribution_source??"unknown"),w=Number(p?.confidence??d.attribution_confidence??0),ce=Number.isFinite(w)?Math.max(0,Math.min(1,w)):0,P=i?.[c],ue=C(P?.bytesAvoided),de=C(P?.bytesReturned);return{event:d,dataHash:v,projectDir:E,attributionSource:L,attributionConfidence:ce,bytesAvoided:ue,bytesReturned:de}}),u=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let c of a)this.stmt(o.checkDuplicate).get(e,K,c.event.type,c.dataHash)||(d>=Y?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,c.event.type,c.event.category,c.event.priority,c.event.data,c.projectDir,c.attributionSource,c.attributionConfidence,c.bytesAvoided,c.bytesReturned,r,c.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>u())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,i=t?.minPriority;return s&&i!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,i,r):s?this.stmt(o.getEventsByType).all(e,s,r):i!==void 0?this.stmt(o.getEventsByPriority).all(e,i,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch(t){return l("SessionDB.getSessionProjectDir",t,this.dbPath),""}}searchEvents(e,t,r,s){try{let i=e.replace(/[%_]/g,u=>"\\"+u),a=s??null;return this.stmt(o.searchEvents).all(r,i,i,a,a,t)}catch(i){return l("SessionDB.searchEvents",i,this.dbPath),[]}}getSessionIdsForProject(e){try{let t=S(e);return this.db.prepare(`SELECT DISTINCT session_id
143
+ FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=b("sha256").update(t.data).digest("hex").slice(0,16).toUpperCase(),c=String(s?.projectDir??t.project_dir??this._getSessionProjectDir(e)).trim(),d=String(s?.source??t.attribution_source??"unknown"),u=Number(s?.confidence??t.attribution_confidence??0),v=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,p=C(a?.bytesAvoided),S=C(a?.bytesReturned),E=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,z,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=K&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,v,p,S,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>E())}bulkInsertEvents(e,t,r="PostToolUse",s,a){if(!t||t.length===0)return;if(t.length===1){this.insertEvent(e,t[0],r,s?.[0],a?.[0]);return}let i=t.map((d,u)=>{let v=b("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),p=s?.[u],S=String(p?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),E=S===""?"":f(S),L=String(p?.source??d.attribution_source??"unknown"),w=Number(p?.confidence??d.attribution_confidence??0),ue=Number.isFinite(w)?Math.max(0,Math.min(1,w)):0,F=a?.[u],de=C(F?.bytesAvoided),le=C(F?.bytesReturned);return{event:d,dataHash:v,projectDir:E,attributionSource:L,attributionConfidence:ue,bytesAvoided:de,bytesReturned:le}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,z,u.event.type,u.dataHash)||(d>=K?this.stmt(o.evictLowestPriority).run(e):d++,this.stmt(o.insertEvent).run(e,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,r,u.dataHash));this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>c())}getEvents(e,t){let r=t?.limit??1e3,s=t?.type,a=t?.minPriority;return s&&a!==void 0?this.stmt(o.getEventsByTypeAndPriority).all(e,s,a,r):s?this.stmt(o.getEventsByType).all(e,s,r):a!==void 0?this.stmt(o.getEventsByPriority).all(e,a,r):this.stmt(o.getEvents).all(e,r)}getEventCount(e){return this.stmt(o.getEventCount).get(e).cnt}getEventBytesSummary(e){let t=this.stmt(o.getEventBytesSummary).get(e);return{bytesAvoided:Number(t?.bytes_avoided??0),bytesReturned:Number(t?.bytes_returned??0)}}getLatestAttributedProjectDir(e){return this.stmt(o.getLatestAttributedProject).get(e)?.project_dir||null}_getSessionProjectDir(e){try{return this.db.prepare("SELECT project_dir FROM session_meta WHERE session_id = ?").get(e)?.project_dir||""}catch(t){return l("SessionDB.getSessionProjectDir",t,this.dbPath),""}}searchEvents(e,t,r,s){try{let a=e.replace(/[%_]/g,c=>"\\"+c),i=s??null;return this.stmt(o.searchEvents).all(r,a,a,i,i,t)}catch(a){return l("SessionDB.searchEvents",a,this.dbPath),[]}}getSessionIdsForProject(e){try{let t=f(e);return this.db.prepare(`SELECT DISTINCT session_id
144
144
  FROM session_events
145
- WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch(t){return l("SessionDB.getSessionIdsForProject",t,this.dbPath),[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),i=this.getSessionStats(e),a=(t?.tool_calls??0)>0?t?.unique_files??0:0,u=t?.errors??0,d=Math.min(a,u);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:i?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}getUsageCursor(e){return this.stmt(o.getUsageCursor).get(e)?.usage_cursor??null}setUsageCursor(e,t){this.stmt(o.setUsageCursor).run(t,e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.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(e){return l("SessionDB.getLatestSessionId",e,this.dbPath),null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch(i){l("SessionDB.incrementToolCall",i,this.dbPath)}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let i of r)s[i.tool]={calls:i.calls,bytesReturned:i.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch(t){return l("SessionDB.getToolCallStats",t,this.dbPath),{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{z as SessionDB,D as StorageDirectoryError,dt as _resetWorktreeSuffixCacheForTests,ae as applyMissingSessionEventsColumns,ct as clearStorageDirectoryCheckCacheForTests,at as describeStorageDirectorySource,Et as ensureSessionEventsSchema,ut as ensureWritableStorageDir,it as formatStorageDirectoryError,$e as getWorktreeSuffix,ie as hashProjectDirCanonical,oe as hashProjectDirLegacy,S as normalizeWorktreePath,st as resolveContentStorageDir,lt as resolveContentStorePath,rt as resolveDefaultSessionDir,gt as resolveSessionDbPath,We as resolveSessionPath,ne as resolveSessionStorageDir,ot as resolveStatsStorageDir};
145
+ WHERE RTRIM(REPLACE(project_dir, '\\', '/'), '/') = ?`).all(t).map(s=>s.session_id)}catch(t){return l("SessionDB.getSessionIdsForProject",t,this.dbPath),[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}getSessionRollup(e){let t=this.stmt(o.getSessionRollup).get(e),r=this.stmt(o.getMaxFileEdits).get(e),s=this.stmt(o.getLatestCommitMessage).get(e),a=this.getSessionStats(e),i=(t?.tool_calls??0)>0?t?.unique_files??0:0,c=t?.errors??0,d=Math.min(i,c);return{tool_calls:t?.tool_calls??0,errors:t?.errors??0,unique_tools:t?.unique_tools??0,unique_files:t?.unique_files??0,max_file_edits:r?.max_file_edits??0,has_commit:t?.has_commit??0,commit_message:s?.data??"",edit_test_cycles:d,duration_min:t?.duration_min??0,compact_count:a?.compact_count??0,sources_indexed:t?.sources_indexed??0,total_chunks:t?.total_chunks??0,search_queries:t?.search_queries??0}}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(e)}getUsageCursor(e){return this.stmt(o.getUsageCursor).get(e)?.usage_cursor??null}setUsageCursor(e,t){this.stmt(o.setUsageCursor).run(t,e)}upsertResume(e,t,r){this.stmt(o.upsertResume).run(e,t,r??0)}getResume(e){return this.stmt(o.getResume).get(e)??null}markResumeConsumed(e){this.stmt(o.markResumeConsumed).run(e)}claimLatestUnconsumedResume(e){let t=this.stmt(o.claimLatestUnconsumedResume).get(e);return t?{sessionId:t.session_id,snapshot:t.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(e){return l("SessionDB.getLatestSessionId",e,this.dbPath),null}}incrementToolCall(e,t,r=0){let s=Number.isFinite(r)&&r>0?Math.round(r):0;try{this.stmt(o.incrementToolCall).run(e,t,s)}catch(a){l("SessionDB.incrementToolCall",a,this.dbPath)}}getToolCallStats(e){try{let t=this.stmt(o.getToolCallTotals).get(e),r=this.stmt(o.getToolCallByTool).all(e),s={};for(let a of r)s[a.tool]={calls:a.calls,bytesReturned:a.bytes_returned};return{totalCalls:t?.calls??0,totalBytesReturned:t?.bytes_returned??0,byTool:s}}catch(t){return l("SessionDB.getToolCallStats",t,this.dbPath),{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(e){this.db.transaction(()=>{this.stmt(o.deleteEvents).run(e),this.stmt(o.deleteResume).run(e),this.stmt(o.deleteMeta).run(e)})()}cleanupOldSessions(e=7){let t=`-${e}`,r=this.stmt(o.getOldSessions).all(t);for(let{session_id:s}of r)this.deleteSession(s);return r.length}pruneOrphanedEvents(){let e=this.db.prepare("DELETE FROM session_events WHERE session_id NOT IN (SELECT session_id FROM session_meta)").run();return Number(e.changes??0)}};export{Q as SessionDB,D as StorageDirectoryError,dt as _resetWorktreeSuffixCacheForTests,ce as applyMissingSessionEventsColumns,ct as clearStorageDirectoryCheckCacheForTests,at as describeStorageDirectorySource,Et as ensureSessionEventsSchema,ut as ensureWritableStorageDir,it as formatStorageDirectoryError,$e as getWorktreeSuffix,ae as hashProjectDirCanonical,ie as hashProjectDirLegacy,f as normalizeWorktreePath,st as resolveContentStorageDir,lt as resolveContentStorePath,rt as resolveDefaultSessionDir,gt as resolveSessionDbPath,We as resolveSessionPath,re as resolveSessionStorageDir,ot as resolveStatsStorageDir};
@@ -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": "2.0.1",
6
+ "version": "2.0.3",
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": "@mxalbert/context-mode",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",