Refactor code structure for improved readability and maintainability

This commit is contained in:
Alex Newman
2025-10-19 01:45:51 -04:00
parent cbd43240c7
commit daf368e343
15 changed files with 94 additions and 79 deletions

View File

@@ -452,11 +452,9 @@ claude-mem/
│ ├── save-hook.js
│ ├── summary-hook.js
│ ├── cleanup-hook.js
│ ├── worker-service.cjs # Background worker
│ └── search-server.js # MCP search server
├── dist/ # Built output
│ └── worker-service.cjs
├── tests/ # Test suite
├── context/ # Architecture docs
└── ecosystem.config.cjs # PM2 configuration
@@ -589,7 +587,7 @@ The build process:
1. Compiles TypeScript to JavaScript using esbuild
2. Creates standalone executables for each hook in `plugin/scripts/`
3. Bundles MCP search server to `plugin/scripts/search-server.js`
4. Bundles worker service to `dist/worker-service.cjs`
4. Bundles worker service to `plugin/scripts/worker-service.cjs`
### Running Tests

View File

@@ -18,7 +18,7 @@ const logDir = path.join(os.homedir(), '.claude-mem', 'logs');
module.exports = {
apps: [{
name: 'claude-mem-worker',
script: './dist/worker-service.cjs',
script: './plugin/scripts/worker-service.cjs',
interpreter: 'node',
instances: 1,
exec_mode: 'fork',

View File

@@ -36,7 +36,7 @@
"scripts": {
"build": "node scripts/build-hooks.js",
"build:hooks": "node scripts/build-hooks.js",
"publish:npm": "node scripts/publish.js",
"release": "node scripts/publish.js",
"prepublishOnly": "npm run build",
"test": "node --test tests/",
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null",
@@ -66,7 +66,6 @@
},
"files": [
"plugin",
"dist",
"src",
"scripts",
"docs",

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import x from"better-sqlite3";import{join as i,dirname as C,basename as P}from"path";import{homedir as h}from"os";import{existsSync as j,mkdirSync as D}from"fs";import{fileURLToPath as k}from"url";function y(){return typeof __dirname<"u"?__dirname:C(k(import.meta.url))}var W=y(),a=process.env.CLAUDE_MEM_DATA_DIR||i(h(),".claude-mem"),m=process.env.CLAUDE_CONFIG_DIR||i(h(),".claude"),K=i(a,"archives"),Y=i(a,"logs"),q=i(a,"trash"),V=i(a,"backups"),J=i(a,"settings.json"),I=i(a,"claude-mem.db"),Q=i(m,"settings.json"),z=i(m,"commands"),Z=i(m,"CLAUDE.md");function R(o){D(o,{recursive:!0})}var u=(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))(u||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=u[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,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),p=u[e].padEnd(5),c=t.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let l="";n!=null&&(this.level===0&&typeof n=="object"?l=`
`+JSON.stringify(n,null,2):l=" "+this.formatData(n));let S="";if(r){let{sessionId:U,sdkSessionId:X,correlationId:w,...b}=r;Object.keys(b).length>0&&(S=` {${Object.entries(b).map(([v,A])=>`${v}=${A}`).join(", ")}}`)}let N=`[${d}] [${p}] [${c}] ${_}${s}${S}${l}`;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`})}},O=new T;var E=class{db;constructor(){R(a),this.db=new x(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()}initializeSchema(){try{this.db.exec(`
import F from"better-sqlite3";import{join as a,dirname as U,basename as q}from"path";import{homedir as R}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 P=M(),d=process.env.CLAUDE_MEM_DATA_DIR||a(R(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(R(),".claude"),ee=a(d,"archives"),se=a(d,"logs"),te=a(d,"trash"),re=a(d,"backups"),ne=a(d,"settings.json"),I=a(d,"claude-mem.db"),oe=a(u,"settings.json"),ie=a(u,"commands"),ae=a(u,"CLAUDE.md");function O(o){w(o,{recursive:!0})}function L(){return a(P,"..","..")}var _=(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=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,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),p=_[e].padEnd(5),c=t.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let m="";n!=null&&(this.level===0&&typeof n=="object"?m=`
`+JSON.stringify(n,null,2):m=" "+this.formatData(n));let h="";if(r){let{sessionId:$,sdkSessionId:B,correlationId:G,...f}=r;Object.keys(f).length>0&&(h=` {${Object.entries(f).map(([y,x])=>`${y}=${x}`).join(", ")}}`)}let N=`[${i}] [${p}] [${c}] ${E}${s}${h}${m}`;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`})}},v=new T;var l=class{db;constructor(){O(d),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()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -208,7 +208,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).run(t,e).changes===0?(O.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?(v.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 = ?
@@ -217,17 +217,17 @@ ${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}storeObservation(e,t,s,r){let n=new Date,d=n.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,t,s,r){let n=new Date,i=n.getTime();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,n.toISOString(),d)}storeSummary(e,t,s,r){let n=new Date,d=n.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,n.toISOString(),i)}storeSummary(e,t,s,r){let n=new Date,i=n.getTime();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,n.toISOString(),d)}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,n.toISOString(),i)}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 = ?
@@ -239,5 +239,5 @@ ${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()}};async function f(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:t}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new E,r=s.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),s.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{s.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}s.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 L}from"process";var g="";L.on("data",o=>g+=o);L.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await f(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import g from"path";import{existsSync as S}from"fs";import{spawn as H}from"child_process";var W=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${W}/health`;async function A(){try{return(await fetch(j,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function k(){try{if(await A())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=g.join(o,"plugin","scripts","worker-service.cjs");if(!S(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=g.join(o,"ecosystem.config.cjs"),s=g.join(o,"node_modules",".bin","pm2");if(!S(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!S(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let r=H(s,["start",t],{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 A())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 C(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:t}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new l,r=s.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),s.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{if(!await k())console.error("[claude-mem cleanup] Worker not available - skipping HTTP cleanup");else{let i=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});i.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await i.text())}}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{s.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}s.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 b="";D.on("data",o=>b+=o);D.on("end",async()=>{try{let o=b.trim()?JSON.parse(b):void 0;await C(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});

View File

@@ -239,7 +239,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 g from"path";import{existsSync as h}from"fs";import{spawn as P}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${$}/health`;async function C(){try{return(await fetch(j,{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 a=v(),e=g.join(a,"dist","worker-service.cjs");if(!h(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=g.join(a,"ecosystem.config.cjs"),s=g.join(a,"node_modules",".bin","pm2");if(!h(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!h(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=P(s,["start",t],{detached:!0,stdio:"ignore",cwd:a});n.on("error",r=>{throw new Error(`Failed to spawn PM2: ${r.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let r=0;r<3;r++)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(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}function S(a){k();let e=a?.cwd??process.cwd(),t=e?W.basename(e):"unknown-project",s=new E;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0)return`# Recent Session Context
`).run(e.toISOString(),t).changes}close(){this.db.close()}};import g from"path";import{existsSync as h}from"fs";import{spawn as P}from"child_process";var $=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),j=`http://127.0.0.1:${$}/health`;async function C(){try{return(await fetch(j,{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 a=v(),e=g.join(a,"plugin","scripts","worker-service.cjs");if(!h(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let t=g.join(a,"ecosystem.config.cjs"),s=g.join(a,"node_modules",".bin","pm2");if(!h(s))throw new Error(`PM2 binary not found at ${s}. This is a bundled dependency - try running: npm install`);if(!h(t))throw new Error(`PM2 ecosystem config not found at ${t}. Plugin installation may be corrupted.`);let n=P(s,["start",t],{detached:!0,stdio:"ignore",cwd:a});n.on("error",r=>{throw new Error(`Failed to spawn PM2: ${r.message}`)}),n.unref(),console.error("[claude-mem] Worker started with PM2");for(let r=0;r<3;r++)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(a){return console.error(`[claude-mem] Failed to start worker: ${a.message}`),!1}}function S(a){k();let e=a?.cwd??process.cwd(),t=e?W.basename(e):"unknown-project",s=new E;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0)return`# Recent Session Context
No previous sessions found for this project yet.`;let r=[];r.push("# Recent Session Context"),r.push(""),r.push(`Showing last ${n.length} session(s) for **${t}**:`),r.push("");for(let i of n)if(i.sdk_session_id){if(r.push("---"),r.push(""),i.has_summary){let o=s.getSummaryForSession(i.sdk_session_id);if(o){let d=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${d}**`),r.push(""),o.request&&r.push(`**Request:** ${o.request}`),o.completed&&r.push(`**Completed:** ${o.completed}`),o.learned&&r.push(`**Learned:** ${o.learned}`),o.next_steps&&r.push(`**Next Steps:** ${o.next_steps}`),o.files_read)try{let c=JSON.parse(o.files_read);Array.isArray(c)&&c.length>0&&r.push(`**Files Read:** ${c.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let c=JSON.parse(o.files_edited);Array.isArray(c)&&c.length>0&&r.push(`**Files Edited:** ${c.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}let m=new Date(o.created_at).toLocaleString();r.push(`**Date:** ${m}`)}}else if(i.status==="active"){r.push("**In Progress**"),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`);let o=s.getObservationsForSession(i.sdk_session_id);if(o.length>0){r.push(""),r.push(`**Observations (${o.length}):**`);for(let m of o)r.push(`- ${m.title}`)}else r.push(""),r.push("*No observations yet*");r.push(""),r.push("**Status:** Active - summary pending");let d=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${d}`)}else{let o=i.status==="failed"?"stopped":i.status;r.push(`**${o.charAt(0).toUpperCase()+o.slice(1)}**`),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`),r.push(""),r.push(`**Status:** ${o} - no summary available`);let d=new Date(i.started_at).toLocaleString();r.push(`**Date:** ${d}`)}r.push("")}return r.join(`
`)}finally{s.close()}}import{stdin as f}from"process";try{if(f.isTTY){let e={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S()}};console.log(JSON.stringify(e)),process.exit(0)}else{let a="";f.on("data",e=>a+=e),f.on("end",()=>{let e=a.trim()?JSON.parse(a):void 0,s={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:S(e)}};console.log(JSON.stringify(s)),process.exit(0)})}}catch(a){console.error(`[claude-mem context-hook error: ${a.message}]`),process.exit(0)}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import K from"path";import W from"better-sqlite3";import{join as d,dirname as P,basename as z}from"path";import{homedir as R}from"os";import{existsSync as te,mkdirSync as M}from"fs";import{fileURLToPath as F}from"url";function H(){return typeof __dirname<"u"?__dirname:P(F(import.meta.url))}var $=H(),m=process.env.CLAUDE_MEM_DATA_DIR||d(R(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||d(R(),".claude"),ne=d(m,"archives"),oe=d(m,"logs"),ie=d(m,"trash"),ae=d(m,"backups"),de=d(m,"settings.json"),O=d(m,"claude-mem.db"),pe=d(T,"settings.json"),ce=d(T,"commands"),ue=d(T,"CLAUDE.md");function I(o){M(o,{recursive:!0})}function L(){return d($,"..","..")}var g=(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))(g||{}),S=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,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=g[e].padEnd(5),p=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let c="";if(r){let{sessionId:Y,sdkSessionId:q,correlationId:V,...h}=r;Object.keys(h).length>0&&(c=` {${Object.entries(h).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let u=`[${a}] [${i}] [${p}] ${_}${t}${c}${E}`;e===3?console.error(u):console.log(u)}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`})}},v=new S;var l=class{db;constructor(){I(m),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()}initializeSchema(){try{this.db.exec(`
${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(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=g[e].padEnd(5),p=s.padEnd(6),l="";r?.correlationId?l=`[${r.correlationId}] `:r?.sessionId&&(l=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let c="";if(r){let{sessionId:Y,sdkSessionId:q,correlationId:V,...h}=r;Object.keys(h).length>0&&(c=` {${Object.entries(h).map(([w,X])=>`${w}=${X}`).join(", ")}}`)}let u=`[${a}] [${i}] [${p}] ${l}${t}${c}${E}`;e===3?console.error(u):console.log(u)}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`})}},v=new S;var _=class{db;constructor(){I(m),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()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -239,4 +239,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 j(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 A(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}from"fs";import{spawn as B}from"child_process";var C=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${C}/health`;async function k(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=b.join(o,"dist","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(o,"ecosystem.config.cjs"),t=b.join(o,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(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(a=>setTimeout(a,500)),await k())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 C}async function x(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=K.basename(s),n=new l;try{let a=n.findActiveSDKSession(e),i,p=!1;if(a){i=a.id;let c=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${c}`)}else{let c=n.findAnySDKSession(e);if(c){i=c.id,n.reactivateSession(i,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${u}`)}else{i=n.createSDKSession(e,r,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Created new session ${i}, prompt #${u}`)}}if(!await D())throw new Error("Worker service failed to start or become healthy");let E=y();if(p){let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let u=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${u}`)}}console.log(A("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as U}from"process";var f="";U.on("data",o=>f+=o);U.on("end",async()=>{let o=f.trim()?JSON.parse(f):void 0;await x(o),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function j(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 A(o,e,s={}){let t=j(o,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}from"fs";import{spawn as B}from"child_process";var C=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),G=`http://127.0.0.1:${C}/health`;async function k(){try{return(await fetch(G,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function D(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let o=L(),e=b.join(o,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(o,"ecosystem.config.cjs"),t=b.join(o,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(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(a=>setTimeout(a,500)),await k())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 C}async function x(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=K.basename(s),n=new _;try{let a=n.findActiveSDKSession(e),i,p=!1;if(a){i=a.id;let c=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${c}`)}else{let c=n.findAnySDKSession(e);if(c){i=c.id,n.reactivateSession(i,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${u}`)}else{i=n.createSDKSession(e,r,t);let u=n.incrementPromptCounter(i);p=!0,console.error(`[new-hook] Created new session ${i}, prompt #${u}`)}}if(!await D())throw new Error("Worker service failed to start or become healthy");let E=y();if(p){let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let u=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${u}`)}}console.log(A("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as U}from"process";var f="";U.on("data",o=>f+=o);U.on("end",async()=>{let o=f.trim()?JSON.parse(f):void 0;await x(o),process.exit(0)});

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import U from"better-sqlite3";import{join as a,dirname as k,basename as G}from"path";import{homedir as I}from"os";import{existsSync as K,mkdirSync as D}from"fs";import{fileURLToPath as y}from"url";function x(){return typeof __dirname<"u"?__dirname:k(y(import.meta.url))}var q=x(),c=process.env.CLAUDE_MEM_DATA_DIR||a(I(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||a(I(),".claude"),V=a(c,"archives"),J=a(c,"logs"),Q=a(c,"trash"),z=a(c,"backups"),Z=a(c,"settings.json"),f=a(c,"claude-mem.db"),ee=a(T,"settings.json"),se=a(T,"commands"),te=a(T,"CLAUDE.md");function h(n){D(n,{recursive:!0})}var S=(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))(S||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[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(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),u=S[e].padEnd(5),d=s.padEnd(6),p="";r?.correlationId?p=`[${r.correlationId}] `:r?.sessionId&&(p=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let N="";if(r){let{sessionId:M,sdkSessionId:P,correlationId:F,...R}=r;Object.keys(R).length>0&&(N=` {${Object.entries(R).map(([A,C])=>`${A}=${C}`).join(", ")}}`)}let O=`[${i}] [${u}] [${d}] ${p}${t}${N}${_}`;e===3?console.error(O):console.log(O)}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`})}},E=new g;var m=class{db;constructor(){h(c),this.db=new U(f),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()}initializeSchema(){try{this.db.exec(`
import H from"better-sqlite3";import{join as a,dirname as w,basename as Q}from"path";import{homedir as I}from"os";import{existsSync as se,mkdirSync as X}from"fs";import{fileURLToPath as P}from"url";function M(){return typeof __dirname<"u"?__dirname:w(P(import.meta.url))}var F=M(),c=process.env.CLAUDE_MEM_DATA_DIR||a(I(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||a(I(),".claude"),re=a(c,"archives"),oe=a(c,"logs"),ne=a(c,"trash"),ie=a(c,"backups"),ae=a(c,"settings.json"),L=a(c,"claude-mem.db"),de=a(g,"settings.json"),pe=a(g,"commands"),ce=a(g,"CLAUDE.md");function v(n){X(n,{recursive:!0})}function A(){return a(F,"..","..")}var S=(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))(S||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=S[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(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),E=S[e].padEnd(5),d=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let p="";o!=null&&(this.level===0&&typeof o=="object"?p=`
`+JSON.stringify(o,null,2):p=" "+this.formatData(o));let l="";if(r){let{sessionId:K,sdkSessionId:Y,correlationId:q,...O}=r;Object.keys(O).length>0&&(l=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${i}] [${E}] [${d}] ${m}${t}${l}${p}`;e===3?console.error(h):console.log(h)}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 b;var _=class{db;constructor(){v(c),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()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -208,7 +208,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).run(s,e).changes===0?(E.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
`).run(s,e).changes===0?(u.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
UPDATE sdk_sessions
SET worker_port = ?
WHERE id = ?
@@ -239,4 +239,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 X(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 l(n,e,s={}){let t=X(n,e,s);return JSON.stringify(t)}var w=new Set(["ListMcpResourcesTool"]);async function L(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(w.has(s)){console.log(l("PostToolUse",!0));return}let o=new m,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port)throw o.close(),E.error("HOOK","No worker port for session",{sessionId:i.id}),new Error("No worker port for session - session may not be properly initialized");let u=o.getPromptCounter(i.id);o.close();let d=E.formatTool(s,t);E.dataIn("HOOK",`PostToolUse: ${d}`,{sessionId:i.id,workerPort:i.worker_port});let p=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/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:u}),signal:AbortSignal.timeout(2e3)});if(!p.ok){let _=await p.text();throw E.failure("HOOK","Failed to send observation",{sessionId:i.id,status:p.status},_),new Error(`Failed to send observation to worker: ${p.status} ${_}`)}E.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s}),console.log(l("PostToolUse",!0))}import{stdin as v}from"process";var b="";v.on("data",n=>b+=n);v.on("end",async()=>{let n=b.trim()?JSON.parse(b):void 0;await L(n),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(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 T(n,e,s={}){let t=W(n,e,s);return JSON.stringify(t)}import f from"path";import{existsSync as N}from"fs";import{spawn as $}from"child_process";var j=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${j}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=f.join(n,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=f.join(n,"ecosystem.config.cjs"),t=f.join(n,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(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 k())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 G=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(G.has(s)){console.log(T("PostToolUse",!0));return}let o=new _,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(T("PostToolUse",!0));return}if(!i.worker_port)throw o.close(),u.error("HOOK","No worker port for session",{sessionId:i.id}),new Error("No worker port for session - session may not be properly initialized");let E=o.getPromptCounter(i.id);if(o.close(),!await C())throw new Error("Worker service failed to start or become healthy");let m=u.formatTool(s,t);u.dataIn("HOOK",`PostToolUse: ${m}`,{sessionId:i.id,workerPort:i.worker_port});let p=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/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:E}),signal:AbortSignal.timeout(2e3)});if(!p.ok){let l=await p.text();throw u.failure("HOOK","Failed to send observation",{sessionId:i.id,status:p.status},l),new Error(`Failed to send observation to worker: ${p.status} ${l}`)}u.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s}),console.log(T("PostToolUse",!0))}import{stdin as y}from"process";var R="";y.on("data",n=>R+=n);y.on("end",async()=>{let n=R.trim()?JSON.parse(R):void 0;await D(n),process.exit(0)});

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import U from"better-sqlite3";import{join as i,dirname as k,basename as B}from"path";import{homedir as I}from"os";import{existsSync as W,mkdirSync as D}from"fs";import{fileURLToPath as y}from"url";function x(){return typeof __dirname<"u"?__dirname:k(y(import.meta.url))}var Y=x(),d=process.env.CLAUDE_MEM_DATA_DIR||i(I(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(I(),".claude"),q=i(d,"archives"),V=i(d,"logs"),J=i(d,"trash"),Q=i(d,"backups"),z=i(d,"settings.json"),f=i(d,"claude-mem.db"),Z=i(l,"settings.json"),ee=i(l,"commands"),se=i(l,"CLAUDE.md");function h(o){D(o,{recursive:!0})}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(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),E=T[e].padEnd(5),p=s.padEnd(6),m="";r?.correlationId?m=`[${r.correlationId}] `:r?.sessionId&&(m=`[session-${r.sessionId}] `);let _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let N="";if(r){let{sessionId:w,sdkSessionId:M,correlationId:F,...R}=r;Object.keys(R).length>0&&(N=` {${Object.entries(R).map(([A,C])=>`${A}=${C}`).join(", ")}}`)}let O=`[${a}] [${E}] [${p}] ${m}${t}${N}${_}`;e===3?console.error(O):console.log(O)}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`})}},c=new S;var u=class{db;constructor(){h(d),this.db=new U(f),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()}initializeSchema(){try{this.db.exec(`
import H from"better-sqlite3";import{join as a,dirname as w,basename as J}from"path";import{homedir as I}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(),p=process.env.CLAUDE_MEM_DATA_DIR||a(I(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||a(I(),".claude"),te=a(p,"archives"),re=a(p,"logs"),oe=a(p,"trash"),ne=a(p,"backups"),ie=a(p,"settings.json"),L=a(p,"claude-mem.db"),ae=a(l,"settings.json"),de=a(l,"commands"),pe=a(l,"CLAUDE.md");function v(n){X(n,{recursive:!0})}function A(){return a(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||{}),g=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(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=T[e].padEnd(5),c=s.padEnd(6),E="";r?.correlationId?E=`[${r.correlationId}] `:r?.sessionId&&(E=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let R="";if(r){let{sessionId:G,sdkSessionId:K,correlationId:Y,...O}=r;Object.keys(O).length>0&&(R=` {${Object.entries(O).map(([x,U])=>`${x}=${U}`).join(", ")}}`)}let h=`[${i}] [${d}] [${c}] ${E}${t}${R}${_}`;e===3?console.error(h):console.log(h)}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 g;var m=class{db;constructor(){v(p),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()}initializeSchema(){try{this.db.exec(`
CREATE TABLE IF NOT EXISTS schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
@@ -63,7 +63,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
`),this.db.prepare("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)").run(4,new Date().toISOString()),console.error("[SessionStore] Migration004 applied successfully"))}catch(e){throw console.error("[SessionStore] Schema initialization error:",e.message),e}}ensureWorkerPortColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(5))return;this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(5,new Date().toISOString())}catch(e){console.error("[SessionStore] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(6))return;this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(6,new Date().toISOString())}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;if(!this.db.pragma("index_list(session_summaries)").some(r=>r.unique===1)){this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString());return}console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
@@ -200,15 +200,15 @@ ${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,n=r.getTime();return this.db.prepare(`
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,o=r.getTime();return this.db.prepare(`
INSERT INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(e,s,t,r.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).run(s,e).changes===0?(c.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
`).run(s,e).changes===0?(u.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
UPDATE sdk_sessions
SET worker_port = ?
WHERE id = ?
@@ -217,17 +217,17 @@ ${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}storeObservation(e,s,t,r){let n=new Date,a=n.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let o=new Date,i=o.getTime();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,n.toISOString(),a)}storeSummary(e,s,t,r){let n=new Date,a=n.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,o.toISOString(),i)}storeSummary(e,s,t,r){let o=new Date,i=o.getTime();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,n.toISOString(),a)}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,o.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 = ?
@@ -239,4 +239,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 X(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 g(o,e,s={}){let t=X(o,e,s);return JSON.stringify(t)}async function L(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,s=new u,t=s.findActiveSDKSession(e);if(!t){s.close(),console.log(g("Stop",!0));return}if(!t.worker_port)throw s.close(),c.error("HOOK","No worker port for session",{sessionId:t.id}),new Error("No worker port for session - session may not be properly initialized");let r=s.getPromptCounter(t.id);s.close(),c.dataIn("HOOK","Stop: Requesting summary",{sessionId:t.id,workerPort:t.worker_port,promptNumber:r});let n=await fetch(`http://127.0.0.1:${t.worker_port}/sessions/${t.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!n.ok){let a=await n.text();throw c.failure("HOOK","Failed to generate summary",{sessionId:t.id,status:n.status},a),new Error(`Failed to request summary from worker: ${n.status} ${a}`)}c.debug("HOOK","Summary request sent successfully",{sessionId:t.id}),console.log(g("Stop",!0))}import{stdin as v}from"process";var b="";v.on("data",o=>b+=o);v.on("end",async()=>{let o=b.trim()?JSON.parse(b):void 0;await L(o),process.exit(0)});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function W(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 S(n,e,s={}){let t=W(n,e,s);return JSON.stringify(t)}import b from"path";import{existsSync as N}from"fs";import{spawn as $}from"child_process";var j=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),B=`http://127.0.0.1:${j}/health`;async function k(){try{return(await fetch(B,{signal:AbortSignal.timeout(500)})).ok}catch{return!1}}async function C(){try{if(await k())return!0;console.error("[claude-mem] Worker not responding, starting...");let n=A(),e=b.join(n,"plugin","scripts","worker-service.cjs");if(!N(e))return console.error(`[claude-mem] Worker service not found at ${e}`),!1;let s=b.join(n,"ecosystem.config.cjs"),t=b.join(n,"node_modules",".bin","pm2");if(!N(t))throw new Error(`PM2 binary not found at ${t}. This is a bundled dependency - try running: npm install`);if(!N(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 k())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,s=new m,t=s.findActiveSDKSession(e);if(!t){s.close(),console.log(S("Stop",!0));return}if(!t.worker_port)throw s.close(),u.error("HOOK","No worker port for session",{sessionId:t.id}),new Error("No worker port for session - session may not be properly initialized");let r=s.getPromptCounter(t.id);if(s.close(),!await C())throw new Error("Worker service failed to start or become healthy");u.dataIn("HOOK","Stop: Requesting summary",{sessionId:t.id,workerPort:t.worker_port,promptNumber:r});let i=await fetch(`http://127.0.0.1:${t.worker_port}/sessions/${t.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let d=await i.text();throw u.failure("HOOK","Failed to generate summary",{sessionId:t.id,status:i.status},d),new Error(`Failed to request summary from worker: ${i.status} ${d}`)}u.debug("HOOK","Summary request sent successfully",{sessionId:t.id}),console.log(S("Stop",!0))}import{stdin as y}from"process";var f="";y.on("data",n=>f+=n);y.on("end",async()=>{let n=f.trim()?JSON.parse(f):void 0;await D(n),process.exit(0)});

View File

@@ -39,18 +39,14 @@ async function buildHooks() {
const version = packageJson.version;
console.log(`📌 Version: ${version}`);
// Create output directories
console.log('\n📦 Preparing output directories...');
// Create output directory
console.log('\n📦 Preparing output directory...');
const hooksDir = 'plugin/scripts';
const distDir = 'dist';
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, { recursive: true });
}
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
console.log('✓ Output directories ready');
console.log('✓ Output directory ready');
// Build worker service
console.log(`\n🔧 Building worker service...`);
@@ -60,7 +56,7 @@ async function buildHooks() {
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${distDir}/${WORKER_SERVICE.name}.cjs`,
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
minify: true,
external: ['better-sqlite3'],
define: {
@@ -72,8 +68,8 @@ async function buildHooks() {
});
// Make worker service executable
fs.chmodSync(`${distDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
const workerStats = fs.statSync(`${distDir}/${WORKER_SERVICE.name}.cjs`);
fs.chmodSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`, 0o755);
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
// Build each hook
@@ -133,9 +129,10 @@ async function buildHooks() {
console.log(`✓ search-server built (${(searchStats.size / 1024).toFixed(2)} KB)`);
console.log('\n✅ All hooks, worker service, and search server built successfully!');
console.log(` Hooks: ${hooksDir}/`);
console.log(` Worker: ${distDir}/worker-service.cjs`);
console.log(` Search: ${hooksDir}/search-server.js`);
console.log(` Output: ${hooksDir}/`);
console.log(` - Hooks: *-hook.js`);
console.log(` - Worker: worker-service.cjs`);
console.log(` - Search: search-server.js`);
} catch (error) {
console.error('\n❌ Build failed:', error.message);

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env node
/**
* Publish script for claude-mem
* Handles version bumping, building, and publishing to npm
* Release script for claude-mem
* Handles version bumping, building, and creating marketplace releases
*/
import { exec } from 'child_process';
@@ -21,7 +21,7 @@ const question = (query) => new Promise((resolve) => rl.question(query, resolve)
async function publish() {
try {
console.log('📦 Claude-mem Publishing Tool\n');
console.log('📦 Claude-mem Marketplace Release Tool\n');
// Check git status
console.log('🔍 Checking git status...');
@@ -82,15 +82,19 @@ async function publish() {
process.exit(0);
}
// Update package.json version
console.log('\n📝 Updating package.json...');
// Update package.json and marketplace.json versions
console.log('\n📝 Updating package.json and marketplace.json...');
packageJson.version = newVersion;
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
console.log('✓ Version updated');
const marketplaceJson = JSON.parse(fs.readFileSync('.claude-plugin/marketplace.json', 'utf-8'));
marketplaceJson.plugins[0].version = newVersion;
fs.writeFileSync('.claude-plugin/marketplace.json', JSON.stringify(marketplaceJson, null, 2) + '\n');
console.log('✓ Versions updated in both files');
// Run build
console.log('\n🔨 Building...');
await execAsync('node build.js');
console.log('\n🔨 Building hooks...');
await execAsync('npm run build');
console.log('✓ Build complete');
// Run tests if they exist
@@ -112,31 +116,26 @@ async function publish() {
// Git commit and tag
console.log('\n📌 Creating git commit and tag...');
await execAsync('git add package.json dist/');
await execAsync(`git commit -m "Release v${newVersion}
await execAsync('git add package.json .claude-plugin/marketplace.json plugin/');
await execAsync(`git commit -m "chore: Release v${newVersion}
Published from npm package build
Source: https://github.com/thedotmack/claude-mem"`);
Marketplace release for Claude Code plugin
https://github.com/thedotmack/claude-mem"`);
await execAsync(`git tag v${newVersion}`);
console.log(`✓ Created commit and tag v${newVersion}`);
// Publish to npm
console.log('\n🚀 Publishing to npm...');
await execAsync('npm publish');
console.log('✓ Published to npm');
// Push to git
console.log('\n⬆ Pushing to git...');
await execAsync('git push');
await execAsync('git push --tags');
console.log('✓ Pushed to git');
console.log(`\n✅ Successfully published v${newVersion}! 🎉`);
console.log(`\n📦 Package: https://www.npmjs.com/package/claude-mem`);
console.log(`🏷️ Tag: https://github.com/thedotmack/claude-mem/releases/tag/v${newVersion}`);
console.log(`\n✅ Successfully released v${newVersion}! 🎉`);
console.log(`\n🏷️ Tag: https://github.com/thedotmack/claude-mem/releases/tag/v${newVersion}`);
console.log(`📦 Marketplace will sync from this tag automatically`);
} catch (error) {
console.error('\n❌ Publish failed:', error.message);
console.error('\n❌ Release failed:', error.message);
if (error.stderr) {
console.error('\nError details:', error.stderr);
}

View File

@@ -1,4 +1,5 @@
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { ensureWorkerRunning } from '../shared/worker-utils.js';
export interface SessionEndInput {
session_id: string;
@@ -67,15 +68,22 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
// 1. Delete session via HTTP
if (session.worker_port) {
try {
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
method: 'DELETE',
signal: AbortSignal.timeout(5000)
});
if (response.ok) {
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
// Ensure worker is running before sending cleanup request
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
console.error('[claude-mem cleanup] Worker not available - skipping HTTP cleanup');
// Continue with local cleanup below
} else {
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}`, {
method: 'DELETE',
signal: AbortSignal.timeout(5000)
});
if (response.ok) {
console.error('[claude-mem cleanup] Session deleted successfully via HTTP');
} else {
console.error('[claude-mem cleanup] Failed to delete session:', await response.text());
}
}
} catch (error: any) {
console.error('[claude-mem cleanup] HTTP DELETE error:', error.message);

View File

@@ -1,6 +1,7 @@
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning } from '../shared/worker-utils.js';
export interface PostToolUseInput {
session_id: string;
@@ -51,6 +52,12 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
const promptNumber = db.getPromptCounter(session.id);
db.close();
// Ensure worker is running before sending observation
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
const toolStr = logger.formatTool(tool_name, tool_input);
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {

View File

@@ -1,6 +1,7 @@
import { SessionStore } from '../services/sqlite/SessionStore.js';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning } from '../shared/worker-utils.js';
export interface StopInput {
session_id: string;
@@ -37,6 +38,12 @@ export async function summaryHook(input?: StopInput): Promise<void> {
const promptNumber = db.getPromptCounter(session.id);
db.close();
// Ensure worker is running before requesting summary
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
throw new Error('Worker service failed to start or become healthy');
}
logger.dataIn('HOOK', 'Stop: Requesting summary', {
sessionId: session.id,
workerPort: session.worker_port,

View File

@@ -37,7 +37,7 @@ export async function ensureWorkerRunning(): Promise<boolean> {
// Find worker service path
const packageRoot = getPackageRoot();
const workerPath = path.join(packageRoot, 'dist', 'worker-service.cjs');
const workerPath = path.join(packageRoot, 'plugin', 'scripts', 'worker-service.cjs');
if (!existsSync(workerPath)) {
console.error(`[claude-mem] Worker service not found at ${workerPath}`);