diff --git a/plugin/scripts/cleanup-hook.js b/plugin/scripts/cleanup-hook.js index e4a30f59..2901258c 100755 --- a/plugin/scripts/cleanup-hook.js +++ b/plugin/scripts/cleanup-hook.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import P from"better-sqlite3";import{join as c,dirname as U,basename as V}from"path";import{homedir as f}from"os";import{existsSync as z,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var F=M(),p=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),ee=c(p,"archives"),se=c(p,"logs"),te=c(p,"trash"),re=c(p,"backups"),ne=c(p,"settings.json"),I=c(p,"claude-mem.db"),oe=c(u,"settings.json"),ie=c(u,"commands"),ae=c(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return c(F,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[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} +#!/usr/bin/env node +import{stdin as N}from"process";import F from"better-sqlite3";import{join as p,dirname as x,basename as Y}from"path";import{homedir as h}from"os";import{existsSync as Q,mkdirSync as U}from"fs";import{fileURLToPath as w}from"url";function X(){return typeof __dirname<"u"?__dirname:x(w(import.meta.url))}var M=X(),c=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),Z=p(c,"archives"),ee=p(c,"logs"),se=p(c,"trash"),te=p(c,"backups"),re=p(c,"settings.json"),I=p(c,"claude-mem.db"),ne=p(u,"settings.json"),oe=p(u,"commands"),ie=p(u,"CLAUDE.md");function O(o){U(o,{recursive:!0})}function L(){return p(M,"..","..")}var l=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(l||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=l[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,n){if(e0&&(b=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}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`})}},A=new T;var m=class{db;constructor(){O(p),this.db=new P(I),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()}initializeSchema(){try{this.db.exec(` +`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let b="";if(r){let{sessionId:H,sdkSessionId:B,correlationId:j,...f}=r;Object.keys(f).length>0&&(b=` {${Object.entries(f).map(([D,y])=>`${D}=${y}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(R):console.log(R)}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`})}},A=new T;var m=class{db;constructor(){O(c),this.db=new F(I),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()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -306,5 +307,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as G}from"child_process";var H=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${H}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` -Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port});try{r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database")}catch(i){console.error("[claude-mem cleanup] Failed to mark session as completed:",i)}r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as D}from"process";var N="";D.on("data",o=>N+=o);D.on("end",async()=>{try{let o=N.trim()?JSON.parse(N):void 0;await k(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}); + `).run(e.toISOString(),s).changes}close(){this.db.close()}};import S from"path";import{existsSync as g}from"fs";import{spawn as P}from"child_process";var G=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),W=`http://127.0.0.1:${G}/health`;async function v(){try{return(await fetch(W,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await v())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=S.join(o,"plugin","scripts","worker-service.cjs");if(!g(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=S.join(o,"ecosystem.config.cjs"),t=S.join(o,"node_modules",".bin","pm2");if(!g(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!g(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=P(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await v())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function k(o){console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` +Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s}),await C()||console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");let r=new m,n=r.findActiveSDKSession(e);n||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),r.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:n.id,sdk_session_id:n.sdk_session_id,project:n.project,worker_port:n.worker_port}),r.markSessionCompleted(n.id),console.error("[claude-mem cleanup] Session marked as completed in database"),r.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(N.isTTY)k(void 0);else{let o="";N.on("data",e=>o+=e),N.on("end",async()=>{let e=o?JSON.parse(o):void 0;await k(e)})} diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index 47f9c1b7..3877a68e 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import G from"path";import de from"better-sqlite3";import{join as T,dirname as re,basename as fe}from"path";import{homedir as Y}from"os";import{existsSync as Re,mkdirSync as ne}from"fs";import{fileURLToPath as ie}from"url";function oe(){return typeof __dirname<"u"?__dirname:re(ie(import.meta.url))}var ae=oe(),I=process.env.CLAUDE_MEM_DATA_DIR||T(Y(),".claude-mem"),U=process.env.CLAUDE_CONFIG_DIR||T(Y(),".claude"),Oe=T(I,"archives"),Le=T(I,"logs"),ve=T(I,"trash"),Ae=T(I,"backups"),ye=T(I,"settings.json"),K=T(I,"claude-mem.db"),ke=T(U,"settings.json"),De=T(U,"commands"),Ce=T(U,"CLAUDE.md");function V(o){ne(o,{recursive:!0})}function q(){return T(ae,"..","..")}var $=(a=>(a[a.DEBUG=0]="DEBUG",a[a.INFO=1]="INFO",a[a.WARN=2]="WARN",a[a.ERROR=3]="ERROR",a[a.SILENT=4]="SILENT",a))($||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=$[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,a){if(e0&&(n=` {${Object.entries(k).map(([d,m])=>`${d}=${m}`).join(", ")}}`)}let N=`[${c}] [${p}] [${u}] ${O}${s}${n}${b}`;e===3?console.error(N):console.log(N)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},J=new M;var D=class{db;constructor(){V(I),this.db=new de(K),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()}initializeSchema(){try{this.db.exec(` +#!/usr/bin/env node +import W from"path";import{stdin as P}from"process";import de from"better-sqlite3";import{join as T,dirname as re,basename as fe}from"path";import{homedir as j}from"os";import{existsSync as Re,mkdirSync as ne}from"fs";import{fileURLToPath as ie}from"url";function oe(){return typeof __dirname<"u"?__dirname:re(ie(import.meta.url))}var ae=oe(),I=process.env.CLAUDE_MEM_DATA_DIR||T(j(),".claude-mem"),U=process.env.CLAUDE_CONFIG_DIR||T(j(),".claude"),Oe=T(I,"archives"),Le=T(I,"logs"),ve=T(I,"trash"),Ae=T(I,"backups"),ye=T(I,"settings.json"),Y=T(I,"claude-mem.db"),De=T(U,"settings.json"),Ce=T(U,"commands"),ke=T(U,"CLAUDE.md");function K(a){ne(a,{recursive:!0})}function V(){return T(ae,"..","..")}var $=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))($||{}),M=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=$[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}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 t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,o){if(e0&&(n=` {${Object.entries(D).map(([d,m])=>`${d}=${m}`).join(", ")}}`)}let N=`[${c}] [${p}] [${u}] ${O}${s}${n}${S}`;e===3?console.error(N):console.log(N)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}},q=new M;var C=class{db;constructor(){K(I),this.db=new de(Y),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()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -222,7 +223,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje SELECT files_read, files_modified FROM observations WHERE sdk_session_id = ? - `).all(e),r=new Set,a=new Set;for(let c of s){if(c.files_read)try{let p=JSON.parse(c.files_read);Array.isArray(p)&&p.forEach(u=>r.add(u))}catch{}if(c.files_modified)try{let p=JSON.parse(c.files_modified);Array.isArray(p)&&p.forEach(u=>a.add(u))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(a)}}getSessionById(e){return this.db.prepare(` + `).all(e),r=new Set,o=new Set;for(let c of s){if(c.files_read)try{let p=JSON.parse(c.files_read);Array.isArray(p)&&p.forEach(u=>r.add(u))}catch{}if(c.files_modified)try{let p=JSON.parse(c.files_modified);Array.isArray(p)&&p.forEach(u=>o.add(u))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(` SELECT id, claude_session_id, sdk_session_id, project, user_prompt FROM sdk_sessions WHERE id = ? @@ -249,17 +250,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje SELECT prompt_counter FROM sdk_sessions WHERE id = ? `).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(` SELECT prompt_counter FROM sdk_sessions WHERE id = ? - `).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let r=new Date,a=r.getTime(),p=this.db.prepare(` + `).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let r=new Date,o=r.getTime(),p=this.db.prepare(` INSERT OR IGNORE INTO sdk_sessions (claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, ?, 'active') - `).run(e,e,t,s,r.toISOString(),a);return p.lastInsertRowid===0||p.changes===0?this.db.prepare(` + `).run(e,e,t,s,r.toISOString(),o);return p.lastInsertRowid===0||p.changes===0?this.db.prepare(` SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1 `).get(e).id:p.lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(` UPDATE sdk_sessions SET sdk_session_id = ? WHERE id = ? AND sdk_session_id IS NULL - `).run(t,e).changes===0?(J.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(` + `).run(t,e).changes===0?(q.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(` UPDATE sdk_sessions SET worker_port = ? WHERE id = ? @@ -268,33 +269,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje FROM sdk_sessions WHERE id = ? LIMIT 1 - `).get(e)?.worker_port||null}saveUserPrompt(e,t,s){let r=new Date,a=r.getTime();return this.db.prepare(` + `).get(e)?.worker_port||null}saveUserPrompt(e,t,s){let r=new Date,o=r.getTime();return this.db.prepare(` INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?) - `).run(e,t,s,r.toISOString(),a).lastInsertRowid}storeObservation(e,t,s,r){let a=new Date,c=a.getTime();this.db.prepare(` + `).run(e,t,s,r.toISOString(),o).lastInsertRowid}storeObservation(e,t,s,r){let o=new Date,c=o.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,t,a.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),r||null,a.toISOString(),c)}storeSummary(e,t,s,r){let a=new Date,c=a.getTime();this.db.prepare(` + `).run(e,t,s.type,s.title,s.subtitle,JSON.stringify(s.facts),s.narrative,JSON.stringify(s.concepts),JSON.stringify(s.files_read),JSON.stringify(s.files_modified),r||null,o.toISOString(),c)}storeSummary(e,t,s,r){let o=new Date,c=o.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,t,a.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,t,o.toISOString(),c),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,r||null,a.toISOString(),c)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(` + `).run(e,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,r||null,o.toISOString(),c)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? @@ -306,7 +307,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),t).changes}close(){this.db.close()}};import X from"path";import{existsSync as F}from"fs";import{spawn as ce}from"child_process";var pe=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),ue=`http://127.0.0.1:${pe}/health`;async function Q(){try{return(await fetch(ue,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function z(){try{if(await Q())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=q(),e=X.join(o,"plugin","scripts","worker-service.cjs");if(!F(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=X.join(o,"ecosystem.config.cjs"),s=X.join(o,"node_modules",".bin","pm2");if(!F(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!F(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let r=ce(s,["start",t],{detached:!0,stdio:"ignore",cwd:o});r.on("error",a=>{throw new Error(`Failed to spawn PM2: ${a.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let a=0;a<3;a++)if(await new Promise(c=>setTimeout(c,500)),await Q())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}var i={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 P(o){if(!o)return[];try{let e=JSON.parse(o);return Array.isArray(e)?e:[]}catch{return[]}}function le(o){return new Date(o).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function me(o){return new Date(o).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function _e(o){return new Date(o).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ee(o){return o?Math.ceil(o.length/4):0}function Te(o,e){try{return G.isAbsolute(o)?G.relative(e,o):o}catch{return o}}function he(o,e){if(e.length===0)return[];let t=e.map(()=>"?").join(",");return o.db.prepare(` + `).run(e.toISOString(),t).changes}close(){this.db.close()}};import X from"path";import{existsSync as F}from"fs";import{spawn as ce}from"child_process";var pe=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),ue=`http://127.0.0.1:${pe}/health`;async function J(){try{return(await fetch(ue,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function Q(){try{if(await J())return!0;console.error("[claude-mem] Worker not responding, starting...");let a=V(),e=X.join(a,"plugin","scripts","worker-service.cjs");if(!F(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=X.join(a,"ecosystem.config.cjs"),s=X.join(a,"node_modules",".bin","pm2");if(!F(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!F(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let r=ce(s,["start",t],{detached:!0,stdio:"ignore",cwd:a});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(c=>setTimeout(c,500)),await J())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}var i={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 G(a){if(!a)return[];let e=JSON.parse(a);return Array.isArray(e)?e:[]}function le(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}function me(a){return new Date(a).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}function _e(a){return new Date(a).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}function Ee(a){return a?Math.ceil(a.length/4):0}function Te(a,e){return W.isAbsolute(a)?W.relative(e,a):a}function he(a,e){if(e.length===0)return[];let t=e.map(()=>"?").join(",");return a.db.prepare(` SELECT id, sdk_session_id, type, title, subtitle, narrative, facts, concepts, files_read, files_modified, @@ -314,20 +315,18 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje FROM observations WHERE sdk_session_id IN (${t}) ORDER BY created_at_epoch DESC - `).all(...e)}function W(o,e=!1,t=!1){z();let s=o?.cwd??process.cwd(),r=s?G.basename(s):"unknown-project",a=new D;try{let c=a.db.prepare(` - SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch - FROM session_summaries - WHERE project = ? - ORDER BY created_at_epoch DESC - LIMIT 4 - `).all(r);if(c.length===0)return e?` + `).all(...e)}function z(a,e=!1,t=!1){Q();let s=a?.cwd??process.cwd(),r=s?W.basename(s):"unknown-project",o=new C,c=o.db.prepare(` + SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 4 + `).all(r);if(c.length===0)return o.close(),e?` ${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset} ${i.gray}${"\u2500".repeat(60)}${i.reset} ${i.dim}No previous sessions found for this project yet.${i.reset} `:`# [${r}] recent context -No previous sessions found for this project yet.`;let p=c.slice(0,3),u=[...new Set(p.map(N=>N.sdk_session_id))],b=he(a,u).filter(N=>{let h=P(N.concepts);return h.includes("what-changed")||h.includes("how-it-works")||h.includes("problem-solution")||h.includes("gotcha")||h.includes("discovery")||h.includes("why-it-exists")||h.includes("decision")||h.includes("trade-off")}),n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),b.length>0){e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off"),n.push("")),e?(n.push(`${i.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${i.reset}`),n.push(`${i.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${i.reset}`),n.push(`${i.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${i.reset}`),n.push(`${i.dim} \u2192 Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately${i.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately"),n.push(""));let N=c[0]?.id,h=p.map((d,m)=>{let l=m===0?null:c[m+1];return{...d,displayEpoch:l?l.created_at_epoch:d.created_at_epoch,displayTime:l?l.created_at:d.created_at,isMostRecent:d.id===N}}),C=[...b.map(d=>({type:"observation",data:d})),...h.map(d=>({type:"summary",data:d}))];C.sort((d,m)=>{let l=d.type==="observation"?d.data.created_at_epoch:d.data.displayEpoch,R=m.type==="observation"?m.data.created_at_epoch:m.data.displayEpoch;return l-R});let L=new Map;for(let d of C){let m=d.type==="observation"?d.data.created_at:d.data.displayTime,l=_e(m);L.has(l)||L.set(l,[]),L.get(l).push(d)}let k=Array.from(L.entries()).sort((d,m)=>{let l=new Date(d[0]).getTime(),R=new Date(m[0]).getTime();return l-R});for(let[d,m]of k){e?(n.push(`${i.bright}${i.cyan}${d}${i.reset}`),n.push("")):(n.push(`### ${d}`),n.push(""));let l=null,R="",v=!1;for(let x of m)if(x.type==="summary"){v&&(n.push(""),v=!1,l=null,R="");let _=x.data,A=`${_.request||"Session started"} (${le(_.displayTime)})`,S=_.isMostRecent?"":`claude-mem://session-summary/${_.id}`;if(e){let E=S?`${i.dim}[${S}]${i.reset}`:"";n.push(`\u{1F3AF} ${i.yellow}#S${_.id}${i.reset} ${A} ${E}`)}else{let E=S?` [\u2192](${S})`:"";n.push(`**\u{1F3AF} #S${_.id}** ${A}${E}`)}n.push("")}else{let _=x.data,A=P(_.files_modified),S=A.length>0?Te(A[0],s):"General";S!==l&&(v&&n.push(""),e?n.push(`${i.dim}${S}${i.reset}`):n.push(`**${S}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),l=S,v=!0,R="");let E=P(_.concepts),f="\u2022";E.includes("gotcha")?f="\u{1F534}":E.includes("decision")?f="\u{1F7E4}":E.includes("trade-off")?f="\u2696\uFE0F":E.includes("problem-solution")?f="\u{1F7E1}":E.includes("discovery")?f="\u{1F7E3}":E.includes("why-it-exists")?f="\u{1F7E0}":E.includes("how-it-works")?f="\u{1F535}":E.includes("what-changed")&&(f="\u{1F7E2}");let y=me(_.created_at),B=_.title||"Untitled",w=Ee(_.narrative),j=y!==R,ee=j?y:"";if(R=y,e){let se=j?`${i.dim}${y}${i.reset}`:" ".repeat(y.length),te=w>0?`${i.dim}(~${w}t)${i.reset}`:"";n.push(` ${i.dim}#${_.id}${i.reset} ${se} ${f} ${B} ${te}`)}else n.push(`| #${_.id} | ${ee||"\u2033"} | ${f} | ${B} | ~${w} |`)}v&&n.push("")}let g=c[0];g&&(g.completed||g.next_steps)&&(g.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${g.completed}`):n.push(`**Completed**: ${g.completed}`),n.push("")),g.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${g.next_steps}`):n.push(`**Next Steps**: ${g.next_steps}`),n.push(""))),e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*"),n.push("")}return e&&(n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")),n.join(` -`)}finally{a.close()}}import{stdin as H}from"process";var Z=process.argv.includes("--index");if(H.isTTY)try{let o=W(void 0,!0,Z);console.log(o),process.exit(0)}catch(o){console.error(`[claude-mem context-hook error: ${o.message}]`),console.error(o.stack),process.exit(1)}else{let o="";H.on("data",e=>o+=e),H.on("end",()=>{try{let e=o.trim()?JSON.parse(o):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:W(e,!1,Z)}};console.log(JSON.stringify(s)),process.exit(0)}catch(e){let t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:`[claude-mem ERROR: ${e.message}] -Input: ${o.substring(0,200)}... -${e.stack}`}};console.log(JSON.stringify(t)),process.exit(1)}})} +No previous sessions found for this project yet.`;let p=c.slice(0,3),u=[...new Set(p.map(N=>N.sdk_session_id))],S=he(o,u).filter(N=>{let h=G(N.concepts);return h.includes("what-changed")||h.includes("how-it-works")||h.includes("problem-solution")||h.includes("gotcha")||h.includes("discovery")||h.includes("why-it-exists")||h.includes("decision")||h.includes("trade-off")}),n=[];if(e?(n.push(""),n.push(`${i.bright}${i.cyan}\u{1F4DD} [${r}] recent context${i.reset}`),n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")):(n.push(`# [${r}] recent context`),n.push("")),S.length>0){e?(n.push(`${i.dim}Legend: \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off${i.reset}`),n.push("")):(n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} gotcha | \u{1F7E1} problem-solution | \u{1F535} how-it-works | \u{1F7E2} what-changed | \u{1F7E3} discovery | \u{1F7E0} why-it-exists | \u{1F7E4} decision | \u2696\uFE0F trade-off"),n.push("")),e?(n.push(`${i.dim}\u{1F4A1} Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${i.reset}`),n.push(`${i.dim} \u2192 Use MCP search tools to fetch full observation details on-demand (Layer 2)${i.reset}`),n.push(`${i.dim} \u2192 Prefer searching observations over re-reading code for past decisions and learnings${i.reset}`),n.push(`${i.dim} \u2192 Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately${i.reset}`),n.push("")):(n.push("\u{1F4A1} **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts)."),n.push("- Use MCP search tools to fetch full observation details on-demand (Layer 2)"),n.push("- Prefer searching observations over re-reading code for past decisions and learnings"),n.push("- Critical types (\u{1F534} gotcha, \u{1F7E4} decision, \u2696\uFE0F trade-off) often worth fetching immediately"),n.push(""));let N=c[0]?.id,h=p.map((d,m)=>{let l=m===0?null:c[m+1];return{...d,displayEpoch:l?l.created_at_epoch:d.created_at_epoch,displayTime:l?l.created_at:d.created_at,isMostRecent:d.id===N}}),k=[...S.map(d=>({type:"observation",data:d})),...h.map(d=>({type:"summary",data:d}))];k.sort((d,m)=>{let l=d.type==="observation"?d.data.created_at_epoch:d.data.displayEpoch,R=m.type==="observation"?m.data.created_at_epoch:m.data.displayEpoch;return l-R});let L=new Map;for(let d of k){let m=d.type==="observation"?d.data.created_at:d.data.displayTime,l=_e(m);L.has(l)||L.set(l,[]),L.get(l).push(d)}let D=Array.from(L.entries()).sort((d,m)=>{let l=new Date(d[0]).getTime(),R=new Date(m[0]).getTime();return l-R});for(let[d,m]of D){e?(n.push(`${i.bright}${i.cyan}${d}${i.reset}`),n.push("")):(n.push(`### ${d}`),n.push(""));let l=null,R="",v=!1;for(let x of m)if(x.type==="summary"){v&&(n.push(""),v=!1,l=null,R="");let _=x.data,A=`${_.request||"Session started"} (${le(_.displayTime)})`,b=_.isMostRecent?"":`claude-mem://session-summary/${_.id}`;if(e){let E=b?`${i.dim}[${b}]${i.reset}`:"";n.push(`\u{1F3AF} ${i.yellow}#S${_.id}${i.reset} ${A} ${E}`)}else{let E=b?` [\u2192](${b})`:"";n.push(`**\u{1F3AF} #S${_.id}** ${A}${E}`)}n.push("")}else{let _=x.data,A=G(_.files_modified),b=A.length>0?Te(A[0],s):"General";b!==l&&(v&&n.push(""),e?n.push(`${i.dim}${b}${i.reset}`):n.push(`**${b}**`),e||(n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|")),l=b,v=!0,R="");let E=G(_.concepts),f="\u2022";E.includes("gotcha")?f="\u{1F534}":E.includes("decision")?f="\u{1F7E4}":E.includes("trade-off")?f="\u2696\uFE0F":E.includes("problem-solution")?f="\u{1F7E1}":E.includes("discovery")?f="\u{1F7E3}":E.includes("why-it-exists")?f="\u{1F7E0}":E.includes("how-it-works")?f="\u{1F535}":E.includes("what-changed")&&(f="\u{1F7E2}");let y=me(_.created_at),H=_.title||"Untitled",w=Ee(_.narrative),B=y!==R,ee=B?y:"";if(R=y,e){let se=B?`${i.dim}${y}${i.reset}`:" ".repeat(y.length),te=w>0?`${i.dim}(~${w}t)${i.reset}`:"";n.push(` ${i.dim}#${_.id}${i.reset} ${se} ${f} ${H} ${te}`)}else n.push(`| #${_.id} | ${ee||"\u2033"} | ${f} | ${H} | ~${w} |`)}v&&n.push("")}let g=c[0];g&&(g.completed||g.next_steps)&&(g.completed&&(e?n.push(`${i.green}Completed:${i.reset} ${g.completed}`):n.push(`**Completed**: ${g.completed}`),n.push("")),g.next_steps&&(e?n.push(`${i.magenta}Next Steps:${i.reset} ${g.next_steps}`):n.push(`**Next Steps**: ${g.next_steps}`),n.push(""))),e?n.push(`${i.dim}Use claude-mem MCP search to access records with the given ID${i.reset}`):n.push("*Use claude-mem MCP search to access records with the given ID*"),n.push("")}return e&&(n.push(`${i.gray}${"\u2500".repeat(60)}${i.reset}`),n.push("")),o.close(),n.join(` +`)}var Z=process.argv.includes("--index");if(P.isTTY){let a=z(void 0,!0,Z);console.log(a),process.exit(0)}else{let a="";P.on("data",e=>a+=e),P.on("end",()=>{let e=a.trim()?JSON.parse(a):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:z(e,!1,Z)}};console.log(JSON.stringify(s)),process.exit(0)})} diff --git a/plugin/scripts/new-hook.js b/plugin/scripts/new-hook.js index 0e8fd440..4726f7f4 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import Y from"path";import W from"better-sqlite3";import{join as p,dirname as M,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as P}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:M(F(import.meta.url))}var G=H(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),oe=p(u,"archives"),ne=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(n){P(n,{recursive:!0})}function L(){return p(G,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[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,o){if(e0&&(_=` {${Object.entries(f).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}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`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new W(O),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()}initializeSchema(){try{this.db.exec(` +#!/usr/bin/env node +import j from"path";import{stdin as x}from"process";import G from"better-sqlite3";import{join as p,dirname as X,basename as z}from"path";import{homedir as h}from"os";import{existsSync as te,mkdirSync as M}from"fs";import{fileURLToPath as P}from"url";function F(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var H=F(),u=process.env.CLAUDE_MEM_DATA_DIR||p(h(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(h(),".claude"),ne=p(u,"archives"),oe=p(u,"logs"),ie=p(u,"trash"),ae=p(u,"backups"),de=p(u,"settings.json"),O=p(u,"claude-mem.db"),pe=p(l,"settings.json"),ce=p(l,"commands"),Ee=p(l,"CLAUDE.md");function I(o){M(o,{recursive:!0})}function L(){return p(H,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[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,n){if(e0&&(_=` {${Object.entries(f).map(([U,w])=>`${U}=${w}`).join(", ")}}`)}let R=`[${i}] [${a}] [${d}] ${E}${t}${_}${c}`;e===3?console.error(R):console.log(R)}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`})}},A=new S;var m=class{db;constructor(){I(u),this.db=new G(O),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()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -222,7 +223,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,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(` + `).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(` SELECT id, claude_session_id, sdk_session_id, project, user_prompt FROM sdk_sessions WHERE id = ? @@ -249,11 +250,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje SELECT prompt_counter FROM sdk_sessions WHERE id = ? `).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(` SELECT prompt_counter FROM sdk_sessions WHERE id = ? - `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(` + `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(` INSERT OR IGNORE INTO sdk_sessions (claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, ?, 'active') - `).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(` + `).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(` SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1 `).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(` UPDATE sdk_sessions @@ -268,33 +269,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje FROM sdk_sessions WHERE id = ? LIMIT 1 - `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(` + `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(` INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?) - `).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(` + `).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.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,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, 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,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.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,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.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,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}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,n.toISOString(),i)}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 = ? @@ -306,4 +307,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),s).changes}close(){this.db.close()}};function B(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=B(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as $}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=L(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=$(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}function y(){return k}async function x(n){if(!n)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=n,r=Y.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m;try{let a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`);let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}finally{i.close()}}import{stdin as U}from"process";var N="";U.on("data",n=>N+=n);U.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await x(n),process.exit(0)}); + `).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=W(o,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as b}from"fs";import{spawn as B}from"child_process";var k=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${k}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!b(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!b(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!b(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=B(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}function y(){return k}async function Y(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=j.basename(s);if(!await D())throw new Error("Worker service failed to start or become healthy");let i=new m,a=i.createSDKSession(e,r,t),d=i.incrementPromptCounter(a);i.saveUserPrompt(e,d,t),console.error(`[new-hook] Session ${a}, prompt #${d}`),i.close();let E=y(),c=await fetch(`http://127.0.0.1:${E}/sessions/${a}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let _=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${_}`)}console.log(v("UserPromptSubmit",!0))}var N="";x.on("data",o=>N+=o);x.on("end",async()=>{let o=N?JSON.parse(N):void 0;await Y(o)}); diff --git a/plugin/scripts/save-hook.js b/plugin/scripts/save-hook.js index b8ec5f27..95c311cc 100755 --- a/plugin/scripts/save-hook.js +++ b/plugin/scripts/save-hook.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import H from"better-sqlite3";import{join as p,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as M}from"fs";import{fileURLToPath as X}from"url";function P(){return typeof __dirname<"u"?__dirname:w(X(import.meta.url))}var F=P(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){M(n,{recursive:!0})}function v(){return p(F,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[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} +#!/usr/bin/env node +import{stdin as D}from"process";import F from"better-sqlite3";import{join as p,dirname as U,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as w}from"fs";import{fileURLToPath as M}from"url";function X(){return typeof __dirname<"u"?__dirname:U(M(import.meta.url))}var P=X(),u=process.env.CLAUDE_MEM_DATA_DIR||p(I(),".claude-mem"),S=process.env.CLAUDE_CONFIG_DIR||p(I(),".claude"),re=p(u,"archives"),oe=p(u,"logs"),ne=p(u,"trash"),ie=p(u,"backups"),ae=p(u,"settings.json"),L=p(u,"claude-mem.db"),de=p(S,"settings.json"),pe=p(S,"commands"),ce=p(S,"CLAUDE.md");function A(n){w(n,{recursive:!0})}function v(){return p(P,"..","..")}var g=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[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,o){if(e0&&(_=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}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`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new H(L),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()}initializeSchema(){try{this.db.exec(` +`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:V,...h}=r;Object.keys(h).length>0&&(_=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let l=`[${i}] [${a}] [${d}] ${c}${t}${_}${E}`;e===3?console.error(l):console.log(l)}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`})}},m=new b;var T=class{db;constructor(){A(u),this.db=new F(L),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()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -306,4 +307,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var j=new Set(["ListMcpResourcesTool"]);async function D(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(j.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}import{stdin as y}from"process";var O="";y.on("data",n=>O+=n);y.on("end",async()=>{let n=O.trim()?JSON.parse(O):void 0;await D(n),process.exit(0)}); + `).run(e.toISOString(),s).changes}close(){this.db.close()}};function H(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function N(n,e,s={}){let t=H(n,e,s);return JSON.stringify(t)}import R from"path";import{existsSync as f}from"fs";import{spawn as G}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${W}/health`;async function C(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=v(),e=R.join(n,"plugin","scripts","worker-service.cjs");if(!f(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=R.join(n,"ecosystem.config.cjs"),t=R.join(n,"node_modules",".bin","pm2");if(!f(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!f(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}var $=new Set(["ListMcpResourcesTool"]);async function j(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if($.has(s)){console.log(N("PostToolUse",!0));return}if(!await k())throw new Error("Worker service failed to start or become healthy");let i=new T,a=i.createSDKSession(e,"",""),d=i.getPromptCounter(a);i.close();let c=m.formatTool(s,t),E=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);m.dataIn("HOOK",`PostToolUse: ${c}`,{sessionId:a,workerPort:E});let _=await fetch(`http://127.0.0.1:${E}/sessions/${a}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:s,tool_input:t!==void 0?JSON.stringify(t):"{}",tool_output:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(!_.ok){let l=await _.text();throw m.failure("HOOK","Failed to send observation",{sessionId:a,status:_.status},l),new Error(`Failed to send observation to worker: ${_.status} ${l}`)}m.debug("HOOK","Observation sent successfully",{sessionId:a,toolName:s}),console.log(N("PostToolUse",!0))}var O="";D.on("data",n=>O+=n);D.on("end",async()=>{let n=O?JSON.parse(O):void 0;await j(n)}); diff --git a/plugin/scripts/summary-hook.js b/plugin/scripts/summary-hook.js index 899ec3c5..34a2d3fa 100755 --- a/plugin/scripts/summary-hook.js +++ b/plugin/scripts/summary-hook.js @@ -1,7 +1,8 @@ #!/usr/bin/env node -import H from"better-sqlite3";import{join as p,dirname as w,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as X}from"fs";import{fileURLToPath as M}from"url";function P(){return typeof __dirname<"u"?__dirname:w(M(import.meta.url))}var F=P(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),oe=p(c,"trash"),ne=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(n){X(n,{recursive:!0})}function A(){return p(F,"..","..")}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[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,o){if(e0&&(b=` {${Object.entries(h).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${b}${_}`;e===3?console.error(f):console.log(f)}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`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new H(I),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()}initializeSchema(){try{this.db.exec(` +#!/usr/bin/env node +import{stdin as D}from"process";import F from"better-sqlite3";import{join as p,dirname as U,basename as J}from"path";import{homedir as O}from"os";import{existsSync as ee,mkdirSync as w}from"fs";import{fileURLToPath as X}from"url";function M(){return typeof __dirname<"u"?__dirname:U(X(import.meta.url))}var P=M(),c=process.env.CLAUDE_MEM_DATA_DIR||p(O(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||p(O(),".claude"),te=p(c,"archives"),re=p(c,"logs"),ne=p(c,"trash"),oe=p(c,"backups"),ie=p(c,"settings.json"),I=p(c,"claude-mem.db"),ae=p(l,"settings.json"),de=p(l,"commands"),pe=p(l,"CLAUDE.md");function L(o){w(o,{recursive:!0})}function A(){return p(P,"..","..")}var T=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(T||{}),S=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[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,n){if(e0&&(N=` {${Object.entries(h).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let f=`[${i}] [${a}] [${d}] ${E}${t}${N}${_}`;e===3?console.error(f):console.log(f)}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`})}},u=new S;var m=class{db;constructor(){L(c),this.db=new F(I),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()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -222,7 +223,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,o=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>o.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(o)}}getSessionById(e){return this.db.prepare(` + `).all(e),r=new Set,n=new Set;for(let i of t){if(i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.forEach(d=>r.add(d))}catch{}if(i.files_modified)try{let a=JSON.parse(i.files_modified);Array.isArray(a)&&a.forEach(d=>n.add(d))}catch{}}return{filesRead:Array.from(r),filesModified:Array.from(n)}}getSessionById(e){return this.db.prepare(` SELECT id, claude_session_id, sdk_session_id, project, user_prompt FROM sdk_sessions WHERE id = ? @@ -249,11 +250,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje SELECT prompt_counter FROM sdk_sessions WHERE id = ? `).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(` SELECT prompt_counter FROM sdk_sessions WHERE id = ? - `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime(),a=this.db.prepare(` + `).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime(),a=this.db.prepare(` INSERT OR IGNORE INTO sdk_sessions (claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, ?, 'active') - `).run(e,e,s,t,r.toISOString(),o);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(` + `).run(e,e,s,t,r.toISOString(),n);return a.lastInsertRowid===0||a.changes===0?this.db.prepare(` SELECT id FROM sdk_sessions WHERE claude_session_id = ? LIMIT 1 `).get(e).id:a.lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(` UPDATE sdk_sessions @@ -268,33 +269,33 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje FROM sdk_sessions WHERE id = ? LIMIT 1 - `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(` + `).get(e)?.worker_port||null}saveUserPrompt(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(` INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?) - `).run(e,s,t,r.toISOString(),o).lastInsertRowid}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();this.db.prepare(` + `).run(e,s,t,r.toISOString(),n).lastInsertRowid}storeObservation(e,s,t,r){let n=new Date,i=n.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,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, 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,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.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,n.toISOString(),i)}storeSummary(e,s,t,r){let n=new Date,i=n.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,o.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` + `).run(e,e,s,n.toISOString(),i),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`)),this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),i)}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,n.toISOString(),i)}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 = ? @@ -306,4 +307,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje UPDATE sdk_sessions SET status = 'failed', completed_at = ?, completed_at_epoch = ? WHERE status = 'active' - `).run(e.toISOString(),s).changes}close(){this.db.close()}};function G(n,e,s){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(n,e,s={}){let t=G(n,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as W}from"child_process";var B=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),$=`http://127.0.0.1:${B}/health`;async function C(){try{return(await fetch($,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=g.join(n,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(n,"ecosystem.config.cjs"),t=g.join(n,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=W(t,["start",s],{detached:!0,stdio:"ignore",cwd:n});r.on("error",o=>{throw new Error(`Failed to spawn PM2: ${o.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let o=0;o<3;o++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(n){return console.error(`[claude-mem] Failed to start worker: ${n.message}`),!1}}async function D(n){if(!n)throw new Error("summaryHook requires input");let{session_id:e}=n;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),o=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:o});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:o}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}import{stdin as y}from"process";var N="";y.on("data",n=>N+=n);y.on("end",async()=>{let n=N.trim()?JSON.parse(N):void 0;await D(n),process.exit(0)}); + `).run(e.toISOString(),s).changes}close(){this.db.close()}};function H(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(o,e,s={}){let t=H(o,e,s);return JSON.stringify(t)}import g from"path";import{existsSync as R}from"fs";import{spawn as G}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${W}/health`;async function C(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await C())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=A(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!R(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=g.join(o,"ecosystem.config.cjs"),t=g.join(o,"node_modules",".bin","pm2");if(!R(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!R(s))throw new Error(`PM2 ecosystem config not found at ${s}. Plugin installation may be corrupted.`);let r=G(t,["start",s],{detached:!0,stdio:"ignore",cwd:o});r.on("error",n=>{throw new Error(`Failed to spawn PM2: ${n.message}`)}),r.unref(),console.error("[claude-mem] Worker started with PM2");for(let n=0;n<3;n++)if(await new Promise(i=>setTimeout(i,500)),await C())return console.error("[claude-mem] Worker is healthy"),!0;return console.error("[claude-mem] Worker failed to become healthy after startup"),!1}catch(o){return console.error(`[claude-mem] Failed to start worker: ${o.message}`),!1}}async function $(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o;if(!await k())throw new Error("Worker service failed to start or become healthy");let t=new m,r=t.createSDKSession(e,"",""),n=t.getPromptCounter(r);t.close();let i=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10);u.dataIn("HOOK","Stop: Requesting summary",{sessionId:r,workerPort:i,promptNumber:n});let a=await fetch(`http://127.0.0.1:${i}/sessions/${r}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:n}),signal:AbortSignal.timeout(2e3)});if(!a.ok){let d=await a.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:r,status:a.status},d),new Error(`Failed to request summary from worker: ${a.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:r}),console.log(v("Stop",!0))}var b="";D.on("data",o=>b+=o);D.on("end",async()=>{let o=b?JSON.parse(b):void 0;await $(o)}); diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index b221cf1a..0ee545d5 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -13,11 +13,11 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const HOOKS = [ - { name: 'context-hook', source: 'src/bin/hooks/context-hook.ts' }, - { name: 'new-hook', source: 'src/bin/hooks/new-hook.ts' }, - { name: 'save-hook', source: 'src/bin/hooks/save-hook.ts' }, - { name: 'summary-hook', source: 'src/bin/hooks/summary-hook.ts' }, - { name: 'cleanup-hook', source: 'src/bin/hooks/cleanup-hook.ts' } + { name: 'context-hook', source: 'src/hooks/context-hook.ts' }, + { name: 'new-hook', source: 'src/hooks/new-hook.ts' }, + { name: 'save-hook', source: 'src/hooks/save-hook.ts' }, + { name: 'summary-hook', source: 'src/hooks/summary-hook.ts' }, + { name: 'cleanup-hook', source: 'src/hooks/cleanup-hook.ts' } ]; const WORKER_SERVICE = { diff --git a/src/bin/hooks/cleanup-hook.ts b/src/bin/hooks/cleanup-hook.ts deleted file mode 100644 index dab3fc80..00000000 --- a/src/bin/hooks/cleanup-hook.ts +++ /dev/null @@ -1,22 +0,0 @@ - -/** - * Cleanup Hook Entry Point - SessionEnd - * Standalone executable for plugin hooks - */ - -import { cleanupHook } from '../../hooks/cleanup.js'; -import { stdin } from 'process'; - -// Read input from stdin -let input = ''; -stdin.on('data', (chunk) => input += chunk); -stdin.on('end', async () => { - try { - const parsed = input.trim() ? JSON.parse(input) : undefined; - await cleanupHook(parsed); - } catch (error: any) { - console.error(`[claude-mem cleanup-hook error: ${error.message}]`); - console.log('{"continue": true, "suppressOutput": true}'); - process.exit(0); - } -}); diff --git a/src/bin/hooks/context-hook.ts b/src/bin/hooks/context-hook.ts deleted file mode 100644 index af4e3ad4..00000000 --- a/src/bin/hooks/context-hook.ts +++ /dev/null @@ -1,52 +0,0 @@ - -/** - * Context Hook Entry Point - SessionStart - * Standalone executable for plugin hooks - */ - -import { contextHook } from '../../hooks/context.js'; -import { stdin } from 'process'; - -// Check for --index flag -const useIndexView = process.argv.includes('--index'); - -if (stdin.isTTY) { - // Running manually from terminal - print formatted output with colors - try { - const contextOutput = contextHook(undefined, true, useIndexView); - console.log(contextOutput); - process.exit(0); - } catch (error: any) { - console.error(`[claude-mem context-hook error: ${error.message}]`); - console.error(error.stack); - process.exit(1); - } -} else { - // Running from hook - wrap in hookSpecificOutput JSON format - let input = ''; - stdin.on('data', (chunk) => input += chunk); - stdin.on('end', () => { - try { - const parsed = input.trim() ? JSON.parse(input) : undefined; - const contextOutput = contextHook(parsed, false, useIndexView); - const result = { - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: contextOutput - } - }; - console.log(JSON.stringify(result)); - process.exit(0); - } catch (error: any) { - // Output error in JSON format so hook doesn't fail - const errorResult = { - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: `[claude-mem ERROR: ${error.message}]\nInput: ${input.substring(0, 200)}...\n${error.stack}` - } - }; - console.log(JSON.stringify(errorResult)); - process.exit(1); - } - }); -} diff --git a/src/bin/hooks/new-hook.ts b/src/bin/hooks/new-hook.ts deleted file mode 100644 index 897bc632..00000000 --- a/src/bin/hooks/new-hook.ts +++ /dev/null @@ -1,17 +0,0 @@ - -/** - * New Hook Entry Point - UserPromptSubmit - * Standalone executable for plugin hooks - */ - -import { newHook } from '../../hooks/new.js'; -import { stdin } from 'process'; - -// Read input from stdin -let input = ''; -stdin.on('data', (chunk) => input += chunk); -stdin.on('end', async () => { - const parsed = input.trim() ? JSON.parse(input) : undefined; - await newHook(parsed); - process.exit(0); -}); diff --git a/src/bin/hooks/save-hook.ts b/src/bin/hooks/save-hook.ts deleted file mode 100644 index 8a1a8bb3..00000000 --- a/src/bin/hooks/save-hook.ts +++ /dev/null @@ -1,17 +0,0 @@ - -/** - * Save Hook Entry Point - PostToolUse - * Standalone executable for plugin hooks - */ - -import { saveHook } from '../../hooks/save.js'; -import { stdin } from 'process'; - -// Read input from stdin -let input = ''; -stdin.on('data', (chunk) => input += chunk); -stdin.on('end', async () => { - const parsed = input.trim() ? JSON.parse(input) : undefined; - await saveHook(parsed); - process.exit(0); -}); diff --git a/src/bin/hooks/summary-hook.ts b/src/bin/hooks/summary-hook.ts deleted file mode 100644 index ffddafaf..00000000 --- a/src/bin/hooks/summary-hook.ts +++ /dev/null @@ -1,17 +0,0 @@ - -/** - * Summary Hook Entry Point - Stop - * Standalone executable for plugin hooks - */ - -import { summaryHook } from '../../hooks/summary.js'; -import { stdin } from 'process'; - -// Read input from stdin -let input = ''; -stdin.on('data', (chunk) => input += chunk); -stdin.on('end', async () => { - const parsed = input.trim() ? JSON.parse(input) : undefined; - await summaryHook(parsed); - process.exit(0); -}); diff --git a/src/bin/hooks/worker.ts b/src/bin/hooks/worker.ts deleted file mode 100644 index c2fb0015..00000000 --- a/src/bin/hooks/worker.ts +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node - -/** - * Worker Entry Point - * Standalone background process for SDK agent - */ - -import { main } from '../../sdk/worker.js'; - -// Entry point - just call the worker main function -main().catch((error) => { - console.error('[SDK Worker] Fatal error:', error); - process.exit(1); -}); diff --git a/src/hooks/cleanup-hook.ts b/src/hooks/cleanup-hook.ts new file mode 100644 index 00000000..6757e914 --- /dev/null +++ b/src/hooks/cleanup-hook.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * Cleanup Hook - SessionEnd + * Consolidated entry point + logic + */ + +import { stdin } from 'process'; +import { SessionStore } from '../services/sqlite/SessionStore.js'; +import { ensureWorkerRunning } from '../shared/worker-utils.js'; + +export interface SessionEndInput { + session_id: string; + cwd: string; + transcript_path?: string; + hook_event_name: string; + reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'; +} + +/** + * Cleanup Hook Main Logic + */ +async function cleanupHook(input?: SessionEndInput): Promise { + // Log hook entry point + console.error('[claude-mem cleanup] Hook fired', { + input: input ? { + session_id: input.session_id, + cwd: input.cwd, + reason: input.reason + } : null + }); + + // Handle standalone execution (no input provided) + if (!input) { + console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook'); + console.log('\nExpected input format:'); + console.log(JSON.stringify({ + session_id: "string", + cwd: "string", + transcript_path: "string", + hook_event_name: "SessionEnd", + reason: "exit" + }, null, 2)); + process.exit(0); + } + + const { session_id, reason } = input; + console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason }); + + // Ensure worker is running first + const workerReady = await ensureWorkerRunning(); + if (!workerReady) { + console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup'); + } + + // Find active SDK session + const db = new SessionStore(); + const session = db.findActiveSDKSession(session_id); + + if (!session) { + // No active session - nothing to clean up + console.error('[claude-mem cleanup] No active SDK session found', { session_id }); + db.close(); + console.log('{"continue": true, "suppressOutput": true}'); + process.exit(0); + } + + console.error('[claude-mem cleanup] Active SDK session found', { + session_id: session.id, + sdk_session_id: session.sdk_session_id, + project: session.project, + worker_port: session.worker_port + }); + + // Mark session as completed in DB + db.markSessionCompleted(session.id); + console.error('[claude-mem cleanup] Session marked as completed in database'); + + db.close(); + + console.error('[claude-mem cleanup] Cleanup completed successfully'); + console.log('{"continue": true, "suppressOutput": true}'); + process.exit(0); +} + +// Entry Point +if (stdin.isTTY) { + // Running manually + cleanupHook(undefined); +} else { + let input = ''; + stdin.on('data', (chunk) => input += chunk); + stdin.on('end', async () => { + const parsed = input ? JSON.parse(input) : undefined; + await cleanupHook(parsed); + }); +} diff --git a/src/hooks/cleanup.ts b/src/hooks/cleanup.ts deleted file mode 100644 index 76254e8d..00000000 --- a/src/hooks/cleanup.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { SessionStore } from '../services/sqlite/SessionStore.js'; -import { ensureWorkerRunning } from '../shared/worker-utils.js'; - -export interface SessionEndInput { - session_id: string; - cwd: string; - transcript_path?: string; - hook_event_name: string; - reason: 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'; -} - -/** - * Cleanup Hook - SessionEnd - * Marks session as completed when Claude Code session ends - * - * This hook runs when a Claude Code session ends. It: - * 1. Finds active SDK session for this Claude session - * 2. Marks session as completed in database - * 3. Allows worker to finish pending operations naturally - */ -export async function cleanupHook(input?: SessionEndInput): Promise { - try { - // Log hook entry point - console.error('[claude-mem cleanup] Hook fired', { - input: input ? { - session_id: input.session_id, - cwd: input.cwd, - reason: input.reason - } : null - }); - - // Handle standalone execution (no input provided) - if (!input) { - console.log('No input provided - this script is designed to run as a Claude Code SessionEnd hook'); - console.log('\nExpected input format:'); - console.log(JSON.stringify({ - session_id: "string", - cwd: "string", - transcript_path: "string", - hook_event_name: "SessionEnd", - reason: "exit" - }, null, 2)); - process.exit(0); - } - - const { session_id, reason } = input; - console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason }); - - // Ensure worker is running first (runs cleanup if restarting) - const workerReady = await ensureWorkerRunning(); - if (!workerReady) { - console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup'); - } - - // Find active SDK session - const db = new SessionStore(); - const session = db.findActiveSDKSession(session_id); - - if (!session) { - // No active session - nothing to clean up - console.error('[claude-mem cleanup] No active SDK session found', { session_id }); - db.close(); - console.log('{"continue": true, "suppressOutput": true}'); - process.exit(0); - } - - console.error('[claude-mem cleanup] Active SDK session found', { - session_id: session.id, - sdk_session_id: session.sdk_session_id, - project: session.project, - worker_port: session.worker_port - }); - - // 1. Mark session as completed in DB (if not already completed) - try { - db.markSessionCompleted(session.id); - console.error('[claude-mem cleanup] Session marked as completed in database'); - } catch (markErr: any) { - console.error('[claude-mem cleanup] Failed to mark session as completed:', markErr); - } - - db.close(); - - console.error('[claude-mem cleanup] Cleanup completed successfully'); - console.log('{"continue": true, "suppressOutput": true}'); - process.exit(0); - - } catch (error: any) { - // On error, don't block Claude Code exit - console.error('[claude-mem cleanup] Unexpected error in hook', { - error: error.message, - stack: error.stack, - name: error.name - }); - console.log('{"continue": true, "suppressOutput": true}'); - process.exit(0); - } -} diff --git a/src/hooks/context-hook.ts b/src/hooks/context-hook.ts new file mode 100644 index 00000000..315d74a8 --- /dev/null +++ b/src/hooks/context-hook.ts @@ -0,0 +1,440 @@ +#!/usr/bin/env node +/** + * Context Hook - SessionStart + * Consolidated entry point + logic (no try-catch bullshit) + */ + +import path from 'path'; +import { stdin } from 'process'; +import { SessionStore } from '../services/sqlite/SessionStore.js'; +import { ensureWorkerRunning } from '../shared/worker-utils.js'; + +export interface SessionStartInput { + session_id?: string; + transcript_path?: string; + cwd?: string; + hook_event_name?: string; + source?: "startup" | "resume" | "clear" | "compact"; + [key: string]: any; +} + +// ANSI color codes for terminal output +const colors = { + 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', +}; + +interface Observation { + id: number; + sdk_session_id: string; + type: string; + title: string | null; + subtitle: string | null; + narrative: string | null; + facts: string | null; + concepts: string | null; + files_read: string | null; + files_modified: string | null; + created_at: string; + created_at_epoch: number; +} + +// Helper: Parse JSON array safely +function parseJsonArray(json: string | null): string[] { + if (!json) return []; + const parsed = JSON.parse(json); + return Array.isArray(parsed) ? parsed : []; +} + +// Helper: Format date with time +function formatDateTime(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +} + +// Helper: Format just time (no date) +function formatTime(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true + }); +} + +// Helper: Format just date +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }); +} + +// Helper: Estimate token count for text +function estimateTokens(text: string | null): number { + if (!text) return 0; + // Rough estimate: ~4 characters per token + return Math.ceil(text.length / 4); +} + +// Helper: Convert absolute paths to relative paths +function toRelativePath(filePath: string, cwd: string): string { + if (path.isAbsolute(filePath)) { + return path.relative(cwd, filePath); + } + return filePath; +} + +// Helper: Get all observations for given sessions +function getObservations(db: SessionStore, sessionIds: string[]): Observation[] { + if (sessionIds.length === 0) return []; + + const placeholders = sessionIds.map(() => '?').join(','); + const observations = db.db.prepare(` + SELECT + id, sdk_session_id, type, title, subtitle, narrative, + facts, concepts, files_read, files_modified, + created_at, created_at_epoch + FROM observations + WHERE sdk_session_id IN (${placeholders}) + ORDER BY created_at_epoch DESC + `).all(...sessionIds) as Observation[]; + + return observations; +} + +/** + * Context Hook Main Logic + */ +function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string { + ensureWorkerRunning(); + const cwd = input?.cwd ?? process.cwd(); + const project = cwd ? path.basename(cwd) : 'unknown-project'; + + const db = new SessionStore(); + + // Get last 4 summaries (use 4th for offset calculation) + const recentSummaries = db.db.prepare(` + SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch + FROM session_summaries + WHERE project = ? + ORDER BY created_at_epoch DESC + LIMIT 4 + `).all(project) as Array<{ id: number; sdk_session_id: string; request: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>; + + if (recentSummaries.length === 0) { + db.close(); + if (useColors) { + return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`; + } + return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`; + } + + // Extract unique session IDs from first 3 summaries + const displaySummaries = recentSummaries.slice(0, 3); + const sessionIds = [...new Set(displaySummaries.map(s => s.sdk_session_id))]; + + // Get all observations from these sessions + const observations = getObservations(db, sessionIds); + + // Filter observations by key concepts for timeline + const timelineObs = observations.filter(obs => { + const concepts = parseJsonArray(obs.concepts); + return concepts.includes('what-changed') || + concepts.includes('how-it-works') || + concepts.includes('problem-solution') || + concepts.includes('gotcha') || + concepts.includes('discovery') || + concepts.includes('why-it-exists') || + concepts.includes('decision') || + concepts.includes('trade-off'); + }); + + // Build output + const output: string[] = []; + + // Header + if (useColors) { + output.push(''); + output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`); + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + output.push(''); + } else { + output.push(`# [${project}] recent context`); + output.push(''); + } + + // Chronological Timeline + if (timelineObs.length > 0) { + // Legend/Key + if (useColors) { + output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off${colors.reset}`); + output.push(''); + } else { + output.push(`**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off`); + output.push(''); + } + + // Progressive Disclosure Usage Instructions + if (useColors) { + output.push(`${colors.dim}💡 Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${colors.reset}`); + output.push(`${colors.dim} → Use MCP search tools to fetch full observation details on-demand (Layer 2)${colors.reset}`); + output.push(`${colors.dim} → Prefer searching observations over re-reading code for past decisions and learnings${colors.reset}`); + output.push(`${colors.dim} → Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately${colors.reset}`); + output.push(''); + } else { + output.push(`💡 **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts).`); + output.push(`- Use MCP search tools to fetch full observation details on-demand (Layer 2)`); + output.push(`- Prefer searching observations over re-reading code for past decisions and learnings`); + output.push(`- Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately`); + output.push(''); + } + + // Create unified timeline with both observations and summaries + const mostRecentSummaryId = recentSummaries[0]?.id; + + // Create offset summaries + const summariesWithOffset = displaySummaries.map((summary, i) => { + // Most recent keeps its own time, others offset to next summary's time + const nextSummary = i === 0 ? null : recentSummaries[i + 1]; + return { + ...summary, + displayEpoch: nextSummary ? nextSummary.created_at_epoch : summary.created_at_epoch, + displayTime: nextSummary ? nextSummary.created_at : summary.created_at, + isMostRecent: summary.id === mostRecentSummaryId + }; + }); + + type TimelineItem = + | { type: 'observation'; data: Observation } + | { type: 'summary'; data: typeof summariesWithOffset[0] }; + + const timeline: TimelineItem[] = [ + ...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })), + ...summariesWithOffset.map(summary => ({ type: 'summary' as const, data: summary })) + ]; + + // Sort chronologically + timeline.sort((a, b) => { + const aEpoch = a.type === 'observation' ? a.data.created_at_epoch : a.data.displayEpoch; + const bEpoch = b.type === 'observation' ? b.data.created_at_epoch : b.data.displayEpoch; + return aEpoch - bEpoch; + }); + + // Group by day for rendering + const dayTimelines = new Map(); + for (const item of timeline) { + const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime; + const day = formatDate(itemDate); + if (!dayTimelines.has(day)) { + dayTimelines.set(day, []); + } + dayTimelines.get(day)!.push(item); + } + + // Sort days chronologically + const sortedDays = Array.from(dayTimelines.entries()).sort((a, b) => { + const aDate = new Date(a[0]).getTime(); + const bDate = new Date(b[0]).getTime(); + return aDate - bDate; + }); + + // Render each day's timeline + for (const [day, dayItems] of sortedDays) { + // Day header + if (useColors) { + output.push(`${colors.bright}${colors.cyan}${day}${colors.reset}`); + output.push(''); + } else { + output.push(`### ${day}`); + output.push(''); + } + + // Render items chronologically with visual file grouping + let currentFile: string | null = null; + let lastTime = ''; + let tableOpen = false; + + for (const item of dayItems) { + if (item.type === 'summary') { + // Close any open table + if (tableOpen) { + output.push(''); + tableOpen = false; + currentFile = null; + lastTime = ''; + } + + // Render summary + const summary = item.data; + const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`; + const link = summary.isMostRecent ? '' : `claude-mem://session-summary/${summary.id}`; + + if (useColors) { + const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : ''; + output.push(`🎯 ${colors.yellow}#S${summary.id}${colors.reset} ${summaryTitle} ${linkPart}`); + } else { + const linkPart = link ? ` [→](${link})` : ''; + output.push(`**🎯 #S${summary.id}** ${summaryTitle}${linkPart}`); + } + output.push(''); + } else { + // Render observation + const obs = item.data; + const files = parseJsonArray(obs.files_modified); + const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General'; + + // Check if we need a new file section + if (file !== currentFile) { + // Close previous table + if (tableOpen) { + output.push(''); + } + + // File header + if (useColors) { + output.push(`${colors.dim}${file}${colors.reset}`); + } else { + output.push(`**${file}**`); + } + + // Table header (markdown only) + if (!useColors) { + output.push(`| ID | Time | T | Title | Tokens |`); + output.push(`|----|------|---|-------|--------|`); + } + + currentFile = file; + tableOpen = true; + lastTime = ''; + } + + // Render observation row + const concepts = parseJsonArray(obs.concepts); + let icon = '•'; + + // Priority order: gotcha > decision > trade-off > problem-solution > discovery > why-it-exists > how-it-works > what-changed + if (concepts.includes('gotcha')) { + icon = '🔴'; + } else if (concepts.includes('decision')) { + icon = '🟤'; + } else if (concepts.includes('trade-off')) { + icon = '⚖️'; + } else if (concepts.includes('problem-solution')) { + icon = '🟡'; + } else if (concepts.includes('discovery')) { + icon = '🟣'; + } else if (concepts.includes('why-it-exists')) { + icon = '🟠'; + } else if (concepts.includes('how-it-works')) { + icon = '🔵'; + } else if (concepts.includes('what-changed')) { + icon = '🟢'; + } + + const time = formatTime(obs.created_at); + const title = obs.title || 'Untitled'; + const tokens = estimateTokens(obs.narrative); + + const showTime = time !== lastTime; + const timeDisplay = showTime ? time : ''; + lastTime = time; + + if (useColors) { + const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length); + const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : ''; + output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`); + } else { + output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`); + } + } + } + + // Close final table if open + if (tableOpen) { + output.push(''); + } + } + + // Add full summary details for most recent session + const mostRecentSummary = recentSummaries[0]; + if (mostRecentSummary && (mostRecentSummary.completed || mostRecentSummary.next_steps)) { + if (mostRecentSummary.completed) { + if (useColors) { + output.push(`${colors.green}Completed:${colors.reset} ${mostRecentSummary.completed}`); + } else { + output.push(`**Completed**: ${mostRecentSummary.completed}`); + } + output.push(''); + } + + if (mostRecentSummary.next_steps) { + if (useColors) { + output.push(`${colors.magenta}Next Steps:${colors.reset} ${mostRecentSummary.next_steps}`); + } else { + output.push(`**Next Steps**: ${mostRecentSummary.next_steps}`); + } + output.push(''); + } + } + + // Footer with MCP search instructions + if (useColors) { + output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`); + } else { + output.push(`*Use claude-mem MCP search to access records with the given ID*`); + } + output.push(''); + } + + // Footer + if (useColors) { + output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); + output.push(''); + } + + db.close(); + return output.join('\n'); +} + +// Entry Point - handle stdin/stdout +const useIndexView = process.argv.includes('--index'); + +if (stdin.isTTY) { + // Running manually from terminal - print formatted output with colors + const contextOutput = contextHook(undefined, true, useIndexView); + console.log(contextOutput); + process.exit(0); +} else { + // Running from hook - wrap in hookSpecificOutput JSON format + let input = ''; + stdin.on('data', (chunk) => input += chunk); + stdin.on('end', () => { + const parsed = input.trim() ? JSON.parse(input) : undefined; + const contextOutput = contextHook(parsed, false, useIndexView); + const result = { + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: contextOutput + } + }; + console.log(JSON.stringify(result)); + process.exit(0); + }); +} diff --git a/src/hooks/context.ts b/src/hooks/context.ts deleted file mode 100644 index 53755f24..00000000 --- a/src/hooks/context.ts +++ /dev/null @@ -1,449 +0,0 @@ -import path from 'path'; -import { SessionStore } from '../services/sqlite/SessionStore.js'; -import { ensureWorkerRunning } from '../shared/worker-utils.js'; - -export interface SessionStartInput { - session_id?: string; - transcript_path?: string; - cwd?: string; - hook_event_name?: string; - source?: "startup" | "resume" | "clear" | "compact"; - [key: string]: any; -} - -// ANSI color codes for terminal output -const colors = { - 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', -}; - -interface Observation { - id: number; - sdk_session_id: string; - type: string; - title: string | null; - subtitle: string | null; - narrative: string | null; - facts: string | null; - concepts: string | null; - files_read: string | null; - files_modified: string | null; - created_at: string; - created_at_epoch: number; -} - - -/** - * Helper: Parse JSON array safely - */ -function parseJsonArray(json: string | null): string[] { - if (!json) return []; - try { - const parsed = JSON.parse(json); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} - -/** - * Helper: Format date with time - */ -function formatDateTime(dateStr: string): string { - const date = new Date(dateStr); - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }); -} - -/** - * Helper: Format just time (no date) - */ -function formatTime(dateStr: string): string { - const date = new Date(dateStr); - return date.toLocaleString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true - }); -} - -/** - * Helper: Format just date - */ -function formatDate(dateStr: string): string { - const date = new Date(dateStr); - return date.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); -} - -/** - * Helper: Estimate token count for text - */ -function estimateTokens(text: string | null): number { - if (!text) return 0; - // Rough estimate: ~4 characters per token - return Math.ceil(text.length / 4); -} - -/** - * Helper: Convert absolute paths to relative paths - */ -function toRelativePath(filePath: string, cwd: string): string { - try { - if (path.isAbsolute(filePath)) { - return path.relative(cwd, filePath); - } - return filePath; - } catch { - return filePath; - } -} - -/** - * Helper: Get recent session IDs for a project - */ -function getRecentSessionIds(db: SessionStore, project: string, limit: number = 3): string[] { - const sessions = db.db.prepare(` - SELECT sdk_session_id - FROM sdk_sessions - WHERE project = ? AND sdk_session_id IS NOT NULL - ORDER BY started_at_epoch DESC - LIMIT ? - `).all(project, limit) as Array<{ sdk_session_id: string }>; - - return sessions.map(s => s.sdk_session_id); -} - -/** - * Helper: Get all observations for given sessions - */ -function getObservations(db: SessionStore, sessionIds: string[]): Observation[] { - if (sessionIds.length === 0) return []; - - const placeholders = sessionIds.map(() => '?').join(','); - const observations = db.db.prepare(` - SELECT - id, sdk_session_id, type, title, subtitle, narrative, - facts, concepts, files_read, files_modified, - created_at, created_at_epoch - FROM observations - WHERE sdk_session_id IN (${placeholders}) - ORDER BY created_at_epoch DESC - `).all(...sessionIds) as Observation[]; - - return observations; -} - - -/** - * Context Hook - SessionStart - * Shows user what happened in recent sessions - */ -export function contextHook(input?: SessionStartInput, useColors: boolean = false, useIndexView: boolean = false): string { - ensureWorkerRunning(); - const cwd = input?.cwd ?? process.cwd(); - const project = cwd ? path.basename(cwd) : 'unknown-project'; - - const db = new SessionStore(); - - try { - // Get last 4 summaries (use 4th for offset calculation) - const recentSummaries = db.db.prepare(` - SELECT id, sdk_session_id, request, completed, next_steps, created_at, created_at_epoch - FROM session_summaries - WHERE project = ? - ORDER BY created_at_epoch DESC - LIMIT 4 - `).all(project) as Array<{ id: number; sdk_session_id: string; request: string | null; completed: string | null; next_steps: string | null; created_at: string; created_at_epoch: number }>; - - if (recentSummaries.length === 0) { - if (useColors) { - return `\n${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}\n${colors.gray}${'─'.repeat(60)}${colors.reset}\n\n${colors.dim}No previous sessions found for this project yet.${colors.reset}\n`; - } - return `# [${project}] recent context\n\nNo previous sessions found for this project yet.`; - } - - // Extract unique session IDs from first 3 summaries - const displaySummaries = recentSummaries.slice(0, 3); - const sessionIds = [...new Set(displaySummaries.map(s => s.sdk_session_id))]; - - // Get all observations from these sessions - const observations = getObservations(db, sessionIds); - - // Filter observations by key concepts for timeline - const timelineObs = observations.filter(obs => { - const concepts = parseJsonArray(obs.concepts); - return concepts.includes('what-changed') || - concepts.includes('how-it-works') || - concepts.includes('problem-solution') || - concepts.includes('gotcha') || - concepts.includes('discovery') || - concepts.includes('why-it-exists') || - concepts.includes('decision') || - concepts.includes('trade-off'); - }); - - // Build output - const output: string[] = []; - - // Header - if (useColors) { - output.push(''); - output.push(`${colors.bright}${colors.cyan}📝 [${project}] recent context${colors.reset}`); - output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); - output.push(''); - } else { - output.push(`# [${project}] recent context`); - output.push(''); - } - - // Chronological Timeline - if (timelineObs.length > 0) { - // Legend/Key - if (useColors) { - output.push(`${colors.dim}Legend: 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off${colors.reset}`); - output.push(''); - } else { - output.push(`**Legend:** 🎯 session-request | 🔴 gotcha | 🟡 problem-solution | 🔵 how-it-works | 🟢 what-changed | 🟣 discovery | 🟠 why-it-exists | 🟤 decision | ⚖️ trade-off`); - output.push(''); - } - - // Progressive Disclosure Usage Instructions - if (useColors) { - output.push(`${colors.dim}💡 Progressive Disclosure: This index shows WHAT exists (titles) and retrieval COST (token counts).${colors.reset}`); - output.push(`${colors.dim} → Use MCP search tools to fetch full observation details on-demand (Layer 2)${colors.reset}`); - output.push(`${colors.dim} → Prefer searching observations over re-reading code for past decisions and learnings${colors.reset}`); - output.push(`${colors.dim} → Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately${colors.reset}`); - output.push(''); - } else { - output.push(`💡 **Progressive Disclosure:** This index shows WHAT exists (titles) and retrieval COST (token counts).`); - output.push(`- Use MCP search tools to fetch full observation details on-demand (Layer 2)`); - output.push(`- Prefer searching observations over re-reading code for past decisions and learnings`); - output.push(`- Critical types (🔴 gotcha, 🟤 decision, ⚖️ trade-off) often worth fetching immediately`); - output.push(''); - } - - // Create unified timeline with both observations and summaries - const mostRecentSummaryId = recentSummaries[0]?.id; - - // Create offset summaries (displaySummaries already defined at top) - const summariesWithOffset = displaySummaries.map((summary, i) => { - // Most recent keeps its own time, others offset to next summary's time - const nextSummary = i === 0 ? null : recentSummaries[i + 1]; - return { - ...summary, - displayEpoch: nextSummary ? nextSummary.created_at_epoch : summary.created_at_epoch, - displayTime: nextSummary ? nextSummary.created_at : summary.created_at, - isMostRecent: summary.id === mostRecentSummaryId - }; - }); - - type TimelineItem = - | { type: 'observation'; data: Observation } - | { type: 'summary'; data: typeof summariesWithOffset[0] }; - - const timeline: TimelineItem[] = [ - ...timelineObs.map(obs => ({ type: 'observation' as const, data: obs })), - ...summariesWithOffset.map(summary => ({ type: 'summary' as const, data: summary })) - ]; - - // Sort chronologically - timeline.sort((a, b) => { - const aEpoch = a.type === 'observation' ? a.data.created_at_epoch : a.data.displayEpoch; - const bEpoch = b.type === 'observation' ? b.data.created_at_epoch : b.data.displayEpoch; - return aEpoch - bEpoch; - }); - - // Group by day for rendering - const dayTimelines = new Map(); - for (const item of timeline) { - const itemDate = item.type === 'observation' ? item.data.created_at : item.data.displayTime; - const day = formatDate(itemDate); - if (!dayTimelines.has(day)) { - dayTimelines.set(day, []); - } - dayTimelines.get(day)!.push(item); - } - - // Sort days chronologically - const sortedDays = Array.from(dayTimelines.entries()).sort((a, b) => { - const aDate = new Date(a[0]).getTime(); - const bDate = new Date(b[0]).getTime(); - return aDate - bDate; - }); - - // Render each day's timeline - for (const [day, dayItems] of sortedDays) { - // Day header - if (useColors) { - output.push(`${colors.bright}${colors.cyan}${day}${colors.reset}`); - output.push(''); - } else { - output.push(`### ${day}`); - output.push(''); - } - - // Render items chronologically with visual file grouping - let currentFile: string | null = null; - let lastTime = ''; - let tableOpen = false; - - for (const item of dayItems) { - if (item.type === 'summary') { - // Close any open table - if (tableOpen) { - output.push(''); - tableOpen = false; - currentFile = null; - lastTime = ''; - } - - // Render summary - const summary = item.data; - const summaryTitle = `${summary.request || 'Session started'} (${formatDateTime(summary.displayTime)})`; - const link = summary.isMostRecent ? '' : `claude-mem://session-summary/${summary.id}`; - - if (useColors) { - const linkPart = link ? `${colors.dim}[${link}]${colors.reset}` : ''; - output.push(`🎯 ${colors.yellow}#S${summary.id}${colors.reset} ${summaryTitle} ${linkPart}`); - } else { - const linkPart = link ? ` [→](${link})` : ''; - output.push(`**🎯 #S${summary.id}** ${summaryTitle}${linkPart}`); - } - output.push(''); - } else { - // Render observation - const obs = item.data; - const files = parseJsonArray(obs.files_modified); - const file = files.length > 0 ? toRelativePath(files[0], cwd) : 'General'; - - // Check if we need a new file section - if (file !== currentFile) { - // Close previous table - if (tableOpen) { - output.push(''); - } - - // File header - if (useColors) { - output.push(`${colors.dim}${file}${colors.reset}`); - } else { - output.push(`**${file}**`); - } - - // Table header (markdown only) - if (!useColors) { - output.push(`| ID | Time | T | Title | Tokens |`); - output.push(`|----|------|---|-------|--------|`); - } - - currentFile = file; - tableOpen = true; - lastTime = ''; - } - - // Render observation row - const concepts = parseJsonArray(obs.concepts); - let icon = '•'; - - // Priority order: gotcha > decision > trade-off > problem-solution > discovery > why-it-exists > how-it-works > what-changed - if (concepts.includes('gotcha')) { - icon = '🔴'; - } else if (concepts.includes('decision')) { - icon = '🟤'; - } else if (concepts.includes('trade-off')) { - icon = '⚖️'; - } else if (concepts.includes('problem-solution')) { - icon = '🟡'; - } else if (concepts.includes('discovery')) { - icon = '🟣'; - } else if (concepts.includes('why-it-exists')) { - icon = '🟠'; - } else if (concepts.includes('how-it-works')) { - icon = '🔵'; - } else if (concepts.includes('what-changed')) { - icon = '🟢'; - } - - const time = formatTime(obs.created_at); - const title = obs.title || 'Untitled'; - const tokens = estimateTokens(obs.narrative); - - const showTime = time !== lastTime; - const timeDisplay = showTime ? time : ''; - lastTime = time; - - if (useColors) { - const timePart = showTime ? `${colors.dim}${time}${colors.reset}` : ' '.repeat(time.length); - const tokensPart = tokens > 0 ? `${colors.dim}(~${tokens}t)${colors.reset}` : ''; - output.push(` ${colors.dim}#${obs.id}${colors.reset} ${timePart} ${icon} ${title} ${tokensPart}`); - } else { - output.push(`| #${obs.id} | ${timeDisplay || '″'} | ${icon} | ${title} | ~${tokens} |`); - } - } - } - - // Close final table if open - if (tableOpen) { - output.push(''); - } - } - - // Add full summary details for most recent session - const mostRecentSummary = recentSummaries[0]; - if (mostRecentSummary && (mostRecentSummary.completed || mostRecentSummary.next_steps)) { - if (mostRecentSummary.completed) { - if (useColors) { - output.push(`${colors.green}Completed:${colors.reset} ${mostRecentSummary.completed}`); - } else { - output.push(`**Completed**: ${mostRecentSummary.completed}`); - } - output.push(''); - } - - if (mostRecentSummary.next_steps) { - if (useColors) { - output.push(`${colors.magenta}Next Steps:${colors.reset} ${mostRecentSummary.next_steps}`); - } else { - output.push(`**Next Steps**: ${mostRecentSummary.next_steps}`); - } - output.push(''); - } - } - - // Footer with MCP search instructions - if (useColors) { - output.push(`${colors.dim}Use claude-mem MCP search to access records with the given ID${colors.reset}`); - } else { - output.push(`*Use claude-mem MCP search to access records with the given ID*`); - } - output.push(''); - } - - // Footer - if (useColors) { - output.push(`${colors.gray}${'─'.repeat(60)}${colors.reset}`); - output.push(''); - } - - return output.join('\n'); - } finally { - db.close(); - } -} diff --git a/src/hooks/new-hook.ts b/src/hooks/new-hook.ts new file mode 100644 index 00000000..b9808c1c --- /dev/null +++ b/src/hooks/new-hook.ts @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * New Hook - UserPromptSubmit + * Consolidated entry point + logic + */ + +import path from 'path'; +import { stdin } from 'process'; +import { SessionStore } from '../services/sqlite/SessionStore.js'; +import { createHookResponse } from './hook-response.js'; +import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; + +export interface UserPromptSubmitInput { + session_id: string; + cwd: string; + prompt: string; + [key: string]: any; +} + +/** + * New Hook Main Logic + */ +async function newHook(input?: UserPromptSubmitInput): Promise { + if (!input) { + throw new Error('newHook requires input'); + } + + const { session_id, cwd, prompt } = input; + const project = path.basename(cwd); + + // Ensure worker is running first + const workerReady = await ensureWorkerRunning(); + if (!workerReady) { + throw new Error('Worker service failed to start or become healthy'); + } + + const db = new SessionStore(); + + // Save session_id for indexing + const sessionDbId = db.createSDKSession(session_id, project, prompt); + const promptNumber = db.incrementPromptCounter(sessionDbId); + + // Save raw user prompt for full-text search + db.saveUserPrompt(session_id, promptNumber, prompt); + + console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`); + + db.close(); + + // Get fixed port + const port = getWorkerPort(); + + // Initialize session via HTTP + const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ project, userPrompt: prompt }), + signal: AbortSignal.timeout(5000) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to initialize session: ${response.status} ${errorText}`); + } + + console.log(createHookResponse('UserPromptSubmit', true)); +} + +// Entry Point +let input = ''; +stdin.on('data', (chunk) => input += chunk); +stdin.on('end', async () => { + const parsed = input ? JSON.parse(input) : undefined; + await newHook(parsed); +}); diff --git a/src/hooks/new.ts b/src/hooks/new.ts deleted file mode 100644 index 791eed3c..00000000 --- a/src/hooks/new.ts +++ /dev/null @@ -1,63 +0,0 @@ -import path from 'path'; -import { SessionStore } from '../services/sqlite/SessionStore.js'; -import { createHookResponse } from './hook-response.js'; -import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; - -export interface UserPromptSubmitInput { - session_id: string; - cwd: string; - prompt: string; - [key: string]: any; -} - -/** - * New Hook - UserPromptSubmit - * Initializes SDK memory session via HTTP POST to worker service - */ -export async function newHook(input?: UserPromptSubmitInput): Promise { - if (!input) { - throw new Error('newHook requires input'); - } - - const { session_id, cwd, prompt } = input; - const project = path.basename(cwd); - - // Ensure worker is running first (runs cleanup if restarting) - const workerReady = await ensureWorkerRunning(); - if (!workerReady) { - throw new Error('Worker service failed to start or become healthy'); - } - - const db = new SessionStore(); - - try { - // Just save session_id for indexing - no validation, no state management - const sessionDbId = db.createSDKSession(session_id, project, prompt); - const promptNumber = db.incrementPromptCounter(sessionDbId); - - // Save raw user prompt for full-text search - db.saveUserPrompt(session_id, promptNumber, prompt); - - console.error(`[new-hook] Session ${sessionDbId}, prompt #${promptNumber}`); - - // Get fixed port - const port = getWorkerPort(); - - // Initialize session via HTTP - const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ project, userPrompt: prompt }), - signal: AbortSignal.timeout(5000) - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to initialize session: ${response.status} ${errorText}`); - } - - console.log(createHookResponse('UserPromptSubmit', true)); - } finally { - db.close(); - } -} diff --git a/src/hooks/save.ts b/src/hooks/save-hook.ts similarity index 80% rename from src/hooks/save.ts rename to src/hooks/save-hook.ts index cea69ca0..b6306b2c 100644 --- a/src/hooks/save.ts +++ b/src/hooks/save-hook.ts @@ -1,3 +1,10 @@ +#!/usr/bin/env node +/** + * Save Hook - PostToolUse + * Consolidated entry point + logic + */ + +import { stdin } from 'process'; import { SessionStore } from '../services/sqlite/SessionStore.js'; import { createHookResponse } from './hook-response.js'; import { logger } from '../utils/logger.js'; @@ -18,10 +25,9 @@ const SKIP_TOOLS = new Set([ ]); /** - * Save Hook - PostToolUse - * Sends tool observations to worker via HTTP POST + * Save Hook Main Logic */ -export async function saveHook(input?: PostToolUseInput): Promise { +async function saveHook(input?: PostToolUseInput): Promise { if (!input) { throw new Error('saveHook requires input'); } @@ -33,7 +39,7 @@ export async function saveHook(input?: PostToolUseInput): Promise { return; } - // Ensure worker is running first (runs cleanup if restarting) + // Ensure worker is running first const workerReady = await ensureWorkerRunning(); if (!workerReady) { throw new Error('Worker service failed to start or become healthy'); @@ -41,14 +47,14 @@ export async function saveHook(input?: PostToolUseInput): Promise { const db = new SessionStore(); - // Get or create session - no validation, just use the session_id from hook - const sessionDbId = db.createSDKSession(session_id, '', ''); // project and prompt not needed for observations + // Get or create session + const sessionDbId = db.createSDKSession(session_id, '', ''); const promptNumber = db.getPromptCounter(sessionDbId); db.close(); const toolStr = logger.formatTool(tool_name, tool_input); - // Use fixed worker port - no session.worker_port validation needed + // Use fixed worker port const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10); logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, { @@ -80,3 +86,11 @@ export async function saveHook(input?: PostToolUseInput): Promise { logger.debug('HOOK', 'Observation sent successfully', { sessionId: sessionDbId, toolName: tool_name }); console.log(createHookResponse('PostToolUse', true)); } + +// Entry Point +let input = ''; +stdin.on('data', (chunk) => input += chunk); +stdin.on('end', async () => { + const parsed = input ? JSON.parse(input) : undefined; + await saveHook(parsed); +}); diff --git a/src/hooks/summary.ts b/src/hooks/summary-hook.ts similarity index 78% rename from src/hooks/summary.ts rename to src/hooks/summary-hook.ts index e02467bb..56ebb591 100644 --- a/src/hooks/summary.ts +++ b/src/hooks/summary-hook.ts @@ -1,3 +1,10 @@ +#!/usr/bin/env node +/** + * Summary Hook - Stop + * Consolidated entry point + logic + */ + +import { stdin } from 'process'; import { SessionStore } from '../services/sqlite/SessionStore.js'; import { createHookResponse } from './hook-response.js'; import { logger } from '../utils/logger.js'; @@ -10,17 +17,16 @@ export interface StopInput { } /** - * Summary Hook - Stop - * Sends SUMMARIZE message to worker via HTTP POST (not finalize - keeps SDK agent running) + * Summary Hook Main Logic */ -export async function summaryHook(input?: StopInput): Promise { +async function summaryHook(input?: StopInput): Promise { if (!input) { throw new Error('summaryHook requires input'); } const { session_id } = input; - // Ensure worker is running first (runs cleanup if restarting) + // Ensure worker is running first const workerReady = await ensureWorkerRunning(); if (!workerReady) { throw new Error('Worker service failed to start or become healthy'); @@ -28,12 +34,12 @@ export async function summaryHook(input?: StopInput): Promise { const db = new SessionStore(); - // Get or create session - no validation, just use the session_id from hook + // Get or create session const sessionDbId = db.createSDKSession(session_id, '', ''); const promptNumber = db.getPromptCounter(sessionDbId); db.close(); - // Use fixed worker port - no session.worker_port validation needed + // Use fixed worker port const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10); logger.dataIn('HOOK', 'Stop: Requesting summary', { @@ -61,3 +67,11 @@ export async function summaryHook(input?: StopInput): Promise { logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId }); console.log(createHookResponse('Stop', true)); } + +// Entry Point +let input = ''; +stdin.on('data', (chunk) => input += chunk); +stdin.on('end', async () => { + const parsed = input ? JSON.parse(input) : undefined; + await summaryHook(parsed); +});