context-mode 1.0.157 → 1.0.159

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
- import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync as P,renameSync as ae}from"node:fs";import{tmpdir as ce}from"node:os";import{join as ue}from"node:path";var A=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()}},w=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()}},E=null;function de(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 le(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 ge(){if(!E){let n=oe(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;E=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(le()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&de(e)?E=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:E=n("better-sqlite3")}else E=n("better-sqlite3")}return E}function k(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function F(n){if(!ie(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function Ee(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function I(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function j(n="context-mode"){return ue(ce(),`${n}-${process.pid}.db`)}function me(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function pe(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 ye(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ae(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var _=Symbol.for("__context_mode_live_dbs_v3__"),C=(()=>{let n=globalThis;return n[_]||(n[_]=new Set,process.on("exit",()=>{for(let e of n[_])I(e);n[_].clear()})),n[_]})(),v=class{#e;#t;constructor(e){let t=ge();this.#e=e,F(e);let r;try{r=new t(e,{timeout:3e4}),k(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(pe(a)){ye(e),F(e);try{r=new t(e,{timeout:3e4}),k(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,C.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){C.delete(this.#t),I(this.#t)}withRetry(e){return me(e)}cleanup(){C.delete(this.#t),I(this.#t),Ee(this.#e)}};import{createHash as f}from"node:crypto";import{execFileSync as _e}from"node:child_process";import{accessSync as fe,constants as he,existsSync as D,mkdirSync as Te,realpathSync as Se,renameSync as x}from"node:fs";import{homedir as G}from"node:os";import{dirname as ve,isAbsolute as Y,join as g,resolve as p}from"node:path";var l="CONTEXT_MODE_DIR",q="sessions",B="content",h=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Ne(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}},b=new Map;function Ge(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):g(Re(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function Re(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,G())}function V(n,e){return n.startsWith("~")?p(G(),n.replace(/^~[/\\]?/,"")):Y(n)?p(n):e?p(e,n):p(n)}function be(n,e,t){return new h(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
2
- `))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!Y(t))throw be(n,t,`${l} must be an absolute path.`);return{kind:"override",root:p(t)}}function De(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:g(t.root,e),envVar:l,source:"override"}}function Le(n,e,t){return{kind:n,path:p(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:g(e.root,q),envVar:l,source:"override"}:Le("session",n,De(e))}function Ye(n){let e=z("content",B);if(e)return e;let t=Q(n);return{kind:"content",path:g(ve(t.path),B),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function qe(n){let e=z("stats",q);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){return n.message}function ze(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Qe(){b.clear()}function Je(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=b.get(e);if(t instanceof h)throw t;if(t===n.path)return t;try{return Te(n.path,{recursive:!0}),fe(n.path,he.W_OK),b.set(e,n.path),n.path}catch(r){let s=new h(n.kind,Ce(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw b.set(e,s),s}}function Ne(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
3
- `)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ce(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var m;function T(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function W(n){let e=n;try{e=Se.native(n)}catch{}let t=T(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return _e("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function Ae(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?T(e):null}function we(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?T(e):null}function Ie(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(m&&m.projectDir===n&&m.envSuffix===e)return m.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=Ae(n),s=we(n);if(r&&s){let a=W(r),i=W(s);a!==i&&(t=`__${f("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return m={projectDir:n,envSuffix:e,suffix:t},t}function Ze(){m=void 0}function ee(n){return f("sha256").update(T(n)).digest("hex").slice(0,16)}function te(n){let e=T(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return f("sha256").update(t).digest("hex").slice(0,16)}function et(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=g(t,`${r}.db`);if(D(s))return s;let a=ee(e);if(a===r)return s;let i=g(t,`${a}.db`);if(D(i))try{x(i,s);for(let c of["-wal","-shm"])try{x(i+c,s+c)}catch{}}catch{}return s}function tt(n){return xe({...n,ext:".db"})}function xe(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??Ie(e),a=te(e),i=g(t,`${a}${s}${r}`);if(D(i))return i;let c=ee(e);if(c===a)return i;let d=g(t,`${c}${s}${r}`);if(D(d))try{x(d,i)}catch{}return i}var H=1e3,X=5;function R(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",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"},Ue=[["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 ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Ue)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 nt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends v{constructor(e){super(e?.dbPath??j("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{}this.db.exec(`
1
+ import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync as P,renameSync as ae}from"node:fs";import{tmpdir as ce}from"node:os";import{join as ue}from"node:path";var A=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()}},w=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()}},g=null;function de(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 le(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 Ee(){if(!g){let n=oe(import.meta.url);if(globalThis.Bun){let e=n(["bun","sqlite"].join(":")).Database;g=function(r,s){let a=new e(r,{readonly:s?.readonly,create:!0}),i=new A(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}}else if(le()){let e=null;try{({DatabaseSync:e}=n(["node","sqlite"].join(":")))}catch{e=null}e&&de(e)?g=function(r,s){let a=new e(r,{readOnly:s?.readonly??!1}),i=new w(a);return s?.timeout&&i.pragma(`busy_timeout = ${s.timeout}`),i}:g=n("better-sqlite3")}else g=n("better-sqlite3")}return g}function F(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}}function k(n){if(!ie(n))for(let e of["-wal","-shm"])try{P(n+e)}catch{}}function ge(n){for(let e of["","-wal","-shm"])try{P(n+e)}catch{}}function x(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function B(n="context-mode"){return ue(ce(),`${n}-${process.pid}.db`)}function me(n,e=[100,500,2e3]){let t;for(let r=0;r<=e.length;r++)try{return n()}catch(s){let a=s instanceof Error?s.message:String(s);if(!a.includes("SQLITE_BUSY")&&!a.includes("database is locked"))throw s;if(t=s instanceof Error?s:new Error(a),r<e.length){let i=e[r],c=Date.now();for(;Date.now()-c<i;);}}throw new Error(`SQLITE_BUSY: database is locked after ${e.length} retries. Original error: ${t?.message}`)}function _e(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 pe(n){let e=Date.now();for(let t of["","-wal","-shm"])try{ae(n+t,`${n}${t}.corrupt-${e}`)}catch{}}var y=Symbol.for("__context_mode_live_dbs_v3__"),C=(()=>{let n=globalThis;return n[y]||(n[y]=new Set,process.on("exit",()=>{for(let e of n[y])x(e);n[y].clear()})),n[y]})(),v=class{#e;#t;constructor(e){let t=Ee();this.#e=e,k(e);let r;try{r=new t(e,{timeout:3e4}),F(r)}catch(s){let a=s instanceof Error?s.message:String(s);if(_e(a)){pe(e),k(e);try{r=new t(e,{timeout:3e4}),F(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,C.add(this.#t),this.initSchema(),this.prepareStatements()}get db(){return this.#t}get dbPath(){return this.#e}close(){C.delete(this.#t),x(this.#t)}withRetry(e){return me(e)}cleanup(){C.delete(this.#t),x(this.#t),ge(this.#e)}};import{createHash as S}from"node:crypto";import{execFileSync as ye}from"node:child_process";import{accessSync as Se,constants as fe,existsSync as D,mkdirSync as he,realpathSync as Te,renameSync as I}from"node:fs";import{homedir as q}from"node:os";import{dirname as ve,isAbsolute as G,join as E,resolve as _}from"node:path";var l="CONTEXT_MODE_DIR",Y="sessions",j="content",f=class extends Error{kind;path;overrideEnvVar;ignoredEnvVar;ignoredReason;constructor(e,t,r=l,s,a,i={}){super(a??Le(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}},b=new Map;function qe(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):E(Re(n.configDir,n.configDirEnv,e),"context-mode","sessions")}function Re(n,e,t){let r=e?t[e]:void 0;return r&&r.trim()!==""?V(r.trim()):V(n,q())}function V(n,e){return n.startsWith("~")?_(q(),n.replace(/^~[/\\]?/,"")):G(n)?_(n):e?_(e,n):_(n)}function be(n,e,t){return new f(n,e,l,void 0,[`Invalid ${l} for context-mode ${n} directory: ${t}`,J()].join(`
2
+ `))}function K(n){let e=process.env[l];if(e===void 0)return{kind:"unset"};let t=e.trim();if(!t)return{kind:"ignored-empty",ignoredEnvVar:l,ignoredReason:"empty"};if(!G(t))throw be(n,t,`${l} must be an absolute path.`);return{kind:"override",root:_(t)}}function De(n){return n.kind==="ignored-empty"?{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason}:{}}function z(n,e){let t=K(n);return t.kind!=="override"?null:{kind:n,path:E(t.root,e),envVar:l,source:"override"}}function Ne(n,e,t){return{kind:n,path:_(e()),envVar:null,source:"default",...t}}function Q(n){let e=K("session");return e.kind==="override"?{kind:"session",path:E(e.root,Y),envVar:l,source:"override"}:Ne("session",n,De(e))}function Ge(n){let e=z("content",j);if(e)return e;let t=Q(n);return{kind:"content",path:E(ve(t.path),j),envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ye(n){let e=z("stats",Y);if(e)return e;let t=Q(n);return{kind:"stats",path:t.path,envVar:t.envVar,source:t.source,ignoredEnvVar:t.ignoredEnvVar,ignoredReason:t.ignoredReason}}function Ke(n){return n.message}function ze(n){return n.source==="override"&&n.envVar?`via ${n.envVar}`:n.ignoredEnvVar&&n.ignoredReason==="empty"?`default; ignored empty ${n.ignoredEnvVar}`:"default"}function Qe(){b.clear()}function Je(n){let e=[n.kind,n.path,n.source,n.envVar??"",n.ignoredEnvVar??"",n.ignoredReason??""].join("\0"),t=b.get(e);if(t instanceof f)throw t;if(t===n.path)return t;try{return he(n.path,{recursive:!0}),Se(n.path,fe.W_OK),b.set(e,n.path),n.path}catch(r){let s=new f(n.kind,Ce(r)??n.path,l,r,void 0,{ignoredEnvVar:n.ignoredEnvVar,ignoredReason:n.ignoredReason});throw b.set(e,s),s}}function Le(n,e,t={}){return[`context-mode ${n} directory is not writable: ${e}`,Oe(t),J()].filter(Boolean).join(`
3
+ `)}function Oe(n){return n.ignoredEnvVar&&n.ignoredReason==="empty"?`Ignored empty ${n.ignoredEnvVar}; using adapter default.`:null}function J(){return`Set ${l} to a writable absolute path.`}function Ce(n){if(!n||typeof n!="object")return null;let e=n.path;return typeof e=="string"&&e.length>0?e:null}var m;function h(n){let e=n.replace(/\\/g,"/");return/^\/+$/.test(e)?"/":/^[A-Za-z]:\/+$/.test(e)?`${e.slice(0,2)}/`:e.replace(/\/+$/,"")}function H(n){let e=n;try{e=Te.native(n)}catch{}let t=h(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Z(n,e){return ye("git",["-C",n,...e],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function Ae(n){let e=Z(n,["rev-parse","--show-toplevel"]);return e.length>0?h(e):null}function we(n){let e=Z(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(t=>t.startsWith("worktree "))?.replace("worktree ","")?.trim();return e?h(e):null}function xe(n=process.cwd()){let e=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(m&&m.projectDir===n&&m.envSuffix===e)return m.suffix;let t="";if(e!==void 0)t=e?`__${e}`:"";else try{let r=Ae(n),s=we(n);if(r&&s){let a=H(r),i=H(s);a!==i&&(t=`__${S("sha256").update(a).digest("hex").slice(0,8)}`)}}catch{}return m={projectDir:n,envSuffix:e,suffix:t},t}function Ze(){m=void 0}function ee(n){return S("sha256").update(h(n)).digest("hex").slice(0,16)}function te(n){let e=h(n),t=process.platform==="darwin"||process.platform==="win32"?e.toLowerCase():e;return S("sha256").update(t).digest("hex").slice(0,16)}function et(n){let{projectDir:e,contentDir:t}=n,r=te(e),s=E(t,`${r}.db`);if(D(s))return s;let a=ee(e);if(a===r)return s;let i=E(t,`${a}.db`);if(D(i))try{I(i,s);for(let c of["-wal","-shm"])try{I(i+c,s+c)}catch{}}catch{}return s}function tt(n){return Ie({...n,ext:".db"})}function Ie(n){let{projectDir:e,sessionsDir:t,ext:r}=n,s=n.suffix??xe(e),a=te(e),i=E(t,`${a}${s}${r}`);if(D(i))return i;let c=ee(e);if(c===a)return i;let d=E(t,`${c}${s}${r}`);if(D(d))try{I(d,i)}catch{}return i}var W=1e3,X=5;function R(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",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"},Ue=[["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 ne(n){let e=n.pragma("table_xinfo(session_events)"),t=new Set(e.map(s=>s.name)),r=!1;for(let[s,a]of Ue)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 nt(n,e){let t=null;try{t=new e(n),ne(t)}catch{}finally{try{t?.close()}catch{}}}var $=class extends v{constructor(e){super(e?.dbPath??B("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{}this.db.exec(`
4
4
  CREATE TABLE IF NOT EXISTS session_events (
5
5
  id INTEGER PRIMARY KEY AUTOINCREMENT,
6
6
  session_id TEXT NOT NULL,
@@ -87,7 +87,24 @@ import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync
87
87
  )`),e(o.updateMetaLastEvent,`UPDATE session_meta
88
88
  SET last_event_at = datetime('now'), event_count = event_count + 1
89
89
  WHERE session_id = ?`),e(o.ensureSession,"INSERT OR IGNORE INTO session_meta (session_id, project_dir) VALUES (?, ?)"),e(o.getSessionStats,`SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count
90
- FROM session_meta WHERE session_id = ?`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
90
+ FROM session_meta WHERE session_id = ?`),e(o.getSessionRollup,`SELECT
91
+ COUNT(*) AS tool_calls,
92
+ COALESCE(SUM(CASE WHEN category = 'error' THEN 1 ELSE 0 END), 0) AS errors,
93
+ COUNT(DISTINCT type) AS unique_tools,
94
+ COUNT(DISTINCT CASE WHEN category = 'file' THEN data END) AS unique_files,
95
+ CASE WHEN SUM(CASE WHEN category = 'git' THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END AS has_commit,
96
+ CAST(COALESCE((MAX(strftime('%s', created_at)) - MIN(strftime('%s', created_at))) / 60.0, 0) AS INTEGER) AS duration_min,
97
+ COALESCE(SUM(CASE WHEN type = 'external_ref' THEN 1 ELSE 0 END), 0) AS sources_indexed,
98
+ CAST(COALESCE(SUM(bytes_avoided) / 1024.0, 0) AS INTEGER) AS total_chunks,
99
+ COALESCE(SUM(CASE WHEN type IN ('file_search', 'file_glob') THEN 1 ELSE 0 END), 0) AS search_queries
100
+ FROM session_events
101
+ WHERE session_id = ?`),e(o.getMaxFileEdits,`SELECT COALESCE(MAX(c), 0) AS max_file_edits
102
+ FROM (
103
+ SELECT COUNT(*) AS c
104
+ FROM session_events
105
+ WHERE session_id = ? AND category = 'file' AND type IN ('file_edit', 'file_write')
106
+ GROUP BY data
107
+ )`),e(o.incrementCompactCount,"UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?"),e(o.upsertResume,`INSERT INTO session_resume (session_id, snapshot, event_count)
91
108
  VALUES (?, ?, ?)
92
109
  ON CONFLICT(session_id) DO UPDATE SET
93
110
  snapshot = excluded.snapshot,
@@ -118,6 +135,6 @@ import{createRequire as oe}from"node:module";import{existsSync as ie,unlinkSync
118
135
  FROM tool_calls WHERE session_id = ?`),e(o.getToolCallByTool,`SELECT tool, calls, bytes_returned
119
136
  FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),e(o.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
120
137
  COALESCE(SUM(bytes_returned), 0) AS bytes_returned
121
- FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=f("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),S=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,y=R(a?.bytesAvoided),L=R(a?.bytesReturned),N=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=H&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,S,y,L,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>N())}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 S=f("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),y=s?.[u],L=String(y?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),N=String(y?.source??d.attribution_source??"unknown"),O=Number(y?.confidence??d.attribution_confidence??0),U=Number.isFinite(O)?Math.max(0,Math.min(1,O)):0,M=a?.[u],re=R(M?.bytesAvoided),se=R(M?.bytesReturned);return{event:d,dataHash:S,projectDir:L,attributionSource:N,attributionConfidence:U,bytesAvoided:re,bytesReturned:se}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=H?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{return""}}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{return[]}}getSessionIdsForProject(e){try{return this.db.prepare(`SELECT DISTINCT session_id
138
+ FROM session_events WHERE session_id = ?`)}insertEvent(e,t,r="PostToolUse",s,a){let i=S("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),T=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,p=R(a?.bytesAvoided),N=R(a?.bytesReturned),L=this.db.transaction(()=>{if(this.stmt(o.checkDuplicate).get(e,X,t.type,i))return;this.stmt(o.getEventCount).get(e).cnt>=W&&this.stmt(o.evictLowestPriority).run(e),this.stmt(o.insertEvent).run(e,t.type,t.category,t.priority,t.data,c,d,T,p,N,r,i),this.stmt(o.updateMetaLastEvent).run(e)});this.withRetry(()=>L())}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 T=S("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),p=s?.[u],N=String(p?.projectDir??d.project_dir??this._getSessionProjectDir(e)??"").trim(),L=String(p?.source??d.attribution_source??"unknown"),O=Number(p?.confidence??d.attribution_confidence??0),U=Number.isFinite(O)?Math.max(0,Math.min(1,O)):0,M=a?.[u],re=R(M?.bytesAvoided),se=R(M?.bytesReturned);return{event:d,dataHash:T,projectDir:N,attributionSource:L,attributionConfidence:U,bytesAvoided:re,bytesReturned:se}}),c=this.db.transaction(()=>{let d=this.stmt(o.getEventCount).get(e).cnt;for(let u of i)this.stmt(o.checkDuplicate).get(e,X,u.event.type,u.dataHash)||(d>=W?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{return""}}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{return[]}}getSessionIdsForProject(e){try{return this.db.prepare(`SELECT DISTINCT session_id
122
139
  FROM session_events
123
- WHERE project_dir = ?`).all(e).map(r=>r.session_id)}catch{return[]}}ensureSession(e,t){this.stmt(o.ensureSession).run(e,t)}getSessionStats(e){return this.stmt(o.getSessionStats).get(e)??null}incrementCompactCount(e){this.stmt(o.incrementCompactCount).run(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{return 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{}}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{return{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{$ as SessionDB,h as StorageDirectoryError,Ze as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Qe as clearStorageDirectoryCheckCacheForTests,ze as describeStorageDirectorySource,nt as ensureSessionEventsSchema,Je as ensureWritableStorageDir,Ke as formatStorageDirectoryError,Ie as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,T as normalizeWorktreePath,Ye as resolveContentStorageDir,et as resolveContentStorePath,Ge as resolveDefaultSessionDir,tt as resolveSessionDbPath,xe as resolveSessionPath,Q as resolveSessionStorageDir,qe as resolveStatsStorageDir};
140
+ WHERE project_dir = ?`).all(e).map(r=>r.session_id)}catch{return[]}}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.getSessionStats(e),a=(t?.tool_calls??0)>0?t?.unique_files??0:0,i=t?.errors??0,c=Math.min(a,i);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,edit_test_cycles:c,duration_min:t?.duration_min??0,compact_count:s?.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)}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{return 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{}}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{return{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{$ as SessionDB,f as StorageDirectoryError,Ze as _resetWorktreeSuffixCacheForTests,ne as applyMissingSessionEventsColumns,Qe as clearStorageDirectoryCheckCacheForTests,ze as describeStorageDirectorySource,nt as ensureSessionEventsSchema,Je as ensureWritableStorageDir,Ke as formatStorageDirectoryError,xe as getWorktreeSuffix,te as hashProjectDirCanonical,ee as hashProjectDirLegacy,h as normalizeWorktreePath,Ge as resolveContentStorageDir,et as resolveContentStorePath,qe as resolveDefaultSessionDir,tt as resolveSessionDbPath,Ie as resolveSessionPath,Q as resolveSessionStorageDir,Ye as resolveStatsStorageDir};
@@ -6,9 +6,10 @@
6
6
  * Fallback: if bundles are missing (marketplace installs), try build/session/*.js.
7
7
  */
8
8
 
9
- import { join } from "node:path";
9
+ import { join, resolve as resolvePath } from "node:path";
10
10
  import { pathToFileURL } from "node:url";
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
12
13
 
13
14
  import { hasPlatformConfig, maybeForward } from "./platform-bridge.mjs";
14
15
  import { detectPlatformFromEnv } from "./core/platform-detect.mjs";
@@ -106,13 +107,316 @@ export function attributeAndInsertEvents(db, sessionId, events, input, projectDi
106
107
  // the unconfigured-user path costs at most one syscall per minute.
107
108
  if (hasPlatformConfig()) {
108
109
  const platform = detectPlatformFromEnv();
110
+ // Session-wide rollup snapshot — stamped onto every outgoing event so
111
+ // the analytics engine sees the seed.ts shape (tool_calls, errors,
112
+ // unique_tools, ...). Defensive call: older SessionDB bundles that
113
+ // predate v1.0.158 won't have getSessionRollup; fall back to null
114
+ // and the bridge will still pass the per-event facts through.
115
+ const rollup = typeof db.getSessionRollup === "function"
116
+ ? db.getSessionRollup(sessionId)
117
+ : null;
118
+
119
+ // v1.0.159: Bash metadata shared across all events from this hook fire.
120
+ // A single Bash tool call may emit multiple canonical events (a `git
121
+ // pull` produces type=git AND type=cwd) — they all share the same
122
+ // command_type / command_tool / exit_code / duration_bucket. Hook
123
+ // metadata (latency, exit_code) is also per-call, not per-event.
124
+ const bashMeta = deriveBashMetadata(input);
125
+ // v1.0.159: latency_ms read from the PreToolUse timestamp stamp.
126
+ // PreToolUse writes ms-precision Date.now() to a tmp file, PostToolUse
127
+ // reads + computes delta + cleans up. Failure → undefined (no field
128
+ // surfaces on the wire; Zod is optional).
129
+ const latencyMs = readLatencyMs(sessionId, input?.tool_name);
130
+
109
131
  for (let i = 0; i < events.length; i++) {
110
- maybeForward(
111
- { ...events[i], ...attributions[i], session_id: sessionId },
112
- platform,
113
- );
132
+ const enriched = enrichEventForPlatform(events[i], attributions[i]);
133
+ const withBash = bashMeta ? { ...enriched, ...bashMeta } : enriched;
134
+ const withLatency = latencyMs !== undefined
135
+ ? { ...withBash, latency_ms: latencyMs, duration_bucket: bucketizeDuration(latencyMs) }
136
+ : withBash;
137
+ const payload = rollup ? { ...withLatency, ...rollup } : withLatency;
138
+ maybeForward({ ...payload, session_id: sessionId }, platform);
114
139
  }
115
140
  }
116
141
 
117
142
  return attributions;
118
143
  }
144
+
145
+ // ── Per-event enrichment (seed.ts shape parity) ──────────────────────────
146
+ //
147
+ // Each canonical event from session-extract carries only {type, category, data}.
148
+ // The platform's events table has 35 columns; the engine's aggregate SQL reads
149
+ // most of them. This helper derives the per-event-derivable subset directly
150
+ // from the event's own facts — no I/O, no classifier dependency, no allocation
151
+ // beyond the spread. Aggregates (tool_calls, errors, ...) come from the
152
+ // session rollup stamp in the caller.
153
+ //
154
+ // PRD-context-as-a-service §5.4 ABI: bridge stays a dumb pipe. This enrichment
155
+ // runs BEFORE maybeForward so the body envelope spreads the enriched event
156
+ // unchanged.
157
+ function enrichEventForPlatform(event, attribution) {
158
+ const error = event?.category === "error" ? 1 : 0;
159
+ const dataStr = typeof event?.data === "string" ? event.data : "";
160
+
161
+ const enriched = {
162
+ ...event,
163
+ ...attribution,
164
+ error,
165
+ // session_* are open-string passthroughs (ADR-0001) — let the platform
166
+ // do forensic queries on the raw shape without forcing the wide→narrow
167
+ // category derivation to ever round-trip.
168
+ session_category: event?.category,
169
+ session_type: event?.type,
170
+ session_data: dataStr.length > 0 ? dataStr.slice(0, 500) : undefined,
171
+ };
172
+
173
+ // Error events: surface the message + classify
174
+ if (error === 1) {
175
+ enriched.error_message = dataStr.slice(0, 1000);
176
+ const cls = classifyError(dataStr);
177
+ enriched.error_category = cls.error_category;
178
+ enriched.error_tool = cls.error_tool;
179
+ }
180
+
181
+ // blocker_status: derive from the canonical event TYPE, not lexical
182
+ // pattern-matching on prose. session-extract already identifies blocker
183
+ // states semantically (type='blocker' when the agent signals stuck;
184
+ // type='blocker_resolved' on recovery). Regex on error_message would
185
+ // false-positive on the millions of error texts in the wild — we let
186
+ // the extractor's structural judgment be the source of truth.
187
+ if (event?.type === "blocker") enriched.blocker_status = "open";
188
+ else if (event?.type === "blocker_resolved") enriched.blocker_status = "resolved";
189
+
190
+ // Git events: surface commit message + mark has_commit at the event level
191
+ // (rollup-level has_commit comes from the session-wide stamp; both win
192
+ // when set — `{...enriched, ...rollup}` order keeps rollup authoritative
193
+ // for non-git events while git events stay marked).
194
+ if (event?.category === "git" && dataStr.length > 0) {
195
+ enriched.commit_message = dataStr.slice(0, 500);
196
+ enriched.has_commit = 1;
197
+ }
198
+
199
+ // File events: ship the file path as the single-item array shape the
200
+ // platform schema expects (Zod: z.array(z.string()).max(20))
201
+ if (event?.category === "file" && dataStr.length > 0) {
202
+ enriched.file_paths = [dataStr.slice(0, 500)];
203
+ }
204
+
205
+ return enriched;
206
+ }
207
+
208
+ // ── Inline error classifier — seed.ts ERROR_CATEGORIES parity ────────────
209
+ //
210
+ // Mirrors src/session/error-classifier.ts's 10-category table for runtime
211
+ // callers (this is a .mjs hook file; the TS classifier ships bundled but
212
+ // the bundle import path costs an extra ~20ms on first hook fire and an
213
+ // extra disk read per hook subprocess. Inline keeps the hot path fast.)
214
+ // If the table ever drifts from error-classifier.ts, the classifier test
215
+ // suite (tests/session/classifier.test.ts) is the canonical source — sync
216
+ // the patterns there first, then mirror here.
217
+ function classifyError(message) {
218
+ const m = String(message ?? "").toLowerCase();
219
+ if (!m) return { error_category: "unknown", error_tool: "Bash" };
220
+
221
+ // Order matters: timeout + git_conflict checked BEFORE test_failed so
222
+ // "test timed out" and "CONFLICT … fail" land in the right bucket.
223
+ if (/etimedout|timed out|timeout|deadline exceeded/.test(m)) return { error_category: "timeout", error_tool: "Bash" };
224
+ if (/conflict.*(merge|rebase|git)|merge conflict|^conflict/.test(m)) return { error_category: "git_conflict", error_tool: "Bash" };
225
+ if (/enoent|no such file|cannot find module|filenotfounderror/.test(m)) return { error_category: "file_not_found", error_tool: "Read" };
226
+ if (/command not found|: not found|exit code 127/.test(m)) return { error_category: "command_not_found", error_tool: "Bash" };
227
+ if (/old_string|could not find string|matches multiple/.test(m)) return { error_category: "edit_match_failed", error_tool: "Edit" };
228
+ if (/eacces|permission denied|operation not permitted|eperm/.test(m)) return { error_category: "permission_denied", error_tool: "Bash" };
229
+ if (/syntaxerror|error ts\d+|unexpected token|parse error/.test(m)) return { error_category: "syntax_error", error_tool: "Bash" };
230
+ if (/typeerror|referenceerror|rangeerror|traceback|nullpointer/.test(m)) return { error_category: "runtime_error", error_tool: "Bash" };
231
+ if (/test failed|fail |tests failed|assertion/.test(m)) return { error_category: "test_failed", error_tool: "Bash" };
232
+ return { error_category: "unknown", error_tool: "Bash" };
233
+ }
234
+
235
+ // ── Bash metadata derivation — algorithmic, not enumerative ──────────────
236
+ //
237
+ // A single Bash tool call may emit MULTIPLE canonical events (a `git pull`
238
+ // produces type='git' AND type='cwd'). The platform's command_metadata
239
+ // describes the BASH CALL, not the per-event derivative — so all events
240
+ // from one PostToolUse fire carry the same shape. Non-Bash tool calls
241
+ // return null and the per-event fields stay undefined (Zod optional drops
242
+ // them silently — no NULL noise on the wire).
243
+ //
244
+ // DESIGN: tool ecosystems contain millions of CLI binaries but converge on
245
+ // a tiny canonical verb set (test/build/install/lint/format/run/start/
246
+ // deploy/...). The classifier scans for these verbs at canonical token
247
+ // positions — agnostic of which package manager / language / framework.
248
+ // New tools without a registry change automatically classify correctly as
249
+ // long as they use the verbs (which is the dominant ecosystem convention).
250
+ // This was originally regex-table enumeration; the table never converges.
251
+ const CANONICAL_VERBS = new Set([
252
+ "test", "build", "install", "lint", "format", "run", "start",
253
+ "deploy", "compile", "bundle", "watch", "serve", "publish",
254
+ ]);
255
+ // Runners that wrap the actual executable — strip them so command_tool
256
+ // reflects the real binary the user invoked (`bunx pytest` → "pytest",
257
+ // not "bunx"). NODE_ENV=production npm run build → "npm".
258
+ const COMMAND_RUNNERS = new Set([
259
+ "sudo", "doas", "env", "exec", "time",
260
+ "npx", "pnpx", "bunx", "pnpm", "yarn", "bun",
261
+ ]);
262
+ const ENV_ASSIGN_RE = /^[A-Z_][A-Z0-9_]*=/;
263
+
264
+ // Tools whose NAME directly implies their type (no subcommand needed).
265
+ // Curated minimum — covers the dominant test/lint/format/build/db/http/
266
+ // deploy invocations across ecosystems. New ecosystem tools land in
267
+ // "other" until added — preferred to a noisy heuristic that misclassifies.
268
+ // Lookup is O(1); contrast with the original regex-table approach which
269
+ // scaled to no boundary and still missed unknowns.
270
+ const CANONICAL_TOOLS = new Map([
271
+ // test runners
272
+ ["pytest", "test"], ["jest", "test"], ["vitest", "test"], ["mocha", "test"],
273
+ ["ava", "test"], ["jasmine", "test"], ["rspec", "test"], ["junit", "test"],
274
+ ["tap", "test"], ["karma", "test"],
275
+ // linters
276
+ ["eslint", "lint"], ["tslint", "lint"], ["ruff", "lint"], ["rubocop", "lint"],
277
+ ["pylint", "lint"], ["flake8", "lint"], ["clippy", "lint"], ["staticcheck", "lint"],
278
+ ["mypy", "lint"], ["shellcheck", "lint"],
279
+ // formatters
280
+ ["prettier", "format"], ["black", "format"], ["gofmt", "format"], ["rustfmt", "format"],
281
+ ["autopep8", "format"], ["yapf", "format"],
282
+ // bundlers / builders
283
+ ["webpack", "build"], ["vite", "build"], ["rollup", "build"], ["esbuild", "build"],
284
+ ["parcel", "build"], ["tsc", "build"], ["swc", "build"], ["turbo", "build"],
285
+ // deploy / infra
286
+ ["docker", "deploy"], ["kubectl", "deploy"], ["terraform", "deploy"], ["pulumi", "deploy"],
287
+ ["ansible", "deploy"], ["helm", "deploy"], ["aws", "deploy"], ["gcloud", "deploy"], ["az", "deploy"],
288
+ // databases
289
+ ["psql", "database"], ["mysql", "database"], ["sqlite3", "database"],
290
+ ["redis-cli", "database"], ["mongosh", "database"], ["mongo", "database"],
291
+ // http
292
+ ["curl", "http"], ["wget", "http"], ["httpie", "http"], ["http", "http"],
293
+ ]);
294
+
295
+ function deriveBashMetadata(input) {
296
+ if (input?.tool_name !== "Bash") return null;
297
+ const cmd = String(input?.tool_input?.command ?? "").trim();
298
+ if (!cmd) return { command_type: "other", command_tool: "Bash" };
299
+
300
+ const tokens = cmd.split(/\s+/);
301
+ const command_tool = extractCommandTool(tokens);
302
+ const command_type = classifyCommandType(tokens, command_tool);
303
+ const exit_code = inferExitCode(input?.tool_response);
304
+ return { command_type, command_tool, exit_code };
305
+ }
306
+
307
+ // Strip env-assign prefixes (`FOO=bar`), then strip runner shells,
308
+ // then return the basename of the executable token.
309
+ function extractCommandTool(tokens) {
310
+ let i = 0;
311
+ // Skip env assignments
312
+ while (i < tokens.length && ENV_ASSIGN_RE.test(tokens[i])) i++;
313
+ // Skip runner shells
314
+ while (i < tokens.length && COMMAND_RUNNERS.has(tokens[i].toLowerCase())) {
315
+ i++;
316
+ // Skip subcommands like `pnpm dlx`, `pnpm exec`, `bun run`
317
+ if (i < tokens.length && /^(dlx|exec|run|x)$/i.test(tokens[i])) i++;
318
+ }
319
+ if (i >= tokens.length) return tokens[0] || "Bash";
320
+ const exe = tokens[i];
321
+ // basename of path-like executables (`/usr/local/bin/foo` → "foo")
322
+ const base = exe.split(/[/\\]/).pop() || "Bash";
323
+ // Strip shell quoting if present
324
+ return base.replace(/^['"]|['"]$/g, "");
325
+ }
326
+
327
+ // Type classification — priority order:
328
+ // 1. Tool name implies type (curated CANONICAL_TOOLS map)
329
+ // 2. Canonical verb at subcommand position (`npm test`, `cargo build`)
330
+ // 3. Argument-shape heuristics (test/ dir, .test.ts suffix, --prod flag)
331
+ // 4. Tool-level fallback (git → git, make → build)
332
+ // 5. "other"
333
+ function classifyCommandType(tokens, command_tool) {
334
+ const toolLc = (command_tool || "").toLowerCase();
335
+
336
+ // 1. Tool name itself names the type
337
+ const fromTool = CANONICAL_TOOLS.get(toolLc);
338
+ if (fromTool) return fromTool;
339
+
340
+ // Skip env + runners to find subcommand position
341
+ const lower = tokens.map((t) => t.toLowerCase());
342
+ let start = 0;
343
+ while (start < lower.length && ENV_ASSIGN_RE.test(tokens[start])) start++;
344
+ while (start < lower.length && COMMAND_RUNNERS.has(lower[start])) {
345
+ start++;
346
+ if (start < lower.length && /^(dlx|exec|run|x)$/.test(lower[start])) start++;
347
+ }
348
+
349
+ // 2. Canonical verb scan within next 4 tokens
350
+ const horizon = Math.min(lower.length, start + 4);
351
+ for (let i = start; i < horizon; i++) {
352
+ if (CANONICAL_VERBS.has(lower[i])) return lower[i];
353
+ }
354
+
355
+ // 3. Argument-shape heuristics
356
+ const tail = tokens.slice(start).join(" ");
357
+ if (/\btests?[/\\]|\bspec[/\\]|__tests__|\.(test|spec)\.[mc]?[jt]sx?\b|test_[\w-]+\.py\b|_test\.go\b/.test(tail)) return "test";
358
+ if (/--(prod|production|release|optimize)\b/.test(tail)) return "build";
359
+ if (/\bDockerfile\b|docker-compose/.test(tail)) return "deploy";
360
+
361
+ // 4. Tool-level fallback for tools whose mere presence implies the type
362
+ if (toolLc === "git") return "git";
363
+ if (toolLc === "make" || toolLc === "ninja" || toolLc === "cmake") return "build";
364
+
365
+ return "other";
366
+ }
367
+
368
+ // Exit code best-effort inference from tool_response. Hook stdin does
369
+ // not carry the actual exit code on CC; we read the shape of the output
370
+ // for signals. Engine treats exit_code as soft signal (Anomaly #3 — no
371
+ // pattern in patterns.ts reads it today), so probabilistic stamps are
372
+ // adequate. Captures named exit code when explicit.
373
+ function inferExitCode(response) {
374
+ const r = String(response ?? "");
375
+ if (!r) return 0;
376
+ // Explicit exit-code marker (some wrappers emit "exit status 137" etc.)
377
+ const explicit = r.match(/\bexit (?:status|code)\s+(\d+)\b/i);
378
+ if (explicit) return Number(explicit[1]);
379
+ // "command not found" → POSIX standard 127
380
+ if (/^bash:.*: (?:command not found|No such file)/m.test(r)) return 127;
381
+ // Heuristic non-zero indicators (line-anchored to avoid false positives
382
+ // inside narrative text from successful commands).
383
+ if (/^(?:Error: |Traceback|FAIL\b|✗|✘)/m.test(r)) return 1;
384
+ return 0;
385
+ }
386
+
387
+ // ── Latency timing — reads PreToolUse marker ────────────────────────────
388
+ //
389
+ // PreToolUse already writes `${tmpdir}/context-mode-latency-${sessionId}-
390
+ // ${toolName}.txt` with the start timestamp (pretooluse.mjs:177). We
391
+ // piggyback on that marker — read + compute delta, do NOT unlink (the
392
+ // downstream slow-tool event emission in posttooluse.mjs:128-152 manages
393
+ // the unlink lifecycle). Failure modes (missing marker, parse error,
394
+ // negative delta, sanity-out-of-range) all return undefined — Zod's
395
+ // optional handling drops the field silently. No NULL noise on the wire.
396
+ function readLatencyMs(sessionId, toolName) {
397
+ if (!sessionId || !toolName) return undefined;
398
+ const markerPath = resolvePath(
399
+ tmpdir(),
400
+ `context-mode-latency-${sessionId}-${toolName}.txt`,
401
+ );
402
+ try {
403
+ const start = parseInt(readFileSync(markerPath, "utf8").trim(), 10);
404
+ if (!Number.isFinite(start) || start <= 0) return undefined;
405
+ const delta = Date.now() - start;
406
+ if (delta < 0 || delta > 24 * 3600 * 1000) return undefined;
407
+ return delta;
408
+ } catch {
409
+ return undefined;
410
+ }
411
+ }
412
+
413
+ // ── Duration bucket ──────────────────────────────────────────────────────
414
+ //
415
+ // Open-string label the platform Zod schema accepts (max 20 chars). Three
416
+ // buckets cover the seed.ts shape: <5s | 5-30s | 30s+.
417
+ function bucketizeDuration(ms) {
418
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) return undefined;
419
+ if (ms < 5_000) return "<5s";
420
+ if (ms < 30_000) return "5-30s";
421
+ return "30s+";
422
+ }
@@ -30,6 +30,7 @@ await runHook(async () => {
30
30
  readStdin,
31
31
  parseStdin,
32
32
  getSessionId,
33
+ getInputProjectDir,
33
34
  getSessionDBPath,
34
35
  getSessionEventsPath,
35
36
  getCleanupFlagPath,
@@ -38,7 +39,7 @@ await runHook(async () => {
38
39
  const { writeSessionEventsFile, buildSessionDirective, getSessionEvents } = await import(
39
40
  "./session-directive.mjs"
40
41
  );
41
- const { createSessionLoaders } = await import("./session-loaders.mjs");
42
+ const { createSessionLoaders, attributeAndInsertEvents } = await import("./session-loaders.mjs");
42
43
  const { join, dirname } = await import("node:path");
43
44
  const { fileURLToPath } = await import("node:url");
44
45
  const { readFileSync, unlinkSync, readdirSync, rmSync, lstatSync } = await import("node:fs");
@@ -49,7 +50,40 @@ await runHook(async () => {
49
50
 
50
51
  // Resolve absolute path for imports (fileURLToPath for Windows compat)
51
52
  const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
52
- const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
53
+ const { loadSessionDB, loadProjectAttribution } = createSessionLoaders(HOOK_DIR);
54
+
55
+ // Emit a `session_start` canonical event at the boundary of each session
56
+ // lifecycle transition (startup / resume / compact). The platform's insight
57
+ // engine joins on `category='session_start'` to compute per-session
58
+ // aggregates (~60 of 180 patterns depend on this anchor row). Bridge
59
+ // forwards via attributeAndInsertEvents which also stamps the rollup
60
+ // snapshot — safe for the FIRST event of a fresh session.
61
+ async function emitSessionStartLifecycle(db, sessionId, source, projectDir, input) {
62
+ try {
63
+ const { resolveProjectAttributions } = await loadProjectAttribution();
64
+ const lifecycleEvent = {
65
+ type: "session_start",
66
+ category: "session_start",
67
+ data: JSON.stringify({
68
+ source,
69
+ project_dir: projectDir,
70
+ started_at: Math.floor(Date.now() / 1000),
71
+ }),
72
+ priority: 1,
73
+ };
74
+ attributeAndInsertEvents(
75
+ db,
76
+ sessionId,
77
+ [lifecycleEvent],
78
+ input,
79
+ projectDir,
80
+ "SessionStart",
81
+ resolveProjectAttributions,
82
+ );
83
+ } catch {
84
+ // Best-effort — lifecycle emission failure MUST NOT block session start.
85
+ }
86
+ }
53
87
 
54
88
  // Self-heal a partial plugin cache install before anything else
55
89
  // touches the cache dir. The Algo-D4 boot gate and the #604
@@ -202,6 +236,13 @@ await runHook(async () => {
202
236
  } catch { /* best-effort */ }
203
237
  }
204
238
 
239
+ // Emit lifecycle anchor BEFORE close — engine joins on
240
+ // category='session_start' to compute per-session aggregates.
241
+ // Cross-platform projectDir via getInputProjectDir (covers cursor's
242
+ // workspace_roots[], codex/gemini/qwen's *_PROJECT_DIR env vars,
243
+ // CC's CLAUDE_PROJECT_DIR, falls back to input.cwd and process.cwd).
244
+ const projectDirCompact = getInputProjectDir(input);
245
+ await emitSessionStartLifecycle(db, sessionId, "compact", projectDirCompact, input);
205
246
  db.close();
206
247
  } else if (source === "resume") {
207
248
  // User invoked --continue, --resume, or /resume — clear cleanup flag so
@@ -234,6 +275,10 @@ await runHook(async () => {
234
275
  }
235
276
  }
236
277
 
278
+ const projectDirResume = getInputProjectDir(input);
279
+ if (sessionId) {
280
+ await emitSessionStartLifecycle(db, sessionId, "resume", projectDirResume, input);
281
+ }
237
282
  db.close();
238
283
  } else if (source === "startup") {
239
284
  // Fresh session (no --continue) — clean slate, capture CLAUDE.md rules.
@@ -294,6 +339,11 @@ await runHook(async () => {
294
339
  } catch { /* file doesn't exist — skip */ }
295
340
  }
296
341
 
342
+ // Lifecycle anchor for a fresh session — emits BEFORE the CLAUDE.md
343
+ // rule events have been forwarded so the `session_start` row lands
344
+ // as the very first row the platform sees for this session.
345
+ await emitSessionStartLifecycle(db, sessionId, "startup", projectDir, input);
346
+
297
347
  db.close();
298
348
 
299
349
  // Age-gated lazy cleanup of old plugin cache version dirs (#181).
@@ -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.157",
6
+ "version": "1.0.159",
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.157",
3
+ "version": "1.0.159",
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",