From 69b17e15a20728a4c50054e870c32643b184e1ba Mon Sep 17 00:00:00 2001 From: Dmytro Gaivoronsky Date: Sun, 30 Nov 2025 14:28:07 -0800 Subject: [PATCH] feat: Auto-detect and rebuild native modules on Node.js version changes (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements three-layer defense against native module version mismatches: Layer 1: Node.js Version Tracking - Track Node.js version alongside package version in .install-version marker - Auto-trigger npm install when Node.js version changes - Backward compatible with old plain-text version marker format Layer 2: Native Module Verification - Add verifyNativeModules() function to test better-sqlite3 loads correctly - Verify after install completes to catch corrupted builds - Retry with force flag if initial install verification fails Layer 3: Graceful Failure - Catch ERR_DLOPEN_FAILED in context-hook and delete version marker - Exit cleanly to avoid error spam in Claude Code UI - Auto-fix on next session start Changes: - scripts/smart-install.js: Add Node.js version tracking and verification - src/hooks/context-hook.ts: Add graceful failure handling for native module errors - tests/smart-install.test.js: Add tests for version marker format compatibility - plugin/scripts/context-hook.js: Built output from TypeScript source Fixes the issue where users see ERR_DLOPEN_FAILED errors after Node.js upgrades, requiring manual npm install. Now automatically detects and fixes the issue. Related design doc: docs/context/native-module-auto-fix-design.md Implementation plan: docs/context/native-module-auto-fix-implementation.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Co-authored-by: Alex Newman --- package-lock.json | 4 + plugin/scripts/context-hook.js | 38 +++++----- scripts/smart-install.js | 135 +++++++++++++++++++++++++++++---- src/hooks/context-hook.ts | 34 ++++++++- tests/smart-install.test.js | 47 ++++++++++++ 5 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 tests/smart-install.test.js diff --git a/package-lock.json b/package-lock.json index ab1b4b1e..ddcc89bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1484,6 +1484,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2337,6 +2338,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3849,6 +3851,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4850,6 +4853,7 @@ "node_modules/zod": { "version": "3.25.76", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index 0e822be6..a4ad9844 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import w from"path";import{homedir as le}from"os";import{existsSync as _e,readFileSync as Ee}from"fs";import{stdin as G}from"process";import me from"better-sqlite3";import{join as S,dirname as de,basename as Le}from"path";import{homedir as V}from"os";import{existsSync as Ce,mkdirSync as ce}from"fs";import{fileURLToPath as pe}from"url";function ue(){return typeof __dirname<"u"?__dirname:de(pe(import.meta.url))}var xe=ue(),L=process.env.CLAUDE_MEM_DATA_DIR||S(V(),".claude-mem"),B=process.env.CLAUDE_CONFIG_DIR||S(V(),".claude"),$e=S(L,"archives"),Ue=S(L,"logs"),Me=S(L,"trash"),we=S(L,"backups"),Fe=S(L,"settings.json"),q=S(L,"claude-mem.db"),Xe=S(L,"vector-db"),Be=S(B,"settings.json"),je=S(B,"commands"),He=S(B,"CLAUDE.md");function J(c){ce(c,{recursive:!0})}var j=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(j||{}),H=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=j[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} -${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e0&&(f=` {${Object.entries(p).map(([x,A])=>`${x}=${A}`).join(", ")}}`)}let b=`[${a}] [${d}] [${m}] ${g}${t}${f}${n}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},Q=new H;var U=class{db;constructor(){J(L),this.db=new me(q),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` +import x from"path";import{homedir as me}from"os";import{existsSync as _e,readFileSync as Ee,unlinkSync as Te}from"fs";import{stdin as P}from"process";import{fileURLToPath as he}from"url";import{dirname as ge}from"path";import le from"better-sqlite3";import{join as f,dirname as de,basename as xe}from"path";import{homedir as V}from"os";import{existsSync as we,mkdirSync as ce}from"fs";import{fileURLToPath as pe}from"url";function ue(){return typeof __dirname<"u"?__dirname:de(pe(import.meta.url))}var Xe=ue(),L=process.env.CLAUDE_MEM_DATA_DIR||f(V(),".claude-mem"),B=process.env.CLAUDE_CONFIG_DIR||f(V(),".claude"),Be=f(L,"archives"),je=f(L,"logs"),He=f(L,"trash"),Pe=f(L,"backups"),Ge=f(L,"settings.json"),q=f(L,"claude-mem.db"),We=f(L,"vector-db"),Ye=f(B,"settings.json"),Ke=f(B,"commands"),Ve=f(B,"CLAUDE.md");function J(c){ce(c,{recursive:!0})}var j=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(j||{}),H=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=j[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} +${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,i){if(e0&&(b=` {${Object.entries(p).map(([$,A])=>`${$}=${A}`).join(", ")}}`)}let S=`[${a}] [${d}] [${l}] ${g}${t}${b}${n}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},Q=new H;var M=class{db;constructor(){J(L),this.db=new le(q),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project); CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC); - `),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(m=>m.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(m=>m.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(m=>m.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(` + `),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(l=>l.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(l=>l.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(l=>l.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(` CREATE TABLE session_summaries_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, sdk_session_id TEXT NOT NULL, @@ -262,7 +262,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje SELECT files_read, files_modified FROM observations WHERE sdk_session_id = ? - `).all(e),r=new Set,i=new Set;for(let a of t){if(a.files_read)try{let d=JSON.parse(a.files_read);Array.isArray(d)&&d.forEach(m=>r.add(m))}catch{}if(a.files_modified)try{let d=JSON.parse(a.files_modified);Array.isArray(d)&&d.forEach(m=>i.add(m))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(` + `).all(e),r=new Set,i=new Set;for(let a of t){if(a.files_read)try{let d=JSON.parse(a.files_read);Array.isArray(d)&&d.forEach(l=>r.add(l))}catch{}if(a.files_modified)try{let d=JSON.parse(a.files_modified);Array.isArray(d)&&d.forEach(l=>i.add(l))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(i)}}getSessionById(e){return this.db.prepare(` SELECT id, claude_session_id, sdk_session_id, project, user_prompt FROM sdk_sessions WHERE id = ? @@ -322,23 +322,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,a.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let f=this.db.prepare(` + `).run(e,e,s,a.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let b=this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i,a.toISOString(),d);return{id:Number(f.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r,i=0){let a=new Date,d=a.getTime();this.db.prepare(` + `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,i,a.toISOString(),d);return{id:Number(b.lastInsertRowid),createdAtEpoch:d}}storeSummary(e,s,t,r,i=0){let a=new Date,d=a.getTime();this.db.prepare(` SELECT id FROM sdk_sessions WHERE sdk_session_id = ? `).get(e)||(this.db.prepare(` INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,a.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let f=this.db.prepare(` + `).run(e,e,s,a.toISOString(),d),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let b=this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i,a.toISOString(),d);return{id:Number(f.lastInsertRowid),createdAtEpoch:d}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` + `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,i,a.toISOString(),d);return{id:Number(b.lastInsertRowid),createdAtEpoch:d}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? @@ -361,7 +361,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE up.id IN (${d}) ORDER BY up.created_at_epoch ${i} ${a} - `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let a=i?"AND project = ?":"",d=i?[i]:[],m,g;if(e!==null){let T=` + `).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,i){let a=i?"AND project = ?":"",d=i?[i]:[],l,g;if(e!==null){let T=` SELECT id, created_at_epoch FROM observations WHERE id <= ? ${a} @@ -373,7 +373,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE id >= ? ${a} ORDER BY id ASC LIMIT ? - `;try{let l=this.db.prepare(T).all(e,...d,t+1),p=this.db.prepare(R).all(e,...d,r+1);if(l.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};m=l.length>0?l[l.length-1].created_at_epoch:s,g=p.length>0?p[p.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary observations:",l.message),{observations:[],sessions:[],prompts:[]}}}else{let T=` + `;try{let m=this.db.prepare(T).all(e,...d,t+1),p=this.db.prepare(R).all(e,...d,r+1);if(m.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};l=m.length>0?m[m.length-1].created_at_epoch:s,g=p.length>0?p[p.length-1].created_at_epoch:s}catch(m){return console.error("[SessionStore] Error getting boundary observations:",m.message),{observations:[],sessions:[],prompts:[]}}}else{let T=` SELECT created_at_epoch FROM observations WHERE created_at_epoch <= ? ${a} @@ -385,23 +385,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE created_at_epoch >= ? ${a} ORDER BY created_at_epoch ASC LIMIT ? - `;try{let l=this.db.prepare(T).all(s,...d,t),p=this.db.prepare(R).all(s,...d,r+1);if(l.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};m=l.length>0?l[l.length-1].created_at_epoch:s,g=p.length>0?p[p.length-1].created_at_epoch:s}catch(l){return console.error("[SessionStore] Error getting boundary timestamps:",l.message),{observations:[],sessions:[],prompts:[]}}}let n=` + `;try{let m=this.db.prepare(T).all(s,...d,t),p=this.db.prepare(R).all(s,...d,r+1);if(m.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};l=m.length>0?m[m.length-1].created_at_epoch:s,g=p.length>0?p[p.length-1].created_at_epoch:s}catch(m){return console.error("[SessionStore] Error getting boundary timestamps:",m.message),{observations:[],sessions:[],prompts:[]}}}let n=` SELECT * FROM observations WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${a} ORDER BY created_at_epoch ASC - `,f=` + `,b=` SELECT * FROM session_summaries WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${a} ORDER BY created_at_epoch ASC - `,b=` + `,S=` SELECT up.*, s.project, s.sdk_session_id FROM user_prompts up JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${a.replace("project","s.project")} ORDER BY up.created_at_epoch ASC - `;try{let T=this.db.prepare(n).all(m,g,...d),R=this.db.prepare(f).all(m,g,...d),l=this.db.prepare(b).all(m,g,...d);return{observations:T,sessions:R.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:l.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function Te(){try{let c=w.join(le(),".claude","settings.json");if(_e(c)){let e=JSON.parse(Ee(c,"utf-8"));if(e.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let s=parseInt(e.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(!isNaN(s)&&s>0)return s}}}catch{}return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10)}var he=Te(),z=10,Z=4,ge=1,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function be(c){if(!c)return[];try{let e=JSON.parse(c);return Array.isArray(e)?e:[]}catch{return[]}}function Se(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function fe(c){return new Date(c).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function Re(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ne(c,e){return w.isAbsolute(c)?w.relative(e,c):c}function M(c,e,s,t){return e?t?[`${s}${c}:${o.reset} ${e}`,""]:[`**${c}**: ${e}`,""]:[]}async function ee(c,e=!1){let s=c?.cwd??process.cwd(),t=s?w.basename(s):"unknown-project",r=new U,i=r.db.prepare(` + `;try{let T=this.db.prepare(n).all(l,g,...d),R=this.db.prepare(b).all(l,g,...d),m=this.db.prepare(S).all(l,g,...d);return{observations:T,sessions:R.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:m.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};var be=he(import.meta.url),Se=ge(be),fe=x.join(Se,"../../.install-version");function Re(){try{let c=x.join(me(),".claude","settings.json");if(_e(c)){let e=JSON.parse(Ee(c,"utf-8"));if(e.env?.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let s=parseInt(e.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(!isNaN(s)&&s>0)return s}}}catch{}return parseInt(process.env.CLAUDE_MEM_CONTEXT_OBSERVATIONS||"50",10)}var Ne=Re(),z=10,Z=4,Oe=1,o={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",gray:"\x1B[90m",red:"\x1B[31m"};function Ie(c){if(!c)return[];try{let e=JSON.parse(c);return Array.isArray(e)?e:[]}catch{return[]}}function ye(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function Le(c){return new Date(c).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function ve(c){return new Date(c).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ae(c,e){return x.isAbsolute(c)?x.relative(e,c):c}function w(c,e,s,t){return e?t?[`${s}${c}:${o.reset} ${e}`,""]:[`**${c}**: ${e}`,""]:[]}async function ee(c,e=!1){let s=c?.cwd??process.cwd(),t=s?x.basename(s):"unknown-project",r;try{r=new M}catch(b){if(b.code==="ERR_DLOPEN_FAILED"){try{Te(fe)}catch{}console.error("\u26A0\uFE0F Native module rebuild needed - restart Claude Code to auto-fix"),console.error(" (This happens after Node.js version upgrades)"),process.exit(0)}throw b}let i=r.db.prepare(` SELECT id, sdk_session_id, type, title, subtitle, narrative, facts, concepts, files_read, files_modified, discovery_tokens, @@ -410,18 +410,18 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(t,he),a=r.db.prepare(` + `).all(t,Ne),a=r.db.prepare(` SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch FROM session_summaries WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ? - `).all(t,z+ge);if(i.length===0&&a.length===0)return r.close(),e?` + `).all(t,z+Oe);if(i.length===0&&a.length===0)return r.close(),e?` ${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset} ${o.gray}${"\u2500".repeat(60)}${o.reset} ${o.dim}No previous sessions found for this project yet.${o.reset} `:`# [${t}] recent context -No previous sessions found for this project yet.`;let d=i,m=a.slice(0,z),g=d,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${t}] recent context`),n.push("")),g.length>0){e?n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision${o.reset}`):n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision"),n.push(""),e?(n.push(`${o.bright}\u{1F4A1} Column Key${o.reset}`),n.push(`${o.dim} Read: Tokens to read this observation (cost to learn it now)${o.reset}`),n.push(`${o.dim} Work: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)${o.reset}`)):(n.push("\u{1F4A1} **Column Key**:"),n.push("- **Read**: Tokens to read this observation (cost to learn it now)"),n.push("- **Work**: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)")),n.push(""),e?(n.push(`${o.dim}\u{1F4A1} Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${o.reset}`),n.push(""),n.push(`${o.dim}When you need implementation details, rationale, or debugging context:${o.reset}`),n.push(`${o.dim} - Use the mem-search skill to fetch full observations on-demand${o.reset}`),n.push(`${o.dim} - Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching${o.reset}`),n.push(`${o.dim} - Trust this index over re-reading code for past decisions and learnings${o.reset}`)):(n.push("\u{1F4A1} **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work."),n.push(""),n.push("When you need implementation details, rationale, or debugging context:"),n.push("- Use the mem-search skill to fetch full observations on-demand"),n.push("- Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching"),n.push("- Trust this index over re-reading code for past decisions and learnings")),n.push("");let f=d.length,b=d.reduce((u,_)=>{let h=(_.title?.length||0)+(_.subtitle?.length||0)+(_.narrative?.length||0)+JSON.stringify(_.facts||[]).length;return u+Math.ceil(h/Z)},0),T=d.reduce((u,_)=>u+(_.discovery_tokens||0),0),R=T-b,l=T>0?Math.round(R/T*100):0;e?(n.push(`${o.bright}${o.cyan}\u{1F4CA} Context Economics${o.reset}`),n.push(`${o.dim} Loading: ${f} observations (${b.toLocaleString()} tokens to read)${o.reset}`),n.push(`${o.dim} Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions${o.reset}`),T>0&&n.push(`${o.green} Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)${o.reset}`),n.push("")):(n.push("\u{1F4CA} **Context Economics**:"),n.push(`- Loading: ${f} observations (${b.toLocaleString()} tokens to read)`),n.push(`- Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions`),T>0&&n.push(`- Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)`),n.push(""));let p=a[0]?.id,P=m.map((u,_)=>{let h=_===0?null:a[_+1];return{...u,displayEpoch:h?h.created_at_epoch:u.created_at_epoch,displayTime:h?h.created_at:u.created_at,shouldShowLink:u.id!==p}}),x=[...g.map(u=>({type:"observation",data:u})),...P.map(u=>({type:"summary",data:u}))];x.sort((u,_)=>{let h=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,v=_.type==="observation"?_.data.created_at_epoch:_.data.displayEpoch;return h-v});let A=new Map;for(let u of x){let _=u.type==="observation"?u.data.created_at:u.data.displayTime,h=Re(_);A.has(h)||A.set(h,[]),A.get(h).push(u)}let se=Array.from(A.entries()).sort((u,_)=>{let h=new Date(u[0]).getTime(),v=new Date(_[0]).getTime();return h-v});for(let[u,_]of se){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let h=null,v="",D=!1;for(let F of _)if(F.type==="summary"){D&&(n.push(""),D=!1,h=null,v="");let E=F.data,C=`${E.request||"Session started"} (${Se(E.displayTime)})`,O=E.shouldShowLink?`claude-mem://session-summary/${E.id}`:"";if(e){let I=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${E.id}${o.reset} ${C} ${I}`)}else{let I=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${E.id}** ${C}${I}`)}n.push("")}else{let E=F.data,C=be(E.files_modified),O=C.length>0?Ne(C[0],s):"General";O!==h&&(D&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Read | Work |"),n.push("|----|------|---|-------|------|------|")),h=O,D=!0,v="");let I=fe(E.created_at),Y=E.title||"Untitled",y="\u2022";switch(E.type){case"bugfix":y="\u{1F534}";break;case"feature":y="\u{1F7E3}";break;case"refactor":y="\u{1F504}";break;case"change":y="\u2705";break;case"discovery":y="\u{1F535}";break;case"decision":y="\u2696\uFE0F";break;default:y="\u2022"}let te=(E.title?.length||0)+(E.subtitle?.length||0)+(E.narrative?.length||0)+JSON.stringify(E.facts||[]).length,X=Math.ceil(te/Z),$=E.discovery_tokens||0,k="\u{1F50D}";switch(E.type){case"discovery":k="\u{1F50D}";break;case"change":case"feature":case"bugfix":case"refactor":k="\u{1F6E0}\uFE0F";break;case"decision":k="\u2696\uFE0F";break}let re=$>0?`${k} ${$.toLocaleString()}`:"-",K=I!==v,ne=K?I:"";if(v=I,e){let oe=K?`${o.dim}${I}${o.reset}`:" ".repeat(I.length),ie=X>0?`${o.dim}(~${X}t)${o.reset}`:"",ae=$>0?`${o.dim}(${k} ${$.toLocaleString()}t)${o.reset}`:"";n.push(` ${o.dim}#${E.id}${o.reset} ${oe} ${y} ${Y} ${ie} ${ae}`)}else n.push(`| #${E.id} | ${ne||"\u2033"} | ${y} | ${Y} | ~${X} | ${re} |`)}D&&n.push("")}let N=a[0],W=d[0];if(N&&(N.investigated||N.learned||N.completed||N.next_steps)&&(!W||N.created_at_epoch>W.created_at_epoch)&&(n.push(...M("Investigated",N.investigated,o.blue,e)),n.push(...M("Learned",N.learned,o.yellow,e)),n.push(...M("Completed",N.completed,o.green,e)),n.push(...M("Next Steps",N.next_steps,o.magenta,e))),T>0&&R>0){let u=Math.round(T/1e3);n.push(""),e?n.push(`${o.dim}\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.${o.reset}`):n.push(`\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.`)}}return r.close(),n.join(` -`).trimEnd()}var Oe=process.argv.includes("--colors");if(G.isTTY||Oe)ee(void 0,!0).then(c=>{console.log(c),process.exit(0)});else{let c="";G.on("data",e=>c+=e),G.on("end",async()=>{let e=c.trim()?JSON.parse(c):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await ee(e,!1)}};console.log(JSON.stringify(t)),process.exit(0)})} +No previous sessions found for this project yet.`;let d=i,l=a.slice(0,z),g=d,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${t}] recent context`),n.push("")),g.length>0){e?n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision${o.reset}`):n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision"),n.push(""),e?(n.push(`${o.bright}\u{1F4A1} Column Key${o.reset}`),n.push(`${o.dim} Read: Tokens to read this observation (cost to learn it now)${o.reset}`),n.push(`${o.dim} Work: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)${o.reset}`)):(n.push("\u{1F4A1} **Column Key**:"),n.push("- **Read**: Tokens to read this observation (cost to learn it now)"),n.push("- **Work**: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)")),n.push(""),e?(n.push(`${o.dim}\u{1F4A1} Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${o.reset}`),n.push(""),n.push(`${o.dim}When you need implementation details, rationale, or debugging context:${o.reset}`),n.push(`${o.dim} - Use the mem-search skill to fetch full observations on-demand${o.reset}`),n.push(`${o.dim} - Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching${o.reset}`),n.push(`${o.dim} - Trust this index over re-reading code for past decisions and learnings${o.reset}`)):(n.push("\u{1F4A1} **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work."),n.push(""),n.push("When you need implementation details, rationale, or debugging context:"),n.push("- Use the mem-search skill to fetch full observations on-demand"),n.push("- Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching"),n.push("- Trust this index over re-reading code for past decisions and learnings")),n.push("");let b=d.length,S=d.reduce((u,_)=>{let h=(_.title?.length||0)+(_.subtitle?.length||0)+(_.narrative?.length||0)+JSON.stringify(_.facts||[]).length;return u+Math.ceil(h/Z)},0),T=d.reduce((u,_)=>u+(_.discovery_tokens||0),0),R=T-S,m=T>0?Math.round(R/T*100):0;e?(n.push(`${o.bright}${o.cyan}\u{1F4CA} Context Economics${o.reset}`),n.push(`${o.dim} Loading: ${b} observations (${S.toLocaleString()} tokens to read)${o.reset}`),n.push(`${o.dim} Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions${o.reset}`),T>0&&n.push(`${o.green} Your savings: ${R.toLocaleString()} tokens (${m}% reduction from reuse)${o.reset}`),n.push("")):(n.push("\u{1F4CA} **Context Economics**:"),n.push(`- Loading: ${b} observations (${S.toLocaleString()} tokens to read)`),n.push(`- Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions`),T>0&&n.push(`- Your savings: ${R.toLocaleString()} tokens (${m}% reduction from reuse)`),n.push(""));let p=a[0]?.id,G=l.map((u,_)=>{let h=_===0?null:a[_+1];return{...u,displayEpoch:h?h.created_at_epoch:u.created_at_epoch,displayTime:h?h.created_at:u.created_at,shouldShowLink:u.id!==p}}),$=[...g.map(u=>({type:"observation",data:u})),...G.map(u=>({type:"summary",data:u}))];$.sort((u,_)=>{let h=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,v=_.type==="observation"?_.data.created_at_epoch:_.data.displayEpoch;return h-v});let A=new Map;for(let u of $){let _=u.type==="observation"?u.data.created_at:u.data.displayTime,h=ve(_);A.has(h)||A.set(h,[]),A.get(h).push(u)}let se=Array.from(A.entries()).sort((u,_)=>{let h=new Date(u[0]).getTime(),v=new Date(_[0]).getTime();return h-v});for(let[u,_]of se){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let h=null,v="",D=!1;for(let F of _)if(F.type==="summary"){D&&(n.push(""),D=!1,h=null,v="");let E=F.data,C=`${E.request||"Session started"} (${ye(E.displayTime)})`,O=E.shouldShowLink?`claude-mem://session-summary/${E.id}`:"";if(e){let I=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${E.id}${o.reset} ${C} ${I}`)}else{let I=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${E.id}** ${C}${I}`)}n.push("")}else{let E=F.data,C=Ie(E.files_modified),O=C.length>0?Ae(C[0],s):"General";O!==h&&(D&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Read | Work |"),n.push("|----|------|---|-------|------|------|")),h=O,D=!0,v="");let I=Le(E.created_at),Y=E.title||"Untitled",y="\u2022";switch(E.type){case"bugfix":y="\u{1F534}";break;case"feature":y="\u{1F7E3}";break;case"refactor":y="\u{1F504}";break;case"change":y="\u2705";break;case"discovery":y="\u{1F535}";break;case"decision":y="\u2696\uFE0F";break;default:y="\u2022"}let te=(E.title?.length||0)+(E.subtitle?.length||0)+(E.narrative?.length||0)+JSON.stringify(E.facts||[]).length,X=Math.ceil(te/Z),U=E.discovery_tokens||0,k="\u{1F50D}";switch(E.type){case"discovery":k="\u{1F50D}";break;case"change":case"feature":case"bugfix":case"refactor":k="\u{1F6E0}\uFE0F";break;case"decision":k="\u2696\uFE0F";break}let re=U>0?`${k} ${U.toLocaleString()}`:"-",K=I!==v,ne=K?I:"";if(v=I,e){let oe=K?`${o.dim}${I}${o.reset}`:" ".repeat(I.length),ie=X>0?`${o.dim}(~${X}t)${o.reset}`:"",ae=U>0?`${o.dim}(${k} ${U.toLocaleString()}t)${o.reset}`:"";n.push(` ${o.dim}#${E.id}${o.reset} ${oe} ${y} ${Y} ${ie} ${ae}`)}else n.push(`| #${E.id} | ${ne||"\u2033"} | ${y} | ${Y} | ~${X} | ${re} |`)}D&&n.push("")}let N=a[0],W=d[0];if(N&&(N.investigated||N.learned||N.completed||N.next_steps)&&(!W||N.created_at_epoch>W.created_at_epoch)&&(n.push(...w("Investigated",N.investigated,o.blue,e)),n.push(...w("Learned",N.learned,o.yellow,e)),n.push(...w("Completed",N.completed,o.green,e)),n.push(...w("Next Steps",N.next_steps,o.magenta,e))),T>0&&R>0){let u=Math.round(T/1e3);n.push(""),e?n.push(`${o.dim}\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${S.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.${o.reset}`):n.push(`\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${S.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.`)}}return r.close(),n.join(` +`).trimEnd()}var De=process.argv.includes("--colors");if(P.isTTY||De)ee(void 0,!0).then(c=>{console.log(c),process.exit(0)});else{let c="";P.on("data",e=>c+=e),P.on("end",async()=>{let e=c.trim()?JSON.parse(c):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await ee(e,!1)}};console.log(JSON.stringify(t)),process.exit(0)})} diff --git a/scripts/smart-install.js b/scripts/smart-install.js index 5f24987d..be36c4ca 100644 --- a/scripts/smart-install.js +++ b/scripts/smart-install.js @@ -51,10 +51,31 @@ function getPackageVersion() { } } +function getNodeVersion() { + return process.version; // e.g., "v22.21.1" +} + function getInstalledVersion() { try { if (existsSync(VERSION_MARKER_PATH)) { - return readFileSync(VERSION_MARKER_PATH, 'utf-8').trim(); + const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim(); + + // Try parsing as JSON (new format) + try { + const marker = JSON.parse(content); + return { + packageVersion: marker.packageVersion, + nodeVersion: marker.nodeVersion, + installedAt: marker.installedAt + }; + } catch { + // Fallback: old format (plain text version string) + return { + packageVersion: content, + nodeVersion: null, // Unknown + installedAt: null + }; + } } } catch (error) { // Marker doesn't exist or can't be read @@ -62,9 +83,14 @@ function getInstalledVersion() { return null; } -function setInstalledVersion(version) { +function setInstalledVersion(packageVersion, nodeVersion) { try { - writeFileSync(VERSION_MARKER_PATH, version, 'utf-8'); + const marker = { + packageVersion, + nodeVersion, + installedAt: new Date().toISOString() + }; + writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf-8'); } catch (error) { log(`⚠️ Failed to write version marker: ${error.message}`, colors.yellow); } @@ -84,24 +110,77 @@ function needsInstall() { } // Check version marker - const currentVersion = getPackageVersion(); - const installedVersion = getInstalledVersion(); + const currentPackageVersion = getPackageVersion(); + const currentNodeVersion = getNodeVersion(); + const installed = getInstalledVersion(); - if (!installedVersion) { + if (!installed) { log('📦 No version marker found - installing', colors.cyan); return true; } - if (currentVersion !== installedVersion) { - log(`📦 Version changed (${installedVersion} → ${currentVersion}) - updating`, colors.cyan); + // Check package version + if (currentPackageVersion !== installed.packageVersion) { + log(`📦 Version changed (${installed.packageVersion} → ${currentPackageVersion}) - updating`, colors.cyan); + return true; + } + + // Check Node.js version + if (installed.nodeVersion && currentNodeVersion !== installed.nodeVersion) { + log(`📦 Node.js version changed (${installed.nodeVersion} → ${currentNodeVersion}) - rebuilding native modules`, colors.cyan); + return true; + } + + // If old format (no nodeVersion), assume needs install + if (!installed.nodeVersion) { + log('📦 Old version marker format - updating', colors.cyan); return true; } // All good - no install needed - log(`✓ Dependencies already installed (v${currentVersion})`, colors.dim); + log(`✓ Dependencies already installed (v${currentPackageVersion})`, colors.dim); return false; } +/** + * Verify that better-sqlite3 native module loads correctly + * This catches ABI mismatches and corrupted builds + */ +async function verifyNativeModules() { + try { + log('🔍 Verifying native modules...', colors.dim); + + // Try to actually load better-sqlite3 + const { default: Database } = await import('better-sqlite3'); + + // Try to create a test in-memory database + const db = new Database(':memory:'); + + // Run a simple query to ensure it works + const result = db.prepare('SELECT 1 + 1 as result').get(); + + // Clean up + db.close(); + + if (result.result !== 2) { + throw new Error('SQLite math check failed'); + } + + log('✓ Native modules verified', colors.dim); + return true; + + } catch (error) { + if (error.code === 'ERR_DLOPEN_FAILED') { + log('⚠️ Native module ABI mismatch detected', colors.yellow); + return false; + } + + // Other errors are unexpected - log and fail + log(`❌ Native module verification failed: ${error.message}`, colors.red); + return false; + } +} + function getWindowsErrorHelp(errorOutput) { // Detect Python version at runtime let pythonStatus = ' Python not detected or version unknown'; @@ -157,7 +236,7 @@ function getWindowsErrorHelp(errorOutput) { return help.join('\n'); } -function runNpmInstall() { +async function runNpmInstall() { const isWindows = process.platform === 'win32'; log('', colors.cyan); @@ -175,7 +254,7 @@ function runNpmInstall() { for (const { command, label } of strategies) { try { log(`Attempting install ${label}...`, colors.dim); - + // Run npm install silently execSync(command, { cwd: PLUGIN_ROOT, @@ -188,12 +267,20 @@ function runNpmInstall() { throw new Error('better-sqlite3 installation verification failed'); } - const version = getPackageVersion(); - setInstalledVersion(version); + // NEW: Verify native modules actually work + const nativeModulesWork = await verifyNativeModules(); + if (!nativeModulesWork) { + throw new Error('Native modules failed to load after install'); + } + + const packageVersion = getPackageVersion(); + const nodeVersion = getNodeVersion(); + setInstalledVersion(packageVersion, nodeVersion); log('', colors.green); log('✅ Dependencies installed successfully!', colors.bright); - log(` Version: ${version}`, colors.dim); + log(` Package version: ${packageVersion}`, colors.dim); + log(` Node.js version: ${nodeVersion}`, colors.dim); log('', colors.reset); return true; @@ -251,8 +338,8 @@ async function main() { const installNeeded = needsInstall(); if (installNeeded) { - // Run installation - const installSuccess = runNpmInstall(); + // Run installation (now async) + const installSuccess = await runNpmInstall(); if (!installSuccess) { log('', colors.red); @@ -260,6 +347,22 @@ async function main() { log('', colors.reset); process.exit(1); } + } else { + // NEW: Even if install not needed, verify native modules work + const nativeModulesWork = await verifyNativeModules(); + + if (!nativeModulesWork) { + log('📦 Native modules need rebuild - reinstalling', colors.cyan); + const installSuccess = await runNpmInstall(); + + if (!installSuccess) { + log('', colors.red); + log('⚠️ Native module rebuild failed', colors.yellow); + log('', colors.reset); + process.exit(1); + } + } + } // Try to start the PM2 worker after fresh install try { diff --git a/src/hooks/context-hook.ts b/src/hooks/context-hook.ts index 0f53b3bc..dcb3fac3 100644 --- a/src/hooks/context-hook.ts +++ b/src/hooks/context-hook.ts @@ -5,10 +5,20 @@ import path from 'path'; import { homedir } from 'os'; -import { existsSync, readFileSync } from 'fs'; +import { existsSync, readFileSync, unlinkSync } from 'fs'; import { stdin } from 'process'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; import { SessionStore } from '../services/sqlite/SessionStore.js'; +// Get __dirname equivalent in ESM +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Version marker path (same as smart-install.js) +// From src/hooks/ we need to go up to plugin root: ../../ +const VERSION_MARKER_PATH = path.join(__dirname, '../../.install-version'); + /** * Get context depth from settings * Priority: ~/.claude/settings.json > env var > default @@ -156,7 +166,27 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false const cwd = input?.cwd ?? process.cwd(); const project = cwd ? path.basename(cwd) : 'unknown-project'; - const db = new SessionStore(); + let db: SessionStore; + try { + db = new SessionStore(); + } catch (error: any) { + if (error.code === 'ERR_DLOPEN_FAILED') { + // Native module ABI mismatch - delete version marker to trigger reinstall + try { + unlinkSync(VERSION_MARKER_PATH); + } catch (unlinkError) { + // Marker might not exist, that's okay + } + + // Log once (not error spam) and exit cleanly + console.error('⚠️ Native module rebuild needed - restart Claude Code to auto-fix'); + console.error(' (This happens after Node.js version upgrades)'); + process.exit(0); // Exit cleanly to avoid error spam + } + + // Other errors should still throw + throw error; + } // Get ALL recent observations for this project (not filtered by summaries) // This ensures we show observations even when summaries haven't been generated diff --git a/tests/smart-install.test.js b/tests/smart-install.test.js new file mode 100644 index 00000000..91070c56 --- /dev/null +++ b/tests/smart-install.test.js @@ -0,0 +1,47 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; + +const VERSION_MARKER_PATH = join(process.cwd(), '.install-version'); + +test('version marker - new JSON format', () => { + const marker = { + packageVersion: '6.3.2', + nodeVersion: 'v22.21.1', + installedAt: new Date().toISOString() + }; + + writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2)); + const content = JSON.parse(readFileSync(VERSION_MARKER_PATH, 'utf-8')); + + assert.strictEqual(content.packageVersion, '6.3.2'); + assert.strictEqual(content.nodeVersion, 'v22.21.1'); + assert.ok(content.installedAt); + + unlinkSync(VERSION_MARKER_PATH); +}); + +test('version marker - backward compatibility with old format', () => { + // Old format: plain text version string + writeFileSync(VERSION_MARKER_PATH, '6.3.2'); + const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim(); + + // Should be able to parse old format + let marker; + try { + marker = JSON.parse(content); + } catch { + // Old format - create compatible object + marker = { + packageVersion: content, + nodeVersion: null, + installedAt: null + }; + } + + assert.strictEqual(marker.packageVersion, '6.3.2'); + assert.strictEqual(marker.nodeVersion, null); + + unlinkSync(VERSION_MARKER_PATH); +});