feat(logging): Implement structured logging across the application

- Introduced a new Logger utility to standardize logging with correlation IDs and structured context.
- Replaced console.error and console.log statements with logger methods in various modules including save.ts, summary.ts, parser.ts, HooksDatabase.ts, and worker-service.ts.
- Enhanced error handling and logging for better traceability of observations and summaries.
- Made observations.text nullable in the database schema to support structured fields.
- Added correlation IDs for tracking observations through the processing pipeline.
This commit is contained in:
Alex Newman
2025-10-18 19:15:52 -04:00
parent 874815770a
commit 05f3889deb
12 changed files with 757 additions and 140 deletions
+62 -34
View File
File diff suppressed because one or more lines are too long
+42 -10
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import k from"better-sqlite3";import{join as i,dirname as R,basename as L}from"path";import{homedir as _}from"os";import{existsSync as O,mkdirSync as h}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(_(),".claude-mem"),p=process.env.CLAUDE_CONFIG_DIR||i(_(),".claude"),C=i(a,"archives"),x=i(a,"logs"),y=i(a,"trash"),w=i(a,"backups"),U=i(a,"settings.json"),E=i(a,"claude-mem.db"),H=i(p,"settings.json"),P=i(p,"commands"),M=i(p,"CLAUDE.md");function g(o){h(o,{recursive:!0})}var m=class{db;constructor(){g(a),this.db=new k(E),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let f=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
import N from"better-sqlite3";import{join as a,dirname as P,basename as $}from"path";import{homedir as k}from"os";import{existsSync as F,mkdirSync as C}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(k(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||a(k(),".claude"),W=a(c,"archives"),G=a(c,"logs"),K=a(c,"trash"),q=a(c,"backups"),J=a(c,"settings.json"),D=a(c,"claude-mem.db"),Y=a(l,"settings.json"),V=a(l,"commands"),Q=a(l,"CLAUDE.md");function S(n){C(n,{recursive:!0})}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))(_||{}),E=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=_[e]??1,this.useColor=process.stdout.isTTY??!1}static correlationId(e,t){return`obs-${e}-${t}`}static 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(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),p=_[e].padEnd(5),T=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let u="";o!=null&&(this.level===0&&typeof o=="object"?u=`
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let b="";if(r){let{sessionId:O,sdkSessionId:y,correlationId:x,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([v,I])=>`${v}=${I}`).join(", ")}}`)}let f=`[${d}] [${p}] [${T}] ${i}${s}${b}${u}`;e===3?console.error(f):console.log(f)}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`})}},A=new E;var m=class{db;constructor(){S(c),this.db=new N(D),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(i=>i.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(i=>i.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table"));let T=this.db.pragma("index_list(session_summaries)").some(i=>i.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] 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,
@@ -35,7 +37,37 @@ import k from"better-sqlite3";import{join as i,dirname as R,basename as L}from"p
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{let t=this.db.pragma("table_info(observations)").find(s=>s.name==="text");if(!t||t.notnull===0)return;console.error("[HooksDatabase] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[HooksDatabase] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
@@ -76,15 +108,15 @@ import k from"better-sqlite3";import{join as i,dirname as R,basename as L}from"p
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,n=r.getTime();return this.db.prepare(`
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){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,t,s,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
`).run(e,t,s,r.toISOString(),o).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?(console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`),!1):!0}setWorkerPort(e,t){this.db.prepare(`
`).run(t,e).changes===0?(A.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 = ?
@@ -93,17 +125,17 @@ import k from"better-sqlite3";import{join as i,dirname as R,basename as L}from"p
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 o=new Date,d=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,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,o.toISOString(),d)}storeSummary(e,t,s,r){let o=new Date,d=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,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,o.toISOString(),d)}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 = ?
@@ -115,5 +147,5 @@ import k from"better-sqlite3";import{join as i,dirname as R,basename as L}from"p
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 T(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 m,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 b}from"process";var l="";b.on("data",o=>l+=o);b.on("end",async()=>{try{let o=l.trim()?JSON.parse(l):void 0;await T(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()}};async function R(n){try{console.error("[claude-mem cleanup] Hook fired",{input:n?{session_id:n.session_id,cwd:n.cwd,reason:n.reason}:null}),n||(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}=n;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:t});let s=new m,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 o=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});o.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await o.text())}catch(o){console.error("[claude-mem cleanup] HTTP DELETE error:",o.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(o){console.error("[claude-mem cleanup] Failed to mark session as failed:",o)}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",n=>g+=n);L.on("end",async()=>{try{let n=g.trim()?JSON.parse(g):void 0;await R(n)}catch(n){console.error(`[claude-mem cleanup-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
+44 -12
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import S from"path";import k from"better-sqlite3";import{join as m,dirname as N,basename as y}from"path";import{homedir as T}from"os";import{existsSync as C,mkdirSync as D}from"fs";var d=process.env.CLAUDE_MEM_DATA_DIR||m(T(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||m(T(),".claude"),w=m(d,"archives"),U=m(d,"logs"),H=m(d,"trash"),P=m(d,"backups"),M=m(d,"settings.json"),h=m(d,"claude-mem.db"),j=m(E,"settings.json"),F=m(E,"commands"),W=m(E,"CLAUDE.md");function f(p){D(p,{recursive:!0})}var l=class{db;constructor(){f(d),this.db=new k(h),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let o=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
import C from"path";import I from"better-sqlite3";import{join as d,dirname as H,basename as P}from"path";import{homedir as A}from"os";import{existsSync as F,mkdirSync as v}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||d(A(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||d(A(),".claude"),W=d(m,"archives"),G=d(m,"logs"),q=d(m,"trash"),K=d(m,"backups"),J=d(m,"settings.json"),D=d(m,"claude-mem.db"),Y=d(g,"settings.json"),V=d(g,"commands"),Q=d(g,"CLAUDE.md");function S(p){v(p,{recursive:!0})}var b=(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))(b||{}),f=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=b[e]??1,this.useColor=process.stdout.isTTY??!1}static correlationId(e,t){return`obs-${e}-${t}`}static 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(e<this.level)return;let n=new Date().toISOString().replace("T"," ").substring(0,23),u=b[e].padEnd(5),i=t.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let a="";o!=null&&(this.level===0&&typeof o=="object"?a=`
`+JSON.stringify(o,null,2):a=" "+this.formatData(o));let l="";if(r){let{sessionId:N,sdkSessionId:O,correlationId:x,...R}=r;Object.keys(R).length>0&&(l=` {${Object.entries(R).map(([k,y])=>`${k}=${y}`).join(", ")}}`)}let _=`[${n}] [${u}] [${i}] ${c}${s}${l}${a}`;e===3?console.error(_):console.log(_)}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`})}},L=new f;var E=class{db;constructor(){S(m),this.db=new I(D),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let i=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] 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,
@@ -35,7 +37,37 @@ import S from"path";import k from"better-sqlite3";import{join as m,dirname as N,
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{let t=this.db.pragma("table_info(observations)").find(s=>s.name==="text");if(!t||t.notnull===0)return;console.error("[HooksDatabase] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[HooksDatabase] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
@@ -76,15 +108,15 @@ import S from"path";import k from"better-sqlite3";import{join as m,dirname as N,
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 i=new Date,a=i.getTime();return this.db.prepare(`
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){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,t,s,i.toISOString(),a).lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
`).run(e,t,s,r.toISOString(),o).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?(console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`),!1):!0}setWorkerPort(e,t){this.db.prepare(`
`).run(t,e).changes===0?(L.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 = ?
@@ -93,17 +125,17 @@ import S from"path";import k from"better-sqlite3";import{join as m,dirname as N,
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,t,s,i){let a=new Date,r=a.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,t,s,r){let o=new Date,n=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,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),i||null,a.toISOString(),r)}storeSummary(e,t,s,i){let a=new Date,r=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(),n)}storeSummary(e,t,s,r){let o=new Date,n=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,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,i||null,a.toISOString(),r)}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(),n)}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 = ?
@@ -115,8 +147,8 @@ import S from"path";import k from"better-sqlite3";import{join as m,dirname as N,
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function b(p){let e=p?.cwd??process.cwd(),t=e?S.basename(e):"unknown-project",s=new l;try{let i=s.getRecentSummaries(t,5),a=s.getRecentObservations(t,20);if(i.length===0&&a.length===0){console.log(`# Recent Session Context
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function T(p){let e=p?.cwd??process.cwd(),t=e?C.basename(e):"unknown-project",s=new E;try{let r=s.getRecentSummaries(t,5),o=s.getRecentObservations(t,20);if(r.length===0&&o.length===0){console.log(`# Recent Session Context
No previous sessions found for this project yet.`);return}let r=[];if(r.push("# Recent Session Context"),r.push(""),a.length>0){r.push(`## Recent Observations (${a.length})`),r.push("");let o={};for(let n of a)o[n.type]||(o[n.type]=[]),o[n.type].push({text:n.text,prompt_number:n.prompt_number,created_at:n.created_at});let c=["feature","bugfix","refactor","discovery","decision"];for(let n of c)if(o[n]&&o[n].length>0){r.push(`### ${n.charAt(0).toUpperCase()+n.slice(1)}s`);for(let _ of o[n]){let A=_.prompt_number?` (prompt #${_.prompt_number})`:"";r.push(`- ${_.text}${A}`)}r.push("")}}if(i.length===0){console.log(r.join(`
`));return}r.push("## Recent Sessions"),r.push("");let u=i.length===1?"session":"sessions";r.push(`Showing last ${i.length} ${u} for **${t}**:`),r.push("");for(let o of i){r.push("---"),r.push("");let c=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${c}**`),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 n=JSON.parse(o.files_read);Array.isArray(n)&&n.length>0&&r.push(`**Files Read:** ${n.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let n=JSON.parse(o.files_edited);Array.isArray(n)&&n.length>0&&r.push(`**Files Edited:** ${n.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}r.push(`**Date:** ${o.created_at.split("T")[0]}`),r.push("")}console.log(r.join(`
`))}finally{s.close()}}import{stdin as g}from"process";try{if(g.isTTY)b();else{let p="";g.on("data",e=>p+=e),g.on("end",()=>{let e=p.trim()?JSON.parse(p):void 0;b(e),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
No previous sessions found for this project yet.`);return}let n=[];if(n.push("# Recent Session Context"),n.push(""),o.length>0){n.push(`## Recent Observations (${o.length})`),n.push("");let i={};for(let a of o)i[a.type]||(i[a.type]=[]),i[a.type].push({text:a.text,prompt_number:a.prompt_number,created_at:a.created_at});let c=["feature","bugfix","refactor","discovery","decision"];for(let a of c)if(i[a]&&i[a].length>0){n.push(`### ${a.charAt(0).toUpperCase()+a.slice(1)}s`);for(let l of i[a]){let _=l.prompt_number?` (prompt #${l.prompt_number})`:"";n.push(`- ${l.text}${_}`)}n.push("")}}if(r.length===0){console.log(n.join(`
`));return}n.push("## Recent Sessions"),n.push("");let u=r.length===1?"session":"sessions";n.push(`Showing last ${r.length} ${u} for **${t}**:`),n.push("");for(let i of r){n.push("---"),n.push("");let c=i.prompt_number?` (Prompt #${i.prompt_number})`:"";if(n.push(`**Summary${c}**`),n.push(""),i.request&&n.push(`**Request:** ${i.request}`),i.completed&&n.push(`**Completed:** ${i.completed}`),i.learned&&n.push(`**Learned:** ${i.learned}`),i.next_steps&&n.push(`**Next Steps:** ${i.next_steps}`),i.files_read)try{let a=JSON.parse(i.files_read);Array.isArray(a)&&a.length>0&&n.push(`**Files Read:** ${a.join(", ")}`)}catch{i.files_read.trim()&&n.push(`**Files Read:** ${i.files_read}`)}if(i.files_edited)try{let a=JSON.parse(i.files_edited);Array.isArray(a)&&a.length>0&&n.push(`**Files Edited:** ${a.join(", ")}`)}catch{i.files_edited.trim()&&n.push(`**Files Edited:** ${i.files_edited}`)}n.push(`**Date:** ${i.created_at.split("T")[0]}`),n.push("")}console.log(n.join(`
`))}finally{s.close()}}import{stdin as h}from"process";try{if(h.isTTY)T();else{let p="";h.on("data",e=>p+=e),h.on("end",()=>{let e=p.trim()?JSON.parse(p):void 0;T(e),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
+49 -17
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,basename as w}from"path";import{homedir as T}from"os";import{existsSync as U,mkdirSync as D}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||c(T(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||c(T(),".claude"),H=c(m,"archives"),M=c(m,"logs"),j=c(m,"trash"),F=c(m,"backups"),W=c(m,"settings.json"),f=c(m,"claude-mem.db"),B=c(b,"settings.json"),X=c(b,"commands"),q=c(b,"CLAUDE.md");function k(r){D(r,{recursive:!0})}var l=class{db;constructor(){k(m),this.db=new A(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let d=this.db.pragma("index_list(session_summaries)").some(p=>p.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
import x from"path";import I from"better-sqlite3";import{join as u,dirname as B,basename as F}from"path";import{homedir as k}from"os";import{existsSync as K,mkdirSync as N}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||u(k(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||u(k(),".claude"),J=u(m,"archives"),Y=u(m,"logs"),V=u(m,"trash"),Q=u(m,"backups"),z=u(m,"settings.json"),R=u(m,"claude-mem.db"),Z=u(b,"settings.json"),ee=u(b,"commands"),te=u(b,"CLAUDE.md");function D(n){N(n,{recursive:!0})}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||{}),T=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}static correlationId(e,s){return`obs-${e}-${s}`}static 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 a=new Date().toISOString().replace("T"," ").substring(0,23),i=g[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[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:U,sdkSessionId:P,correlationId:H,...S}=r;Object.keys(S).length>0&&(l=` {${Object.entries(S).map(([C,v])=>`${C}=${v}`).join(", ")}}`)}let h=`[${a}] [${i}] [${d}] ${c}${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`})}},A=new T;var _=class{db;constructor(){D(m),this.db=new I(R),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let d=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[HooksDatabase] 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,
@@ -27,7 +29,7 @@ import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[HooksDatabase] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(s=>s.name==="title"))return;console.error("[HooksDatabase] Adding hierarchical fields to observations table..."),this.db.exec(`
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[HooksDatabase] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(t=>t.name==="title"))return;console.error("[HooksDatabase] Adding hierarchical fields to observations table..."),this.db.exec(`
ALTER TABLE observations ADD COLUMN title TEXT;
ALTER TABLE observations ADD COLUMN subtitle TEXT;
ALTER TABLE observations ADD COLUMN facts TEXT;
@@ -35,7 +37,37 @@ import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{let s=this.db.pragma("table_info(observations)").find(t=>t.name==="text");if(!s||s.notnull===0)return;console.error("[HooksDatabase] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully made observations.text nullable")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[HooksDatabase] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
@@ -43,13 +75,13 @@ import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
SELECT type, text, prompt_number, created_at
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,t)}getSessionById(e){return this.db.prepare(`
`).all(e,s)}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -64,11 +96,11 @@ import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,
FROM sdk_sessions
WHERE claude_session_id = ?
LIMIT 1
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
UPDATE sdk_sessions
SET status = 'active', user_prompt = ?, worker_port = NULL
WHERE id = ?
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
UPDATE sdk_sessions
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
WHERE id = ?
@@ -76,43 +108,43 @@ import O from"path";import A from"better-sqlite3";import{join as c,dirname as v,
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 n=new Date,o=n.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,t,s,n.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,t){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(t,e).changes===0?(console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`),!1):!0}setWorkerPort(e,t){this.db.prepare(`
`).run(s,e).changes===0?(A.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 = ?
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
SELECT worker_port
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,t,s,n){let o=new Date,a=o.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let o=new Date,a=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,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),n||null,o.toISOString(),a)}storeSummary(e,t,s,n){let o=new Date,a=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,o.toISOString(),a)}storeSummary(e,s,t,r){let o=new Date,a=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,t,s.request,s.investigated,s.learned,s.completed,s.next_steps,s.notes,n||null,o.toISOString(),a)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
`).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,o.toISOString(),a)}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 = ?
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function R(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function _(r,e,t={}){let s=R(r,e,t);return JSON.stringify(s)}async function I(){let{readFileSync:r,existsSync:e}=await import("fs"),{join:t}=await import("path"),{homedir:s}=await import("os"),n=t(s(),".claude-mem","worker.port");if(!e(n))return null;try{let o=r(n,"utf8").trim();return parseInt(o,10)}catch{return null}}async function h(r){if(!r)throw new Error("newHook requires input");let{session_id:e,cwd:t,prompt:s}=r,n=O.basename(t),o=new l;try{let a=o.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let u=o.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${u}`)}else{let u=o.findAnySDKSession(e);if(u){i=u.id,o.reactivateSession(i,s);let E=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${E}`)}else{i=o.createSDKSession(e,n,s);let E=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Created new session ${i}, prompt #${E}`)}}let p=await I();if(!p){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(_("UserPromptSubmit",!0));return}if(d){let u=await fetch(`http://127.0.0.1:${p}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:n,userPrompt:s}),signal:AbortSignal.timeout(5e3)});u.ok||console.error("[new-hook] Failed to init session:",await u.text())}console.log(_("UserPromptSubmit",!0))}catch(a){console.error("[new-hook] FATAL ERROR:",a.message),console.error("[new-hook] Stack:",a.stack),console.error("[new-hook] Full error:",JSON.stringify(a,Object.getOwnPropertyNames(a))),console.log(_("UserPromptSubmit",!0))}finally{o.close()}}import{stdin as S}from"process";var g="";S.on("data",r=>g+=r);S.on("end",async()=>{try{let r=g.trim()?JSON.parse(g):void 0;await h(r),process.exit(0)}catch(r){console.error(`[claude-mem new-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function y(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 E(n,e,s={}){let t=y(n,e,s);return JSON.stringify(t)}async function w(){let{readFileSync:n,existsSync:e}=await import("fs"),{join:s}=await import("path"),{homedir:t}=await import("os"),r=s(t(),".claude-mem","worker.port");if(!e(r))return null;try{let o=n(r,"utf8").trim();return parseInt(o,10)}catch{return null}}async function O(n){if(!n)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=n,r=x.basename(s),o=new _;try{let a=o.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let p=o.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${p}`)}else{let p=o.findAnySDKSession(e);if(p){i=p.id,o.reactivateSession(i,t);let l=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${l}`)}else{i=o.createSDKSession(e,r,t);let l=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Created new session ${i}, prompt #${l}`)}}let c=await w();if(!c){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(E("UserPromptSubmit",!0));return}if(d){let p=await fetch(`http://127.0.0.1:${c}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});p.ok||console.error("[new-hook] Failed to init session:",await p.text())}console.log(E("UserPromptSubmit",!0))}catch(a){console.error("[new-hook] FATAL ERROR:",a.message),console.error("[new-hook] Stack:",a.stack),console.error("[new-hook] Full error:",JSON.stringify(a,Object.getOwnPropertyNames(a))),console.log(E("UserPromptSubmit",!0))}finally{o.close()}}import{stdin as L}from"process";var f="";L.on("data",n=>f+=n);L.on("end",async()=>{try{let n=f.trim()?JSON.parse(f):void 0;await O(n),process.exit(0)}catch(n){console.error(`[claude-mem new-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
+41 -9
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import S from"better-sqlite3";import{join as a,dirname as I,basename as N}from"path";import{homedir as g}from"os";import{existsSync as y,mkdirSync as h}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(g(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||a(g(),".claude"),U=a(c,"archives"),P=a(c,"logs"),w=a(c,"trash"),H=a(c,"backups"),M=a(c,"settings.json"),b=a(c,"claude-mem.db"),B=a(_,"settings.json"),W=a(_,"commands"),j=a(_,"CLAUDE.md");function T(r){h(r,{recursive:!0})}var l=class{db;constructor(){T(c),this.db=new S(b),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(u=>u.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table"));let m=this.db.pragma("index_list(session_summaries)").some(u=>u.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
import C from"better-sqlite3";import{join as c,dirname as B,basename as j}from"path";import{homedir as S}from"os";import{existsSync as K,mkdirSync as N}from"fs";var u=process.env.CLAUDE_MEM_DATA_DIR||c(S(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(S(),".claude"),q=c(u,"archives"),J=c(u,"logs"),Y=c(u,"trash"),V=c(u,"backups"),Q=c(u,"settings.json"),R=c(u,"claude-mem.db"),z=c(g,"settings.json"),Z=c(g,"commands"),ee=c(g,"CLAUDE.md");function D(n){N(n,{recursive:!0})}var b=(r=>(r[r.DEBUG=0]="DEBUG",r[r.INFO=1]="INFO",r[r.WARN=2]="WARN",r[r.ERROR=3]="ERROR",r[r.SILENT=4]="SILENT",r))(b||{}),T=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=b[e]??1,this.useColor=process.stdout.isTTY??!1}static correlationId(e,s){return`obs-${e}-${s}`}static 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 o=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${o})`}if(e==="Read"&&t.file_path){let o=t.file_path.split("/").pop()||t.file_path;return`${e}(${o})`}if(e==="Edit"&&t.file_path){let o=t.file_path.split("/").pop()||t.file_path;return`${e}(${o})`}if(e==="Write"&&t.file_path){let o=t.file_path.split("/").pop()||t.file_path;return`${e}(${o})`}return e}catch{return e}}log(e,s,t,o,r){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=b[e].padEnd(5),_=s.padEnd(6),a="";o?.correlationId?a=`[${o.correlationId}] `:o?.sessionId&&(a=`[session-${o.sessionId}] `);let m="";r!=null&&(this.level===0&&typeof r=="object"?m=`
`+JSON.stringify(r,null,2):m=" "+this.formatData(r));let h="";if(o){let{sessionId:U,sdkSessionId:w,correlationId:H,...O}=o;Object.keys(O).length>0&&(h=` {${Object.entries(O).map(([I,L])=>`${I}=${L}`).join(", ")}}`)}let k=`[${i}] [${d}] [${_}] ${a}${t}${h}${m}`;e===3?console.error(k):console.log(k)}debug(e,s,t,o){this.log(0,e,s,t,o)}info(e,s,t,o){this.log(1,e,s,t,o)}warn(e,s,t,o){this.log(2,e,s,t,o)}error(e,s,t,o){this.log(3,e,s,t,o)}dataIn(e,s,t,o){this.info(e,`\u2192 ${s}`,t,o)}dataOut(e,s,t,o){this.info(e,`\u2190 ${s}`,t,o)}success(e,s,t,o){this.info(e,`\u2713 ${s}`,t,o)}failure(e,s,t,o){this.error(e,`\u2717 ${s}`,t,o)}timing(e,s,t,o){this.info(e,`\u23F1 ${s}`,o,{duration:`${t}ms`})}},p=new T;var E=class{db;constructor(){D(u),this.db=new C(R),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(a=>a.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(a=>a.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table"));let _=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[HooksDatabase] 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,
@@ -35,7 +37,37 @@ import S from"better-sqlite3";import{join as a,dirname as I,basename as N}from"p
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{let s=this.db.pragma("table_info(observations)").find(t=>t.name==="text");if(!s||s.notnull===0)return;console.error("[HooksDatabase] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully made observations.text nullable")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[HooksDatabase] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
@@ -76,15 +108,15 @@ import S from"better-sqlite3";import{join as a,dirname as I,basename as N}from"p
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 n=new Date,o=n.getTime();return this.db.prepare(`
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let o=new Date,r=o.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,n.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(e,s,t,o.toISOString(),r).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?(console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`),!1):!0}setWorkerPort(e,s){this.db.prepare(`
`).run(s,e).changes===0?(p.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 = ?
@@ -93,17 +125,17 @@ import S from"better-sqlite3";import{join as a,dirname as I,basename as N}from"p
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,s,t,n){let o=new Date,i=o.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,s,t,o){let r=new Date,i=r.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),n||null,o.toISOString(),i)}storeSummary(e,s,t,n){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),o||null,r.toISOString(),i)}storeSummary(e,s,t,o){let r=new Date,i=r.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,n||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,o||null,r.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 = ?
@@ -115,4 +147,4 @@ import S from"better-sqlite3";import{join as a,dirname as I,basename as N}from"p
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function D(r,e,s){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function d(r,e,s={}){let t=D(r,e,s);return JSON.stringify(t)}var A=new Set(["ListMcpResourcesTool"]);async function f(r){if(!r)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:n}=r;if(A.has(s)){console.log(d("PostToolUse",!0));return}let o=new l,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(d("PostToolUse",!0));return}if(!i.worker_port){o.close(),console.error("[save-hook] No worker port for session",i.id),console.log(d("PostToolUse",!0));return}let p=o.getPromptCounter(i.id);o.close();try{let m=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:JSON.stringify(t),tool_output:JSON.stringify(n),prompt_number:p}),signal:AbortSignal.timeout(2e3)});m.ok||console.error("[save-hook] Failed to send observation:",await m.text())}catch(m){console.error("[save-hook] Error:",m.message)}finally{console.log(d("PostToolUse",!0))}}import{stdin as k}from"process";var E="";k.on("data",r=>E+=r);k.on("end",async()=>{try{let r=E.trim()?JSON.parse(E):void 0;await f(r),process.exit(0)}catch(r){console.error(`[claude-mem save-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function y(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=y(n,e,s);return JSON.stringify(t)}var x=new Set(["ListMcpResourcesTool"]);async function A(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:o}=n;if(x.has(s)){console.log(l("PostToolUse",!0));return}let r=new E,i=r.findActiveSDKSession(e);if(!i){r.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port){r.close(),p.error("HOOK","No worker port for session",{sessionId:i.id}),console.log(l("PostToolUse",!0));return}let d=r.getPromptCounter(i.id);r.close();let _=p.formatTool(s,t);try{p.dataIn("HOOK",`PostToolUse: ${_}`,{sessionId:i.id,workerPort:i.worker_port});let a=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:o!==void 0?JSON.stringify(o):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(a.ok)p.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s});else{let m=await a.text();p.failure("HOOK","Failed to send observation",{sessionId:i.id,status:a.status},m)}}catch(a){p.failure("HOOK","Error sending observation",{sessionId:i.id},a)}finally{console.log(l("PostToolUse",!0))}}import{stdin as v}from"process";var f="";v.on("data",n=>f+=n);v.on("end",async()=>{try{let n=f.trim()?JSON.parse(f):void 0;await A(n),process.exit(0)}catch(n){console.error(`[claude-mem save-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
+49 -17
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node
import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"path";import{homedir as E}from"os";import{existsSync as v,mkdirSync as k}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(E(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(E(),".claude"),x=i(a,"archives"),H=i(a,"logs"),w=i(a,"trash"),U=i(a,"backups"),P=i(a,"settings.json"),g=i(a,"claude-mem.db"),M=i(l,"settings.json"),B=i(l,"commands"),W=i(l,"CLAUDE.md");function b(r){k(r,{recursive:!0})}var m=class{db;constructor(){b(a),this.db=new h(g),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{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("[HooksDatabase] 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("[HooksDatabase] 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("[HooksDatabase] Added prompt_number column to session_summaries table"));let D=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
import v from"better-sqlite3";import{join as a,dirname as X,basename as B}from"path";import{homedir as R}from"os";import{existsSync as G,mkdirSync as C}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||a(R(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||a(R(),".claude"),q=a(p,"archives"),K=a(p,"logs"),J=a(p,"trash"),Y=a(p,"backups"),V=a(p,"settings.json"),O=a(p,"claude-mem.db"),Q=a(E,"settings.json"),z=a(E,"commands"),Z=a(E,"CLAUDE.md");function D(n){C(n,{recursive:!0})}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}static correlationId(e,t){return`obs-${e}-${t}`}static 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(e<this.level)return;let c=new Date().toISOString().replace("T"," ").substring(0,23),d=g[e].padEnd(5),f=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let _="";o!=null&&(this.level===0&&typeof o=="object"?_=`
`+JSON.stringify(o,null,2):_=" "+this.formatData(o));let h="";if(r){let{sessionId:x,sdkSessionId:U,correlationId:H,...k}=r;Object.keys(k).length>0&&(h=` {${Object.entries(k).map(([L,y])=>`${L}=${y}`).join(", ")}}`)}let S=`[${c}] [${d}] [${f}] ${i}${s}${h}${_}`;e===3?console.error(S):console.log(S)}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`})}},u=new b;var m=class{db;constructor(){D(p),this.db=new v(O),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(i=>i.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(i=>i.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table"));let f=this.db.pragma("index_list(session_summaries)").some(i=>i.unique===1)}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[HooksDatabase] 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,
@@ -27,7 +29,7 @@ import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"p
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[HooksDatabase] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(t=>t.name==="title"))return;console.error("[HooksDatabase] Adding hierarchical fields to observations table..."),this.db.exec(`
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[HooksDatabase] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(s=>s.name==="title"))return;console.error("[HooksDatabase] Adding hierarchical fields to observations table..."),this.db.exec(`
ALTER TABLE observations ADD COLUMN title TEXT;
ALTER TABLE observations ADD COLUMN subtitle TEXT;
ALTER TABLE observations ADD COLUMN facts TEXT;
@@ -35,7 +37,37 @@ import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"p
ALTER TABLE observations ADD COLUMN concepts TEXT;
ALTER TABLE observations ADD COLUMN files_read TEXT;
ALTER TABLE observations ADD COLUMN files_modified TEXT;
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),console.error("[HooksDatabase] Successfully added hierarchical fields to observations table")}catch(e){console.error("[HooksDatabase] Migration error (add hierarchical fields):",e.message)}}makeObservationsTextNullable(){try{let t=this.db.pragma("table_info(observations)").find(s=>s.name==="text");if(!t||t.notnull===0)return;console.error("[HooksDatabase] Making observations.text nullable..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`),this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`),this.db.exec("DROP TABLE observations"),this.db.exec("ALTER TABLE observations_new RENAME TO observations"),this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`),this.db.exec("COMMIT"),console.error("[HooksDatabase] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[HooksDatabase] Migration error (make text nullable):",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
@@ -43,13 +75,13 @@ import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"p
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
SELECT type, text, prompt_number, created_at
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`).all(e,s)}getSessionById(e){return this.db.prepare(`
`).all(e,t)}getSessionById(e){return this.db.prepare(`
SELECT id, sdk_session_id, project, user_prompt
FROM sdk_sessions
WHERE id = ?
@@ -64,11 +96,11 @@ import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"p
FROM sdk_sessions
WHERE claude_session_id = ?
LIMIT 1
`).get(e)||null}reactivateSession(e,s){this.db.prepare(`
`).get(e)||null}reactivateSession(e,t){this.db.prepare(`
UPDATE sdk_sessions
SET status = 'active', user_prompt = ?, worker_port = NULL
WHERE id = ?
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
UPDATE sdk_sessions
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
WHERE id = ?
@@ -76,43 +108,43 @@ import h from"better-sqlite3";import{join as i,dirname as O,basename as I}from"p
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 o=new Date,n=o.getTime();return this.db.prepare(`
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){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,o.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){return this.db.prepare(`
`).run(e,t,s,r.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,t){return this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ? AND sdk_session_id IS NULL
`).run(s,e).changes===0?(console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`),!1):!0}setWorkerPort(e,s){this.db.prepare(`
`).run(t,e).changes===0?(u.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 = ?
`).run(s,e)}getWorkerPort(e){return this.db.prepare(`
`).run(t,e)}getWorkerPort(e){return this.db.prepare(`
SELECT worker_port
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`).get(e)?.worker_port||null}storeObservation(e,s,t,o){let n=new Date,u=n.getTime();this.db.prepare(`
`).get(e)?.worker_port||null}storeObservation(e,t,s,r){let o=new Date,c=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),o||null,n.toISOString(),u)}storeSummary(e,s,t,o){let n=new Date,u=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,o.toISOString(),c)}storeSummary(e,t,s,r){let o=new Date,c=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,o||null,n.toISOString(),u)}markSessionCompleted(e){let s=new Date,t=s.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 = ?
`).run(s.toISOString(),t,e)}markSessionFailed(e){let s=new Date,t=s.getTime();this.db.prepare(`
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`).run(s.toISOString(),t,e)}cleanupOrphanedSessions(){let e=new Date,s=e.getTime();return this.db.prepare(`
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function S(r,e,s){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function p(r,e,s={}){let t=S(r,e,s);return JSON.stringify(t)}async function T(r){if(!r)throw new Error("summaryHook requires input");let{session_id:e}=r,s=new m,t=s.findActiveSDKSession(e);if(!t){s.close(),console.log(p("Stop",!0));return}if(!t.worker_port){s.close(),console.error("[summary-hook] No worker port for session",t.id),console.log(p("Stop",!0));return}let o=s.getPromptCounter(t.id);s.close();try{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:o}),signal:AbortSignal.timeout(2e3)});n.ok||console.error("[summary-hook] Failed to generate summary:",await n.text())}catch(n){console.error("[summary-hook] Error:",n.message)}finally{console.log(p("Stop",!0))}}import{stdin as f}from"process";var _="";f.on("data",r=>_+=r);f.on("end",async()=>{try{let r=_.trim()?JSON.parse(_):void 0;await T(r),process.exit(0)}catch(r){console.error(`[claude-mem summary-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function N(n,e,t){return n==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function l(n,e,t={}){let s=N(n,e,t);return JSON.stringify(s)}async function A(n){if(!n)throw new Error("summaryHook requires input");let{session_id:e}=n,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(l("Stop",!0));return}if(!s.worker_port){t.close(),u.error("HOOK","No worker port for session",{sessionId:s.id}),console.log(l("Stop",!0));return}let r=t.getPromptCounter(s.id);t.close();try{u.dataIn("HOOK","Stop: Requesting summary",{sessionId:s.id,workerPort:s.worker_port,promptNumber:r});let o=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(o.ok)u.debug("HOOK","Summary request sent successfully",{sessionId:s.id});else{let c=await o.text();u.failure("HOOK","Failed to generate summary",{sessionId:s.id,status:o.status},c)}}catch(o){u.failure("HOOK","Error requesting summary",{sessionId:s.id},o)}finally{console.log(l("Stop",!0))}}import{stdin as I}from"process";var T="";I.on("data",n=>T+=n);I.on("end",async()=>{try{let n=T.trim()?JSON.parse(T):void 0;await A(n),process.exit(0)}catch(n){console.error(`[claude-mem summary-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
+19 -5
View File
@@ -1,5 +1,6 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
export interface PostToolUseInput {
session_id: string;
@@ -42,7 +43,7 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
if (!session.worker_port) {
db.close();
console.error('[save-hook] No worker port for session', session.id);
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
console.log(createHookResponse('PostToolUse', true));
return;
}
@@ -51,24 +52,37 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
const promptNumber = db.getPromptCounter(session.id);
db.close();
const toolStr = logger.formatTool(tool_name, tool_input);
try {
logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {
sessionId: session.id,
workerPort: session.worker_port
});
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool_name,
tool_input: JSON.stringify(tool_input),
tool_output: JSON.stringify(tool_output),
tool_input: tool_input !== undefined ? JSON.stringify(tool_input) : '{}',
tool_output: tool_output !== undefined ? JSON.stringify(tool_output) : '{}',
prompt_number: promptNumber
}),
signal: AbortSignal.timeout(2000)
});
if (!response.ok) {
console.error('[save-hook] Failed to send observation:', await response.text());
const errorText = await response.text();
logger.failure('HOOK', 'Failed to send observation', {
sessionId: session.id,
status: response.status
}, errorText);
} else {
logger.debug('HOOK', 'Observation sent successfully', { sessionId: session.id, toolName: tool_name });
}
} catch (error: any) {
console.error('[save-hook] Error:', error.message);
logger.failure('HOOK', 'Error sending observation', { sessionId: session.id }, error);
} finally {
console.log(createHookResponse('PostToolUse', true));
}
+16 -3
View File
@@ -1,5 +1,6 @@
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
export interface StopInput {
session_id: string;
@@ -28,7 +29,7 @@ export async function summaryHook(input?: StopInput): Promise<void> {
if (!session.worker_port) {
db.close();
console.error('[summary-hook] No worker port for session', session.id);
logger.error('HOOK', 'No worker port for session', { sessionId: session.id });
console.log(createHookResponse('Stop', true));
return;
}
@@ -38,6 +39,12 @@ export async function summaryHook(input?: StopInput): Promise<void> {
db.close();
try {
logger.dataIn('HOOK', 'Stop: Requesting summary', {
sessionId: session.id,
workerPort: session.worker_port,
promptNumber
});
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -46,10 +53,16 @@ export async function summaryHook(input?: StopInput): Promise<void> {
});
if (!response.ok) {
console.error('[summary-hook] Failed to generate summary:', await response.text());
const errorText = await response.text();
logger.failure('HOOK', 'Failed to generate summary', {
sessionId: session.id,
status: response.status
}, errorText);
} else {
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: session.id });
}
} catch (error: any) {
console.error('[summary-hook] Error:', error.message);
logger.failure('HOOK', 'Error requesting summary', { sessionId: session.id }, error);
} finally {
console.log(createHookResponse('Stop', true));
}
+20 -5
View File
@@ -3,6 +3,8 @@
* Parses observation and summary XML blocks from SDK responses
*/
import { logger } from '../utils/logger.js';
export interface ParsedObservation {
type: string;
title: string;
@@ -27,7 +29,7 @@ export interface ParsedSummary {
* Parse observation XML blocks from SDK response
* Returns all observations found in the response
*/
export function parseObservations(text: string): ParsedObservation[] {
export function parseObservations(text: string, correlationId?: string): ParsedObservation[] {
const observations: ParsedObservation[] = [];
// Match <observation>...</observation> blocks (non-greedy)
@@ -49,14 +51,20 @@ export function parseObservations(text: string): ParsedObservation[] {
// Validate required fields
if (!type || !title || !subtitle || !narrative) {
console.warn('[SDK Parser] Observation missing required fields, skipping');
logger.warn('PARSER', 'Observation missing required fields, skipping', {
correlationId,
hasType: !!type,
hasTitle: !!title,
hasSubtitle: !!subtitle,
hasNarrative: !!narrative
});
continue;
}
// Validate type
const validTypes = ['change', 'discovery', 'decision'];
if (!validTypes.includes(type.trim())) {
console.warn(`[SDK Parser] Invalid observation type: ${type}, skipping`);
logger.warn('PARSER', `Invalid observation type: ${type}, skipping`, { correlationId });
continue;
}
@@ -79,7 +87,7 @@ export function parseObservations(text: string): ParsedObservation[] {
* Parse summary XML block from SDK response
* Returns null if no valid summary found
*/
export function parseSummary(text: string): ParsedSummary | null {
export function parseSummary(text: string, sessionId?: number): ParsedSummary | null {
// Match <summary>...</summary> block (non-greedy)
const summaryRegex = /<summary>([\s\S]*?)<\/summary>/;
const summaryMatch = summaryRegex.exec(text);
@@ -100,7 +108,14 @@ export function parseSummary(text: string): ParsedSummary | null {
// Validate required fields are present (notes is optional)
if (!request || !investigated || !learned || !completed || !next_steps) {
console.warn('[SDK Parser] Summary missing required fields');
logger.warn('PARSER', 'Summary missing required fields', {
sessionId,
hasRequest: !!request,
hasInvestigated: !!investigated,
hasLearned: !!learned,
hasCompleted: !!completed,
hasNextSteps: !!next_steps
});
return null;
}
+88 -1
View File
@@ -1,5 +1,6 @@
import Database from 'better-sqlite3';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
import { logger } from '../../utils/logger.js';
/**
* Lightweight database interface for hooks
@@ -23,6 +24,7 @@ export class HooksDatabase {
this.ensurePromptTrackingColumns();
this.removeSessionSummariesUniqueConstraint();
this.addObservationHierarchicalFields();
this.makeObservationsTextNullable();
}
/**
@@ -195,6 +197,86 @@ export class HooksDatabase {
}
}
/**
* Make observations.text nullable (migration 009)
* The text field is deprecated in favor of structured fields (title, subtitle, narrative, etc.)
*/
private makeObservationsTextNullable(): void {
try {
// Check if text column is already nullable
const tableInfo = this.db.pragma('table_info(observations)');
const textColumn = (tableInfo as any[]).find((col: any) => col.name === 'text');
if (!textColumn || textColumn.notnull === 0) {
// Already migrated or text column doesn't exist
return;
}
console.error('[HooksDatabase] Making observations.text nullable...');
// Begin transaction
this.db.exec('BEGIN TRANSACTION');
try {
// Create new table with text as nullable
this.db.exec(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`);
// Copy data from old table (all existing columns)
this.db.exec(`
INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch
FROM observations
`);
// Drop old table
this.db.exec('DROP TABLE observations');
// Rename new table
this.db.exec('ALTER TABLE observations_new RENAME TO observations');
// Recreate indexes
this.db.exec(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`);
// Commit transaction
this.db.exec('COMMIT');
console.error('[HooksDatabase] Successfully made observations.text nullable');
} catch (error: any) {
// Rollback on error
this.db.exec('ROLLBACK');
throw error;
}
} catch (error: any) {
console.error('[HooksDatabase] Migration error (make text nullable):', error.message);
}
}
/**
* Get recent session summaries for a project
*/
@@ -370,7 +452,12 @@ export class HooksDatabase {
const result = stmt.run(sdkSessionId, id);
if (result.changes === 0) {
console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${id} - already set (prevents FOREIGN KEY constraint violation)`);
// This is expected behavior - sdk_session_id is already set
// Only log at debug level to avoid noise
logger.debug('DB', 'sdk_session_id already set, skipping update', {
sessionId: id,
sdkSessionId
});
return false;
}
+93 -27
View File
@@ -11,6 +11,7 @@ import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from '..
import { parseObservations, parseSummary } from '../sdk/parser.js';
import type { SDKSession } from '../sdk/prompts.js';
import { findAvailablePort } from '../utils/port-allocator.js';
import { logger } from '../utils/logger.js';
const MODEL = 'claude-sonnet-4-5';
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
@@ -42,6 +43,8 @@ interface ActiveSession {
abortController: AbortController;
generatorPromise: Promise<void> | null;
lastPromptNumber: number; // Track which prompt_number we last sent to SDK
observationCounter: number; // Counter for correlation IDs
startTime: number; // Session start timestamp
}
class WorkerService {
@@ -79,14 +82,12 @@ class WorkerService {
db.close();
if (cleanedCount > 0) {
console.log(`[WorkerService] Cleaned up ${cleanedCount} orphaned sessions`);
logger.info('SYSTEM', `Cleaned up ${cleanedCount} orphaned sessions`);
}
return new Promise((resolve, reject) => {
this.app.listen(port, '127.0.0.1', () => {
console.log(`[WorkerService] Started on http://127.0.0.1:${port}`);
console.log(`[WorkerService] PID: ${process.pid}`);
console.log(`[WorkerService] Active sessions: ${this.sessions.size}`);
logger.info('SYSTEM', `Worker started`, { port, pid: process.pid, activeSessions: this.sessions.size });
// Write port to file for hooks to discover
const { writeFileSync } = require('fs');
@@ -122,7 +123,8 @@ class WorkerService {
const sessionDbId = parseInt(req.params.sessionDbId, 10);
const { project, userPrompt } = req.body;
console.log(`[WorkerService] Initializing session ${sessionDbId}`, { project });
const correlationId = logger.sessionId(sessionDbId);
logger.info('WORKER', 'Session init', { correlationId, project });
if (this.sessions.has(sessionDbId)) {
res.status(409).json({ error: 'Session already exists' });
@@ -138,7 +140,9 @@ class WorkerService {
pendingMessages: [],
abortController: new AbortController(),
generatorPromise: null,
lastPromptNumber: 0
lastPromptNumber: 0,
observationCounter: 0,
startTime: Date.now()
};
this.sessions.set(sessionDbId, session);
@@ -150,13 +154,14 @@ class WorkerService {
// Start SDK agent in background
session.generatorPromise = this.runSDKAgent(session).catch(err => {
console.error(`[WorkerService] SDK agent error for session ${sessionDbId}:`, err);
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
const db = new HooksDatabase();
db.markSessionFailed(sessionDbId);
db.close();
this.sessions.delete(sessionDbId);
});
logger.success('WORKER', 'Session initialized', { sessionId: sessionDbId, port: this.port });
res.json({
status: 'initialized',
sessionDbId,
@@ -178,7 +183,15 @@ class WorkerService {
return;
}
console.log(`[WorkerService] Queueing observation for session ${sessionDbId}:`, tool_name);
// Create correlation ID for tracking this observation
session.observationCounter++;
const correlationId = logger.correlationId(sessionDbId, session.observationCounter);
const toolStr = logger.formatTool(tool_name, tool_input);
logger.dataIn('WORKER', `Observation queued: ${toolStr}`, {
correlationId,
queue: session.pendingMessages.length + 1
});
session.pendingMessages.push({
type: 'observation',
@@ -205,7 +218,11 @@ class WorkerService {
return;
}
console.log(`[WorkerService] Requesting summary for session ${sessionDbId}, prompt #${prompt_number}`);
logger.dataIn('WORKER', 'Summary requested', {
sessionId: sessionDbId,
promptNumber: prompt_number,
queue: session.pendingMessages.length + 1
});
session.pendingMessages.push({
type: 'summarize',
@@ -247,7 +264,7 @@ class WorkerService {
return;
}
console.error(`[WorkerService] Deleting session ${sessionDbId}`);
logger.warn('WORKER', 'Session delete requested', { sessionId: sessionDbId });
// Abort SDK agent
session.abortController.abort();
@@ -267,6 +284,7 @@ class WorkerService {
this.sessions.delete(sessionDbId);
logger.info('WORKER', 'Session deleted', { sessionId: sessionDbId });
res.json({ status: 'deleted' });
}
@@ -274,7 +292,7 @@ class WorkerService {
* Run SDK agent for a session
*/
private async runSDKAgent(session: ActiveSession): Promise<void> {
console.log(`[WorkerService] Starting SDK agent for session ${session.sessionDbId}`);
logger.info('SDK', 'Agent starting', { sessionId: session.sessionDbId });
const claudePath = process.env.CLAUDE_CODE_PATH || '/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude';
@@ -300,7 +318,10 @@ class WorkerService {
db.close();
if (updated) {
console.log(`[WorkerService] SDK session initialized:`, systemMsg.session_id);
logger.success('SDK', 'Session initialized', {
sessionId: session.sessionDbId,
sdkSessionId: systemMsg.session_id
});
session.sdkSessionId = systemMsg.session_id;
}
}
@@ -312,7 +333,14 @@ class WorkerService {
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
: typeof content === 'string' ? content : '';
console.log(`[WorkerService] SDK response for prompt #${session.lastPromptNumber}:\n${textContent}`);
const responseSize = textContent.length;
logger.dataOut('SDK', `Response received (${responseSize} chars)`, {
sessionId: session.sessionDbId,
promptNumber: session.lastPromptNumber
});
// In debug mode, log the full response
logger.debug('SDK', 'Full response', { sessionId: session.sessionDbId }, textContent);
// Parse and store with prompt number
this.handleAgentMessage(session, textContent, session.lastPromptNumber);
@@ -320,7 +348,12 @@ class WorkerService {
}
// Mark completed
console.log(`[WorkerService] SDK agent completed for session ${session.sessionDbId}`);
const sessionDuration = Date.now() - session.startTime;
logger.success('SDK', 'Agent completed', {
sessionId: session.sessionDbId,
duration: `${(sessionDuration / 1000).toFixed(1)}s`
});
const db = new HooksDatabase();
db.markSessionCompleted(session.sessionDbId);
db.close();
@@ -329,9 +362,9 @@ class WorkerService {
} catch (error: any) {
if (error.name === 'AbortError') {
console.error(`[WorkerService] SDK agent aborted for session ${session.sessionDbId}`);
logger.warn('SDK', 'Agent aborted', { sessionId: session.sessionDbId });
} else {
console.error(`[WorkerService] SDK agent error for session ${session.sessionDbId}:`, error);
logger.failure('SDK', 'Agent error', { sessionId: session.sessionDbId }, error);
}
throw error;
}
@@ -345,7 +378,11 @@ class WorkerService {
const claudeSessionId = `session-${session.sessionDbId}`;
const initPrompt = buildInitPrompt(session.project, claudeSessionId, session.userPrompt);
console.log(`[WorkerService] Yielding init prompt:\n${initPrompt}`);
logger.dataIn('SDK', `Init prompt sent (${initPrompt.length} chars)`, {
sessionId: session.sessionDbId,
project: session.project
});
logger.debug('SDK', 'Full init prompt', { sessionId: session.sessionDbId }, initPrompt);
yield {
type: 'user',
@@ -372,7 +409,6 @@ class WorkerService {
const message = session.pendingMessages.shift()!;
if (message.type === 'summarize') {
console.log(`[WorkerService] Processing SUMMARIZE for session ${session.sessionDbId}, prompt #${message.prompt_number}`);
session.lastPromptNumber = message.prompt_number;
const db = new HooksDatabase();
@@ -382,7 +418,11 @@ class WorkerService {
if (dbSession) {
const summarizePrompt = buildFinalizePrompt(dbSession);
console.log(`[WorkerService] Yielding summarize prompt:\n${summarizePrompt}`);
logger.dataIn('SDK', `Summary prompt sent (${summarizePrompt.length} chars)`, {
sessionId: session.sessionDbId,
promptNumber: message.prompt_number
});
logger.debug('SDK', 'Full summary prompt', { sessionId: session.sessionDbId }, summarizePrompt);
yield {
type: 'user',
@@ -405,7 +445,15 @@ class WorkerService {
created_at_epoch: Date.now()
});
console.log(`[WorkerService] Yielding observation (prompt #${message.prompt_number}):\n${observationPrompt}`);
const toolStr = logger.formatTool(message.tool_name, message.tool_input);
const correlationId = logger.correlationId(session.sessionDbId, session.observationCounter);
logger.dataIn('SDK', `Observation prompt: ${toolStr}`, {
correlationId,
promptNumber: message.prompt_number,
size: `${observationPrompt.length} chars`
});
logger.debug('SDK', 'Full observation prompt', { correlationId }, observationPrompt);
yield {
type: 'user',
@@ -426,23 +474,41 @@ class WorkerService {
* Gets prompt_number from the message that triggered this response
*/
private handleAgentMessage(session: ActiveSession, content: string, promptNumber: number): void {
const correlationId = logger.correlationId(session.sessionDbId, session.observationCounter);
// Parse observations
const observations = parseObservations(content);
console.log(`[WorkerService] Parsed ${observations.length} observations for prompt #${promptNumber}`);
const observations = parseObservations(content, correlationId);
if (observations.length > 0) {
logger.info('PARSER', `Parsed ${observations.length} observation(s)`, {
correlationId,
promptNumber,
types: observations.map(o => o.type).join(', ')
});
}
const db = new HooksDatabase();
for (const obs of observations) {
if (session.sdkSessionId) {
db.storeObservation(session.sdkSessionId, session.project, obs, promptNumber);
logger.success('DB', 'Observation stored', {
correlationId,
type: obs.type,
title: obs.title
});
}
}
// Parse summary
const summary = parseSummary(content);
const summary = parseSummary(content, session.sessionDbId);
if (summary && session.sdkSessionId) {
console.log(`[WorkerService] Parsed summary for session ${session.sessionDbId}, prompt #${promptNumber}`);
logger.info('PARSER', 'Summary parsed', {
sessionId: session.sessionDbId,
promptNumber
});
db.storeSummary(session.sdkSessionId, session.project, summary, promptNumber);
logger.success('DB', 'Summary stored', { sessionId: session.sessionDbId });
}
db.close();
@@ -456,19 +522,19 @@ async function main() {
// Graceful shutdown
process.on('SIGINT', () => {
console.error('[WorkerService] Shutting down gracefully...');
logger.warn('SYSTEM', 'Shutting down (SIGINT)');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('[WorkerService] Shutting down gracefully...');
logger.warn('SYSTEM', 'Shutting down (SIGTERM)');
process.exit(0);
});
}
// Auto-start when run directly (not when imported)
main().catch(err => {
console.error('[WorkerService] Fatal error:', err);
logger.failure('SYSTEM', 'Fatal startup error', {}, err);
process.exit(1);
});
+234
View File
@@ -0,0 +1,234 @@
/**
* Structured Logger for claude-mem Worker Service
* Provides readable, traceable logging with correlation IDs and data flow tracking
*/
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
SILENT = 4
}
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM';
interface LogContext {
sessionId?: number;
sdkSessionId?: string;
correlationId?: string;
[key: string]: any;
}
class Logger {
private level: LogLevel;
private useColor: boolean;
constructor() {
// Parse log level from environment
const envLevel = process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase() || 'INFO';
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
// Disable colors when output is not a TTY (e.g., PM2 logs)
this.useColor = process.stdout.isTTY ?? false;
}
/**
* Create correlation ID for tracking an observation through the pipeline
*/
static correlationId(sessionId: number, observationNum: number): string {
return `obs-${sessionId}-${observationNum}`;
}
/**
* Create session correlation ID
*/
static sessionId(sessionId: number): string {
return `session-${sessionId}`;
}
/**
* Format data for logging - create compact summaries instead of full dumps
*/
private formatData(data: any): string {
if (data === null || data === undefined) return '';
if (typeof data === 'string') return data;
if (typeof data === 'number') return data.toString();
if (typeof data === 'boolean') return data.toString();
// For objects, create compact summaries
if (typeof data === 'object') {
// If it's an error, show message and stack in debug mode
if (data instanceof Error) {
return this.level === LogLevel.DEBUG
? `${data.message}\n${data.stack}`
: data.message;
}
// For arrays, show count
if (Array.isArray(data)) {
return `[${data.length} items]`;
}
// For objects, show key count
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
// Show small objects inline
return JSON.stringify(data);
}
return `{${keys.length} keys: ${keys.slice(0, 3).join(', ')}...}`;
}
return String(data);
}
/**
* Format a tool name and input for compact display
*/
formatTool(toolName: string, toolInput?: any): string {
if (!toolInput) return toolName;
try {
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
// Special formatting for common tools
if (toolName === 'Bash' && input.command) {
const cmd = input.command.length > 50
? input.command.substring(0, 50) + '...'
: input.command;
return `${toolName}(${cmd})`;
}
if (toolName === 'Read' && input.file_path) {
const path = input.file_path.split('/').pop() || input.file_path;
return `${toolName}(${path})`;
}
if (toolName === 'Edit' && input.file_path) {
const path = input.file_path.split('/').pop() || input.file_path;
return `${toolName}(${path})`;
}
if (toolName === 'Write' && input.file_path) {
const path = input.file_path.split('/').pop() || input.file_path;
return `${toolName}(${path})`;
}
// Default: just show tool name
return toolName;
} catch {
return toolName;
}
}
/**
* Core logging method
*/
private log(
level: LogLevel,
component: Component,
message: string,
context?: LogContext,
data?: any
): void {
if (level < this.level) return;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
const levelStr = LogLevel[level].padEnd(5);
const componentStr = component.padEnd(6);
// Build correlation ID part
let correlationStr = '';
if (context?.correlationId) {
correlationStr = `[${context.correlationId}] `;
} else if (context?.sessionId) {
correlationStr = `[session-${context.sessionId}] `;
}
// Build data part
let dataStr = '';
if (data !== undefined && data !== null) {
if (this.level === LogLevel.DEBUG && typeof data === 'object') {
// In debug mode, show full JSON for objects
dataStr = '\n' + JSON.stringify(data, null, 2);
} else {
dataStr = ' ' + this.formatData(data);
}
}
// Build additional context
let contextStr = '';
if (context) {
const { sessionId, sdkSessionId, correlationId, ...rest } = context;
if (Object.keys(rest).length > 0) {
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
contextStr = ` {${pairs.join(', ')}}`;
}
}
const logLine = `[${timestamp}] [${levelStr}] [${componentStr}] ${correlationStr}${message}${contextStr}${dataStr}`;
// Output to appropriate stream
if (level === LogLevel.ERROR) {
console.error(logLine);
} else {
console.log(logLine);
}
}
// Public logging methods
debug(component: Component, message: string, context?: LogContext, data?: any): void {
this.log(LogLevel.DEBUG, component, message, context, data);
}
info(component: Component, message: string, context?: LogContext, data?: any): void {
this.log(LogLevel.INFO, component, message, context, data);
}
warn(component: Component, message: string, context?: LogContext, data?: any): void {
this.log(LogLevel.WARN, component, message, context, data);
}
error(component: Component, message: string, context?: LogContext, data?: any): void {
this.log(LogLevel.ERROR, component, message, context, data);
}
/**
* Log data flow: input → processing
*/
dataIn(component: Component, message: string, context?: LogContext, data?: any): void {
this.info(component, `${message}`, context, data);
}
/**
* Log data flow: processing → output
*/
dataOut(component: Component, message: string, context?: LogContext, data?: any): void {
this.info(component, `${message}`, context, data);
}
/**
* Log successful completion
*/
success(component: Component, message: string, context?: LogContext, data?: any): void {
this.info(component, `${message}`, context, data);
}
/**
* Log failure
*/
failure(component: Component, message: string, context?: LogContext, data?: any): void {
this.error(component, `${message}`, context, data);
}
/**
* Log timing information
*/
timing(component: Component, message: string, durationMs: number, context?: LogContext): void {
this.info(component, `${message}`, context, { duration: `${durationMs}ms` });
}
}
// Export singleton instance
export const logger = new Logger();