feat: Implement session management and search functionality
- Added SDK session, observation, and summary types to types.ts. - Refactored worker-service to use SessionStore for session management. - Created SessionSearch class for FTS5 full-text search and structured queries. - Implemented SessionStore for CRUD operations on SDK sessions, observations, and summaries. - Added migrations for database schema updates, including new columns and constraints. - Enhanced search capabilities with filters for projects, types, concepts, and date ranges.
This commit is contained in:
Vendored
+4
-4
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import N from"better-sqlite3";import{join as a,dirname as P,basename as B}from"path";import{homedir as k}from"os";import{existsSync as j,mkdirSync as I}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(k(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(k(),".claude"),W=a(c,"archives"),G=a(c,"logs"),q=a(c,"trash"),K=a(c,"backups"),Y=a(c,"settings.json"),S=a(c,"claude-mem.db"),J=a(u,"settings.json"),V=a(u,"commands"),Q=a(u,"CLAUDE.md");function R(o){I(o,{recursive:!0})}var _=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(_||{}),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}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),l=_[e].padEnd(5),b=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let p="";n!=null&&(this.level===0&&typeof n=="object"?p=`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let T="";if(r){let{sessionId:C,sdkSessionId:y,correlationId:x,...h}=r;Object.keys(h).length>0&&(T=` {${Object.entries(h).map(([O,v])=>`${O}=${v}`).join(", ")}}`)}let f=`[${d}] [${l}] [${b}] ${i}${s}${T}${p}`;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`})}},D=new E;var m=class{db;constructor(){R(c),this.db=new N(S),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 b=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(`
|
||||
import N from"better-sqlite3";import{join as a,dirname as B,basename as H}from"path";import{homedir as S}from"os";import{existsSync as j,mkdirSync as I}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(S(),".claude-mem"),u=process.env.CLAUDE_CONFIG_DIR||a(S(),".claude"),W=a(c,"archives"),G=a(c,"logs"),q=a(c,"trash"),K=a(c,"backups"),Y=a(c,"settings.json"),R=a(c,"claude-mem.db"),J=a(u,"settings.json"),V=a(u,"commands"),Q=a(u,"CLAUDE.md");function L(o){I(o,{recursive:!0})}var _=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(_||{}),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}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let d=new Date().toISOString().replace("T"," ").substring(0,23),l=_[e].padEnd(5),T=t.padEnd(6),i="";r?.correlationId?i=`[${r.correlationId}] `:r?.sessionId&&(i=`[session-${r.sessionId}] `);let p="";n!=null&&(this.level===0&&typeof n=="object"?p=`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let b="";if(r){let{sessionId:C,sdkSessionId:y,correlationId:x,...h}=r;Object.keys(h).length>0&&(b=` {${Object.entries(h).map(([O,v])=>`${O}=${v}`).join(", ")}}`)}let f=`[${d}] [${l}] [${T}] ${i}${s}${b}${p}`;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`})}},k=new E;var m=class{db;constructor(){L(c),this.db=new N(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(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[SessionStore] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -29,7 +29,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(s=>s.name==="title"))return;console.error("[SessionStore] 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;
|
||||
@@ -37,7 +37,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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)}}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(`
|
||||
`),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] 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("[SessionStore] 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,
|
||||
@@ -67,7 +67,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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(`
|
||||
`),this.db.exec("COMMIT"),console.error("[SessionStore] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] 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
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0?(D.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
`).run(t,e).changes===0?(k.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 = ?
|
||||
@@ -177,5 +177,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};async function L(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 A}from"process";var g="";A.on("data",o=>g+=o);A.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await L(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 A(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 D}from"process";var g="";D.on("data",o=>g+=o);D.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await A(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import N from"path";import I from"better-sqlite3";import{join as p,dirname as H,basename as P}from"path";import{homedir as S}from"os";import{existsSync as X,mkdirSync as y}from"fs";var d=process.env.CLAUDE_MEM_DATA_DIR||p(S(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||p(S(),".claude"),W=p(d,"archives"),G=p(d,"logs"),q=p(d,"trash"),Y=p(d,"backups"),J=p(d,"settings.json"),D=p(d,"claude-mem.db"),K=p(_,"settings.json"),V=p(_,"commands"),Q=p(_,"CLAUDE.md");function k(c){y(c,{recursive:!0})}var E=(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))(E||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=E[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import C from"path";import O from"better-sqlite3";import{join as p,dirname as P,basename as B}from"path";import{homedir as R}from"os";import{existsSync as X,mkdirSync as N}from"fs";var d=process.env.CLAUDE_MEM_DATA_DIR||p(R(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||p(R(),".claude"),W=p(d,"archives"),G=p(d,"logs"),q=p(d,"trash"),Y=p(d,"backups"),J=p(d,"settings.json"),A=p(d,"claude-mem.db"),K=p(_,"settings.json"),V=p(_,"commands"),Q=p(_,"CLAUDE.md");function L(c){N(c,{recursive:!0})}var E=(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))(E||{}),g=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=E[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let n=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${n})`}if(e==="Read"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Edit"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}if(e==="Write"&&s.file_path){let n=s.file_path.split("/").pop()||s.file_path;return`${e}(${n})`}return e}catch{return e}}log(e,t,s,n,r){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=E[e].padEnd(5),u=t.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";r!=null&&(this.level===0&&typeof r=="object"?l=`
|
||||
`+JSON.stringify(r,null,2):l=" "+this.formatData(r));let h="";if(n){let{sessionId:O,sdkSessionId:C,correlationId:x,...R}=n;Object.keys(R).length>0&&(h=` {${Object.entries(R).map(([L,v])=>`${L}=${v}`).join(", ")}}`)}let f=`[${i}] [${o}] [${u}] ${a}${s}${h}${l}`;e===3?console.error(f):console.log(f)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}},A=new g;var m=class{db;constructor(){k(d),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(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 u=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(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(`
|
||||
`+JSON.stringify(r,null,2):l=" "+this.formatData(r));let h="";if(n){let{sessionId:D,sdkSessionId:k,correlationId:x,...S}=n;Object.keys(S).length>0&&(h=` {${Object.entries(S).map(([y,I])=>`${y}=${I}`).join(", ")}}`)}let f=`[${i}] [${o}] [${u}] ${a}${s}${h}${l}`;e===3?console.error(f):console.log(f)}debug(e,t,s,n){this.log(0,e,t,s,n)}info(e,t,s,n){this.log(1,e,t,s,n)}warn(e,t,s,n){this.log(2,e,t,s,n)}error(e,t,s,n){this.log(3,e,t,s,n)}dataIn(e,t,s,n){this.info(e,`\u2192 ${t}`,s,n)}dataOut(e,t,s,n){this.info(e,`\u2190 ${t}`,s,n)}success(e,t,s,n){this.info(e,`\u2713 ${t}`,s,n)}failure(e,t,s,n){this.error(e,`\u2717 ${t}`,s,n)}timing(e,t,s,n){this.info(e,`\u23F1 ${t}`,n,{duration:`${s}ms`})}},v=new g;var m=class{db;constructor(){L(d),this.db=new O(A),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("[SessionStore] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] Added prompt_number column to session_summaries table"));let u=this.db.pragma("index_list(session_summaries)").some(a=>a.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -29,7 +29,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(s=>s.name==="title"))return;console.error("[SessionStore] 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;
|
||||
@@ -37,7 +37,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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)}}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(`
|
||||
`),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] 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("[SessionStore] 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,
|
||||
@@ -67,7 +67,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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(`
|
||||
`),this.db.exec("COMMIT"),console.error("[SessionStore] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] 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
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0?(A.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
`).run(t,e).changes===0?(v.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:t}),!1):!0}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -177,7 +177,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function b(c){let e=c?.cwd??process.cwd(),t=e?N.basename(e):"unknown-project",s=new m;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0){console.log(`# Recent Session Context
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function T(c){let e=c?.cwd??process.cwd(),t=e?C.basename(e):"unknown-project",s=new m;try{let n=s.getRecentSessionsWithStatus(t,3);if(n.length===0){console.log(`# Recent Session Context
|
||||
|
||||
No previous sessions found for this project yet.`);return}let r=[];r.push("# Recent Session Context"),r.push(""),r.push(`Showing last ${n.length} session(s) for **${t}**:`),r.push("");for(let i of n)if(i.sdk_session_id){if(r.push("---"),r.push(""),i.has_summary){let o=s.getSummaryForSession(i.sdk_session_id);if(o){let u=o.prompt_number?` (Prompt #${o.prompt_number})`:"";if(r.push(`**Summary${u}**`),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 a=JSON.parse(o.files_read);Array.isArray(a)&&a.length>0&&r.push(`**Files Read:** ${a.join(", ")}`)}catch{o.files_read.trim()&&r.push(`**Files Read:** ${o.files_read}`)}if(o.files_edited)try{let a=JSON.parse(o.files_edited);Array.isArray(a)&&a.length>0&&r.push(`**Files Edited:** ${a.join(", ")}`)}catch{o.files_edited.trim()&&r.push(`**Files Edited:** ${o.files_edited}`)}r.push(`**Date:** ${o.created_at.split("T")[0]}`)}}else if(i.status==="active"){r.push("**In Progress**"),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`);let o=s.getObservationsForSession(i.sdk_session_id);if(o.length>0){r.push(""),r.push(`**Observations (${o.length}):**`);for(let u of o)r.push(`- ${u.title}`)}else r.push(""),r.push("*No observations yet*");r.push(""),r.push("**Status:** Active - summary pending"),r.push(`**Date:** ${i.started_at.split("T")[0]}`)}else r.push(`**${i.status.charAt(0).toUpperCase()+i.status.slice(1)}**`),r.push(""),i.user_prompt&&r.push(`**Request:** ${i.user_prompt}`),r.push(""),r.push(`**Status:** ${i.status} - no summary available`),r.push(`**Date:** ${i.started_at.split("T")[0]}`);r.push("")}console.log(r.join(`
|
||||
`))}finally{s.close()}}import{stdin as T}from"process";try{if(T.isTTY)b();else{let c="";T.on("data",e=>c+=e),T.on("end",()=>{let e=c.trim()?JSON.parse(c):void 0;b(e),process.exit(0)})}}catch(c){console.error(`[claude-mem context-hook error: ${c.message}]`),process.exit(0)}
|
||||
`))}finally{s.close()}}import{stdin as b}from"process";try{if(b.isTTY)T();else{let c="";b.on("data",e=>c+=e),b.on("end",()=>{let e=c.trim()?JSON.parse(c):void 0;T(e),process.exit(0)})}}catch(c){console.error(`[claude-mem context-hook error: ${c.message}]`),process.exit(0)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import x from"path";import I from"better-sqlite3";import{join as u,dirname as j,basename as X}from"path";import{homedir as k}from"os";import{existsSync as K,mkdirSync as y}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||u(k(),".claude-mem"),g=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(g,"settings.json"),ee=u(g,"commands"),se=u(g,"CLAUDE.md");function O(o){y(o,{recursive:!0})}var b=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(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}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import x from"path";import v from"better-sqlite3";import{join as u,dirname as j,basename as X}from"path";import{homedir as R}from"os";import{existsSync as K,mkdirSync as I}from"fs";var m=process.env.CLAUDE_MEM_DATA_DIR||u(R(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||u(R(),".claude"),J=u(m,"archives"),Y=u(m,"logs"),V=u(m,"trash"),Q=u(m,"backups"),z=u(m,"settings.json"),k=u(m,"claude-mem.db"),Z=u(g,"settings.json"),ee=u(g,"commands"),se=u(g,"CLAUDE.md");function O(o){I(o,{recursive:!0})}var b=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(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}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let a=new Date().toISOString().replace("T"," ").substring(0,23),i=b[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let p="";n!=null&&(this.level===0&&typeof n=="object"?p=`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let l="";if(r){let{sessionId:U,sdkSessionId:P,correlationId:H,...S}=r;Object.keys(S).length>0&&(l=` {${Object.entries(S).map(([C,N])=>`${C}=${N}`).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`})}},D=new T;var _=class{db;constructor(){O(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(`
|
||||
`+JSON.stringify(n,null,2):p=" "+this.formatData(n));let l="";if(r){let{sessionId:U,sdkSessionId:P,correlationId:M,...h}=r;Object.keys(h).length>0&&(l=` {${Object.entries(h).map(([N,y])=>`${N}=${y}`).join(", ")}}`)}let S=`[${a}] [${i}] [${d}] ${c}${t}${l}${p}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new T;var _=class{db;constructor(){O(m),this.db=new v(k),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("[SessionStore] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[SessionStore] 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("[SessionStore] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[SessionStore] Added prompt_number column to session_summaries table"));let d=this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)}catch(e){console.error("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -29,7 +29,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(t=>t.name==="title"))return;console.error("[SessionStore] 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;
|
||||
@@ -37,7 +37,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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)}}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(`
|
||||
`),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] 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("[SessionStore] 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,
|
||||
@@ -67,7 +67,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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(`
|
||||
`),this.db.exec("COMMIT"),console.error("[SessionStore] Successfully made observations.text nullable")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] 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
|
||||
@@ -146,7 +146,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(D.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(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 = ?
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function v(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function E(o,e,s={}){let t=v(o,e,s);return JSON.stringify(t)}async function w(){let{readFileSync:o,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 n=o(r,"utf8").trim();return parseInt(n,10)}catch{return null}}async function A(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=x.basename(s),n=new _;try{let a=n.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let p=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${p}`)}else{let p=n.findAnySDKSession(e);if(p){i=p.id,n.reactivateSession(i,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${l}`)}else{i=n.createSDKSession(e,r,t);let l=n.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{n.close()}}import{stdin as L}from"process";var f="";L.on("data",o=>f+=o);L.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await A(o),process.exit(0)}catch(o){console.error(`[claude-mem new-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function D(o,e,s){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function E(o,e,s={}){let t=D(o,e,s);return JSON.stringify(t)}async function w(){let{readFileSync:o,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 n=o(r,"utf8").trim();return parseInt(n,10)}catch{return null}}async function L(o){if(!o)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=o,r=x.basename(s),n=new _;try{let a=n.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let p=n.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${p}`)}else{let p=n.findAnySDKSession(e);if(p){i=p.id,n.reactivateSession(i,t);let l=n.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${l}`)}else{i=n.createSDKSession(e,r,t);let l=n.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{n.close()}}import{stdin as C}from"process";var f="";C.on("data",o=>f+=o);C.on("end",async()=>{try{let o=f.trim()?JSON.parse(f):void 0;await L(o),process.exit(0)}catch(o){console.error(`[claude-mem new-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import C from"better-sqlite3";import{join as c,dirname as X,basename as j}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as v}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||c(O(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(O(),".claude"),q=c(p,"archives"),J=c(p,"logs"),Y=c(p,"trash"),V=c(p,"backups"),Q=c(p,"settings.json"),k=c(p,"claude-mem.db"),z=c(g,"settings.json"),Z=c(g,"commands"),ee=c(g,"CLAUDE.md");function L(n){v(n,{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||{}),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}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=b[e].padEnd(5),_=s.padEnd(6),a="";r?.correlationId?a=`[${r.correlationId}] `:r?.sessionId&&(a=`[session-${r.sessionId}] `);let m="";o!=null&&(this.level===0&&typeof o=="object"?m=`
|
||||
`+JSON.stringify(o,null,2):m=" "+this.formatData(o));let h="";if(r){let{sessionId:U,sdkSessionId:H,correlationId:w,...R}=r;Object.keys(R).length>0&&(h=` {${Object.entries(R).map(([I,N])=>`${I}=${N}`).join(", ")}}`)}let S=`[${i}] [${d}] [${_}] ${a}${t}${h}${m}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new T;var E=class{db;constructor(){L(p),this.db=new C(k),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(`
|
||||
import y from"better-sqlite3";import{join as c,dirname as X,basename as j}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as C}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||c(O(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(O(),".claude"),q=c(p,"archives"),J=c(p,"logs"),Y=c(p,"trash"),V=c(p,"backups"),Q=c(p,"settings.json"),L=c(p,"claude-mem.db"),z=c(g,"settings.json"),Z=c(g,"commands"),ee=c(g,"CLAUDE.md");function k(n){C(n,{recursive:!0})}var T=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(T||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),d=T[e].padEnd(5),_=s.padEnd(6),a="";r?.correlationId?a=`[${r.correlationId}] `:r?.sessionId&&(a=`[session-${r.sessionId}] `);let m="";o!=null&&(this.level===0&&typeof o=="object"?m=`
|
||||
`+JSON.stringify(o,null,2):m=" "+this.formatData(o));let S="";if(r){let{sessionId:U,sdkSessionId:w,correlationId:P,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([N,v])=>`${N}=${v}`).join(", ")}}`)}let h=`[${i}] [${d}] [${_}] ${a}${t}${S}${m}`;e===3?console.error(h):console.log(h)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},u=new b;var E=class{db;constructor(){k(p),this.db=new y(L),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("[SessionStore] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(t=>t.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -29,7 +29,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(t=>t.name==="title"))return;console.error("[SessionStore] 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;
|
||||
@@ -37,7 +37,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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)}}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(`
|
||||
`),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] 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("[SessionStore] 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,
|
||||
@@ -67,7 +67,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
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(`
|
||||
`),this.db.exec("COMMIT"),console.error("[SessionStore] Successfully made observations.text nullable")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] 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
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function 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 D(n){if(!n)throw new Error("saveHook requires input");let{session_id:e,tool_name:s,tool_input:t,tool_output:r}=n;if(x.has(s)){console.log(l("PostToolUse",!0));return}let o=new E,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port){o.close(),u.error("HOOK","No worker port for session",{sessionId:i.id}),console.log(l("PostToolUse",!0));return}let d=o.getPromptCounter(i.id);o.close();let _=u.formatTool(s,t);try{u.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:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(a.ok)u.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s});else{let m=await a.text();u.failure("HOOK","Failed to send observation",{sessionId:i.id,status:a.status},m)}}catch(a){u.failure("HOOK","Error sending observation",{sessionId:i.id},a)}finally{console.log(l("PostToolUse",!0))}}import{stdin as A}from"process";var f="";A.on("data",n=>f+=n);A.on("end",async()=>{try{let n=f.trim()?JSON.parse(f):void 0;await D(n),process.exit(0)}catch(n){console.error(`[claude-mem save-hook error: ${n.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};function D(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=D(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:r}=n;if(x.has(s)){console.log(l("PostToolUse",!0));return}let o=new E,i=o.findActiveSDKSession(e);if(!i){o.close(),console.log(l("PostToolUse",!0));return}if(!i.worker_port){o.close(),u.error("HOOK","No worker port for session",{sessionId:i.id}),console.log(l("PostToolUse",!0));return}let d=o.getPromptCounter(i.id);o.close();let _=u.formatTool(s,t);try{u.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:r!==void 0?JSON.stringify(r):"{}",prompt_number:d}),signal:AbortSignal.timeout(2e3)});if(a.ok)u.debug("HOOK","Observation sent successfully",{sessionId:i.id,toolName:s});else{let m=await a.text();u.failure("HOOK","Failed to send observation",{sessionId:i.id,status:a.status},m)}}catch(a){u.failure("HOOK","Error sending observation",{sessionId:i.id},a)}finally{console.log(l("PostToolUse",!0))}}import{stdin as I}from"process";var f="";I.on("data",n=>f+=n);I.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)}});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import N from"better-sqlite3";import{join as a,dirname as $,basename as X}from"path";import{homedir as k}from"os";import{existsSync as G,mkdirSync as C}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(k(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||a(k(),".claude"),q=a(c,"archives"),K=a(c,"logs"),J=a(c,"trash"),Y=a(c,"backups"),V=a(c,"settings.json"),O=a(c,"claude-mem.db"),Q=a(E,"settings.json"),z=a(E,"commands"),Z=a(E,"CLAUDE.md");function D(o){C(o,{recursive:!0})}var g=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import v from"better-sqlite3";import{join as a,dirname as $,basename as X}from"path";import{homedir as O}from"os";import{existsSync as G,mkdirSync as N}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||a(O(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||a(O(),".claude"),q=a(c,"archives"),K=a(c,"logs"),J=a(c,"trash"),Y=a(c,"backups"),V=a(c,"settings.json"),k=a(c,"claude-mem.db"),Q=a(E,"settings.json"),z=a(E,"commands"),Z=a(E,"CLAUDE.md");function L(o){N(o,{recursive:!0})}var g=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(g||{}),b=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=g[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,t){return`obs-${e}-${t}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;try{let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command){let r=s.command.length>50?s.command.substring(0,50)+"...":s.command;return`${e}(${r})`}if(e==="Read"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Edit"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}if(e==="Write"&&s.file_path){let r=s.file_path.split("/").pop()||s.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,t,s,r,n){if(e<this.level)return;let u=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 _="";n!=null&&(this.level===0&&typeof n=="object"?_=`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let h="";if(r){let{sessionId:x,sdkSessionId:H,correlationId:U,...R}=r;Object.keys(R).length>0&&(h=` {${Object.entries(R).map(([I,y])=>`${I}=${y}`).join(", ")}}`)}let S=`[${u}] [${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`})}},p=new b;var m=class{db;constructor(){D(c),this.db=new N(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(`
|
||||
`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let S="";if(r){let{sessionId:x,sdkSessionId:U,correlationId:w,...R}=r;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([y,C])=>`${y}=${C}`).join(", ")}}`)}let h=`[${u}] [${d}] [${f}] ${i}${s}${S}${_}`;e===3?console.error(h):console.log(h)}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`})}},p=new b;var m=class{db;constructor(){L(c),this.db=new v(k),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("[SessionStore] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] 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("[SessionStore] Prompt tracking migration error:",e.message)}}removeSessionSummariesUniqueConstraint(){try{if(!this.db.pragma("index_list(session_summaries)").some(s=>s.unique===1))return;console.error("[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id..."),this.db.exec("BEGIN TRANSACTION");try{this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
@@ -29,7 +29,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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("[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] Migration error (remove UNIQUE constraint):",e.message)}}addObservationHierarchicalFields(){try{if(this.db.pragma("table_info(observations)").some(s=>s.name==="title"))return;console.error("[SessionStore] 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;
|
||||
@@ -37,7 +37,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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)}}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(`
|
||||
`),console.error("[SessionStore] Successfully added hierarchical fields to observations table")}catch(e){console.error("[SessionStore] 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("[SessionStore] 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,
|
||||
@@ -67,7 +67,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
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(`
|
||||
`),this.db.exec("COMMIT"),console.error("[SessionStore] Successfully made observations.text nullable")}catch(s){throw this.db.exec("ROLLBACK"),s}}catch(e){console.error("[SessionStore] 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
|
||||
@@ -177,4 +177,4 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function v(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function l(o,e,t={}){let s=v(o,e,t);return JSON.stringify(s)}async function L(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(l("Stop",!0));return}if(!s.worker_port){t.close(),p.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{p.dataIn("HOOK","Stop: Requesting summary",{sessionId:s.id,workerPort:s.worker_port,promptNumber:r});let n=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(n.ok)p.debug("HOOK","Summary request sent successfully",{sessionId:s.id});else{let u=await n.text();p.failure("HOOK","Failed to generate summary",{sessionId:s.id,status:n.status},u)}}catch(n){p.failure("HOOK","Error requesting summary",{sessionId:s.id},n)}finally{console.log(l("Stop",!0))}}import{stdin as A}from"process";var T="";A.on("data",o=>T+=o);A.on("end",async()=>{try{let o=T.trim()?JSON.parse(T):void 0;await L(o),process.exit(0)}catch(o){console.error(`[claude-mem summary-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function D(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function l(o,e,t={}){let s=D(o,e,t);return JSON.stringify(s)}async function A(o){if(!o)throw new Error("summaryHook requires input");let{session_id:e}=o,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(l("Stop",!0));return}if(!s.worker_port){t.close(),p.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{p.dataIn("HOOK","Stop: Requesting summary",{sessionId:s.id,workerPort:s.worker_port,promptNumber:r});let n=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(n.ok)p.debug("HOOK","Summary request sent successfully",{sessionId:s.id});else{let u=await n.text();p.failure("HOOK","Failed to generate summary",{sessionId:s.id,status:n.status},u)}}catch(n){p.failure("HOOK","Error requesting summary",{sessionId:s.id},n)}finally{console.log(l("Stop",!0))}}import{stdin as I}from"process";var T="";I.on("data",o=>T+=o);I.on("end",async()=>{try{let o=T.trim()?JSON.parse(T):void 0;await A(o),process.exit(0)}catch(o){console.error(`[claude-mem summary-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
|
||||
export interface SessionEndInput {
|
||||
session_id: string;
|
||||
@@ -46,7 +46,7 @@ export async function cleanupHook(input?: SessionEndInput): Promise<void> {
|
||||
console.error('[claude-mem cleanup] Searching for active SDK session', { session_id, reason });
|
||||
|
||||
// Find active SDK session
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
|
||||
export interface SessionStartInput {
|
||||
session_id?: string;
|
||||
@@ -20,7 +20,7 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
const cwd = input?.cwd ?? process.cwd();
|
||||
const project = cwd ? path.basename(cwd) : 'unknown-project';
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
const sessions = db.getRecentSessionsWithStatus(project, 3);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
|
||||
export interface UserPromptSubmitInput {
|
||||
@@ -42,7 +42,7 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
|
||||
const { session_id, cwd, prompt } = input;
|
||||
const project = path.basename(cwd);
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
|
||||
try {
|
||||
// Check for any existing session (active, failed, or completed)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
}
|
||||
|
||||
const { session_id } = input;
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
|
||||
if (!session) {
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import net from 'net';
|
||||
import { unlinkSync, existsSync } from 'fs';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { HooksDatabase } from '../services/sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { getWorkerSocketPath } from '../shared/paths.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from './prompts.js';
|
||||
import { parseObservations, parseSummary } from './parser.js';
|
||||
@@ -60,7 +60,7 @@ export async function main() {
|
||||
*/
|
||||
class SDKWorker {
|
||||
private sessionDbId: number;
|
||||
private db: HooksDatabase;
|
||||
private db: SessionStore;
|
||||
private socketPath: string;
|
||||
private server: net.Server | null = null;
|
||||
private sdkSessionId: string | null = null;
|
||||
@@ -72,7 +72,7 @@ class SDKWorker {
|
||||
|
||||
constructor(sessionDbId: number) {
|
||||
this.sessionDbId = sessionDbId;
|
||||
this.db = new HooksDatabase();
|
||||
this.db = new SessionStore();
|
||||
this.abortController = new AbortController();
|
||||
this.socketPath = getWorkerSocketPath(sessionDbId);
|
||||
console.error('[claude-mem worker] Worker instance created', {
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||
import {
|
||||
ObservationSearchResult,
|
||||
SessionSummarySearchResult,
|
||||
SearchOptions,
|
||||
SearchFilters,
|
||||
DateRange,
|
||||
ObservationRow
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Search interface for session-based memory
|
||||
* Provides FTS5 full-text search and structured queries for sessions, observations, and summaries
|
||||
*/
|
||||
export class SessionSearch {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(dbPath?: string) {
|
||||
if (!dbPath) {
|
||||
ensureDir(DATA_DIR);
|
||||
dbPath = DB_PATH;
|
||||
}
|
||||
this.db = new Database(dbPath);
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
|
||||
// Ensure FTS tables exist
|
||||
this.ensureFTSTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure FTS5 tables exist (inline migration)
|
||||
*/
|
||||
private ensureFTSTables(): void {
|
||||
try {
|
||||
// Check if FTS tables already exist
|
||||
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_fts'").all() as any[];
|
||||
const hasFTS = tables.some((t: any) => t.name === 'observations_fts' || t.name === 'session_summaries_fts');
|
||||
|
||||
if (hasFTS) {
|
||||
// Already migrated
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[SessionSearch] Creating FTS5 tables...');
|
||||
|
||||
// Create observations_fts virtual table
|
||||
this.db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
`);
|
||||
|
||||
// Populate with existing data
|
||||
this.db.exec(`
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
SELECT id, title, subtitle, narrative, text, facts, concepts
|
||||
FROM observations;
|
||||
`);
|
||||
|
||||
// Create triggers for observations
|
||||
this.db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
`);
|
||||
|
||||
// Create session_summaries_fts virtual table
|
||||
this.db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
`);
|
||||
|
||||
// Populate with existing data
|
||||
this.db.exec(`
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
SELECT id, request, investigated, learned, completed, next_steps, notes
|
||||
FROM session_summaries;
|
||||
`);
|
||||
|
||||
// Create triggers for session_summaries
|
||||
this.db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_ad AFTER DELETE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_au AFTER UPDATE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
`);
|
||||
|
||||
console.error('[SessionSearch] FTS5 tables created successfully');
|
||||
} catch (error: any) {
|
||||
console.error('[SessionSearch] FTS migration error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape FTS5 special characters in user input
|
||||
*/
|
||||
private escapeFTS5(text: string): string {
|
||||
// FTS5 special characters: " * ( ) AND OR NOT
|
||||
// For safety, we'll wrap the entire query in quotes for phrase search
|
||||
// or let advanced users pass boolean operators directly
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WHERE clause for structured filters
|
||||
*/
|
||||
private buildFilterClause(
|
||||
filters: SearchFilters,
|
||||
params: any[],
|
||||
tableAlias: string = 'o'
|
||||
): string {
|
||||
const conditions: string[] = [];
|
||||
|
||||
// Project filter
|
||||
if (filters.project) {
|
||||
conditions.push(`${tableAlias}.project = ?`);
|
||||
params.push(filters.project);
|
||||
}
|
||||
|
||||
// Type filter (for observations only)
|
||||
if (filters.type) {
|
||||
if (Array.isArray(filters.type)) {
|
||||
const placeholders = filters.type.map(() => '?').join(',');
|
||||
conditions.push(`${tableAlias}.type IN (${placeholders})`);
|
||||
params.push(...filters.type);
|
||||
} else {
|
||||
conditions.push(`${tableAlias}.type = ?`);
|
||||
params.push(filters.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if (filters.dateRange) {
|
||||
const { start, end } = filters.dateRange;
|
||||
if (start) {
|
||||
const startEpoch = typeof start === 'number' ? start : new Date(start).getTime();
|
||||
conditions.push(`${tableAlias}.created_at_epoch >= ?`);
|
||||
params.push(startEpoch);
|
||||
}
|
||||
if (end) {
|
||||
const endEpoch = typeof end === 'number' ? end : new Date(end).getTime();
|
||||
conditions.push(`${tableAlias}.created_at_epoch <= ?`);
|
||||
params.push(endEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
// Concepts filter (JSON array search)
|
||||
if (filters.concepts) {
|
||||
const concepts = Array.isArray(filters.concepts) ? filters.concepts : [filters.concepts];
|
||||
const conceptConditions = concepts.map(() => {
|
||||
return `EXISTS (SELECT 1 FROM json_each(${tableAlias}.concepts) WHERE value = ?)`;
|
||||
});
|
||||
if (conceptConditions.length > 0) {
|
||||
conditions.push(`(${conceptConditions.join(' OR ')})`);
|
||||
params.push(...concepts);
|
||||
}
|
||||
}
|
||||
|
||||
// Files filter (JSON array search)
|
||||
if (filters.files) {
|
||||
const files = Array.isArray(filters.files) ? filters.files : [filters.files];
|
||||
const fileConditions = files.map(() => {
|
||||
return `(
|
||||
EXISTS (SELECT 1 FROM json_each(${tableAlias}.files_read) WHERE value LIKE ?)
|
||||
OR EXISTS (SELECT 1 FROM json_each(${tableAlias}.files_modified) WHERE value LIKE ?)
|
||||
)`;
|
||||
});
|
||||
if (fileConditions.length > 0) {
|
||||
conditions.push(`(${fileConditions.join(' OR ')})`);
|
||||
files.forEach(file => {
|
||||
params.push(`%${file}%`, `%${file}%`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return conditions.length > 0 ? conditions.join(' AND ') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ORDER BY clause
|
||||
*/
|
||||
private buildOrderClause(orderBy: SearchOptions['orderBy'] = 'relevance', hasFTS: boolean = true, ftsTable: string = 'observations_fts'): string {
|
||||
switch (orderBy) {
|
||||
case 'relevance':
|
||||
return hasFTS ? `ORDER BY ${ftsTable}.rank ASC` : 'ORDER BY o.created_at_epoch DESC';
|
||||
case 'date_desc':
|
||||
return 'ORDER BY o.created_at_epoch DESC';
|
||||
case 'date_asc':
|
||||
return 'ORDER BY o.created_at_epoch ASC';
|
||||
default:
|
||||
return 'ORDER BY o.created_at_epoch DESC';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search observations using FTS5 full-text search
|
||||
*/
|
||||
searchObservations(query: string, options: SearchOptions = {}): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
|
||||
// Build filter conditions
|
||||
const filterClause = this.buildFilterClause(filters, params, 'o');
|
||||
const whereClause = filterClause ? `AND ${filterClause}` : '';
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = this.buildOrderClause(orderBy, true);
|
||||
|
||||
// Main query with FTS5
|
||||
const sql = `
|
||||
SELECT
|
||||
o.*,
|
||||
observations_fts.rank as rank
|
||||
FROM observations o
|
||||
JOIN observations_fts ON o.id = observations_fts.rowid
|
||||
WHERE observations_fts MATCH ?
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
|
||||
// Normalize rank to score (0-1, higher is better)
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
// Invert rank (lower rank = better match) and normalize to 0-1
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search session summaries using FTS5 full-text search
|
||||
*/
|
||||
searchSessions(query: string, options: SearchOptions = {}): SessionSummarySearchResult[] {
|
||||
const params: any[] = [];
|
||||
const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;
|
||||
|
||||
// Build FTS5 match query
|
||||
const ftsQuery = this.escapeFTS5(query);
|
||||
params.push(ftsQuery);
|
||||
|
||||
// Build filter conditions (without type filter - not applicable to summaries)
|
||||
const filterOptions = { ...filters };
|
||||
delete filterOptions.type;
|
||||
const filterClause = this.buildFilterClause(filterOptions, params, 's');
|
||||
const whereClause = filterClause ? `AND ${filterClause}` : '';
|
||||
|
||||
// Note: session_summaries don't have files_read/files_modified in the same way
|
||||
// We'll need to adjust the filter clause
|
||||
const adjustedWhereClause = whereClause.replace(/files_read/g, 'files_read').replace(/files_modified/g, 'files_edited');
|
||||
|
||||
// Build ORDER BY
|
||||
const orderClause = orderBy === 'relevance'
|
||||
? 'ORDER BY session_summaries_fts.rank ASC'
|
||||
: orderBy === 'date_asc'
|
||||
? 'ORDER BY s.created_at_epoch ASC'
|
||||
: 'ORDER BY s.created_at_epoch DESC';
|
||||
|
||||
// Main query with FTS5
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
session_summaries_fts.rank as rank
|
||||
FROM session_summaries s
|
||||
JOIN session_summaries_fts ON s.id = session_summaries_fts.rowid
|
||||
WHERE session_summaries_fts MATCH ?
|
||||
${adjustedWhereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
params.push(limit, offset);
|
||||
|
||||
const results = this.db.prepare(sql).all(...params) as SessionSummarySearchResult[];
|
||||
|
||||
// Normalize rank to score
|
||||
if (results.length > 0) {
|
||||
const minRank = Math.min(...results.map(r => r.rank || 0));
|
||||
const maxRank = Math.max(...results.map(r => r.rank || 0));
|
||||
const range = maxRank - minRank || 1;
|
||||
|
||||
results.forEach(r => {
|
||||
if (r.rank !== undefined) {
|
||||
r.score = 1 - ((r.rank - minRank) / range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find observations by concept tag
|
||||
*/
|
||||
findByConcept(concept: string, filters: SearchFilters = {}): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
|
||||
// Add concept to filters
|
||||
const conceptFilters = { ...filters, concepts: concept };
|
||||
const filterClause = this.buildFilterClause(conceptFilters, params, 'o');
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
`;
|
||||
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find observations and summaries by file path
|
||||
*/
|
||||
findByFile(filePath: string, filters: SearchFilters = {}): {
|
||||
observations: ObservationSearchResult[];
|
||||
sessions: SessionSummarySearchResult[];
|
||||
} {
|
||||
const params: any[] = [];
|
||||
|
||||
// Add file to filters
|
||||
const fileFilters = { ...filters, files: filePath };
|
||||
const filterClause = this.buildFilterClause(fileFilters, params, 'o');
|
||||
|
||||
const observationsSql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
`;
|
||||
|
||||
const observations = this.db.prepare(observationsSql).all(...params) as ObservationSearchResult[];
|
||||
|
||||
// For session summaries, search files_read and files_edited
|
||||
const sessionParams: any[] = [];
|
||||
const sessionFilters = { ...filters };
|
||||
delete sessionFilters.type; // Remove type filter for sessions
|
||||
|
||||
const baseConditions: string[] = [];
|
||||
if (sessionFilters.project) {
|
||||
baseConditions.push('s.project = ?');
|
||||
sessionParams.push(sessionFilters.project);
|
||||
}
|
||||
|
||||
if (sessionFilters.dateRange) {
|
||||
const { start, end } = sessionFilters.dateRange;
|
||||
if (start) {
|
||||
const startEpoch = typeof start === 'number' ? start : new Date(start).getTime();
|
||||
baseConditions.push('s.created_at_epoch >= ?');
|
||||
sessionParams.push(startEpoch);
|
||||
}
|
||||
if (end) {
|
||||
const endEpoch = typeof end === 'number' ? end : new Date(end).getTime();
|
||||
baseConditions.push('s.created_at_epoch <= ?');
|
||||
sessionParams.push(endEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
// File condition
|
||||
baseConditions.push(`(
|
||||
EXISTS (SELECT 1 FROM json_each(s.files_read) WHERE value LIKE ?)
|
||||
OR EXISTS (SELECT 1 FROM json_each(s.files_edited) WHERE value LIKE ?)
|
||||
)`);
|
||||
sessionParams.push(`%${filePath}%`, `%${filePath}%`);
|
||||
|
||||
const sessionsSql = `
|
||||
SELECT s.*
|
||||
FROM session_summaries s
|
||||
WHERE ${baseConditions.join(' AND ')}
|
||||
ORDER BY s.created_at_epoch DESC
|
||||
`;
|
||||
|
||||
const sessions = this.db.prepare(sessionsSql).all(...sessionParams) as SessionSummarySearchResult[];
|
||||
|
||||
return { observations, sessions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find observations by type
|
||||
*/
|
||||
findByType(
|
||||
type: ObservationRow['type'] | ObservationRow['type'][],
|
||||
filters: SearchFilters = {}
|
||||
): ObservationSearchResult[] {
|
||||
const params: any[] = [];
|
||||
|
||||
// Add type to filters
|
||||
const typeFilters = { ...filters, type };
|
||||
const filterClause = this.buildFilterClause(typeFilters, params, 'o');
|
||||
|
||||
const sql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
`;
|
||||
|
||||
return this.db.prepare(sql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced search combining FTS5 and structured filters
|
||||
*/
|
||||
advancedSearch(options: {
|
||||
textQuery?: string;
|
||||
searchSessions?: boolean;
|
||||
} & SearchOptions): {
|
||||
observations: ObservationSearchResult[];
|
||||
sessions: SessionSummarySearchResult[];
|
||||
} {
|
||||
const { textQuery, searchSessions = true, ...searchOptions } = options;
|
||||
|
||||
let observations: ObservationSearchResult[] = [];
|
||||
let sessions: SessionSummarySearchResult[] = [];
|
||||
|
||||
if (textQuery) {
|
||||
// Use FTS5 search
|
||||
observations = this.searchObservations(textQuery, searchOptions);
|
||||
if (searchSessions) {
|
||||
sessions = this.searchSessions(textQuery, searchOptions);
|
||||
}
|
||||
} else {
|
||||
// Pure structured query (no FTS)
|
||||
const params: any[] = [];
|
||||
const filterClause = this.buildFilterClause(searchOptions, params, 'o');
|
||||
|
||||
if (filterClause) {
|
||||
const obsSql = `
|
||||
SELECT o.*
|
||||
FROM observations o
|
||||
WHERE ${filterClause}
|
||||
${this.buildOrderClause(searchOptions.orderBy, false)}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
params.push(searchOptions.limit || 50, searchOptions.offset || 0);
|
||||
observations = this.db.prepare(obsSql).all(...params) as ObservationSearchResult[];
|
||||
}
|
||||
|
||||
if (searchSessions) {
|
||||
const sessionParams: any[] = [];
|
||||
const sessionFilters = { ...searchOptions };
|
||||
delete sessionFilters.type;
|
||||
const sessionFilterClause = this.buildFilterClause(sessionFilters, sessionParams, 's');
|
||||
|
||||
if (sessionFilterClause) {
|
||||
const sessSql = `
|
||||
SELECT s.*
|
||||
FROM session_summaries s
|
||||
WHERE ${sessionFilterClause}
|
||||
ORDER BY s.created_at_epoch DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
sessionParams.push(searchOptions.limit || 50, searchOptions.offset || 0);
|
||||
sessions = this.db.prepare(sessSql).all(...sessionParams) as SessionSummarySearchResult[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { observations, sessions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,10 @@ import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* Lightweight database interface for hooks
|
||||
* Provides simple, synchronous operations for hook commands
|
||||
* No complex logic - just basic CRUD operations
|
||||
* Session data store for SDK sessions, observations, and summaries
|
||||
* Provides simple, synchronous CRUD operations for session-based memory
|
||||
*/
|
||||
export class HooksDatabase {
|
||||
export class SessionStore {
|
||||
private db: Database;
|
||||
|
||||
constructor() {
|
||||
@@ -38,10 +37,10 @@ export class HooksDatabase {
|
||||
|
||||
if (!hasWorkerPort) {
|
||||
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
|
||||
console.error('[HooksDatabase] Added worker_port column to sdk_sessions table');
|
||||
console.error('[SessionStore] Added worker_port column to sdk_sessions table');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Migration error:', error.message);
|
||||
console.error('[SessionStore] Migration error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ export class HooksDatabase {
|
||||
|
||||
if (!hasPromptCounter) {
|
||||
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');
|
||||
console.error('[SessionStore] Added prompt_counter column to sdk_sessions table');
|
||||
}
|
||||
|
||||
// Check observations for prompt_number
|
||||
@@ -65,7 +64,7 @@ export class HooksDatabase {
|
||||
|
||||
if (!obsHasPromptNumber) {
|
||||
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
|
||||
console.error('[HooksDatabase] Added prompt_number column to observations table');
|
||||
console.error('[SessionStore] Added prompt_number column to observations table');
|
||||
}
|
||||
|
||||
// Check session_summaries for prompt_number
|
||||
@@ -74,7 +73,7 @@ export class HooksDatabase {
|
||||
|
||||
if (!sumHasPromptNumber) {
|
||||
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
|
||||
console.error('[HooksDatabase] Added prompt_number column to session_summaries table');
|
||||
console.error('[SessionStore] Added prompt_number column to session_summaries table');
|
||||
}
|
||||
|
||||
// Remove UNIQUE constraint on session_summaries.sdk_session_id
|
||||
@@ -83,7 +82,7 @@ export class HooksDatabase {
|
||||
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Prompt tracking migration error:', error.message);
|
||||
console.error('[SessionStore] Prompt tracking migration error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +100,7 @@ export class HooksDatabase {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id...');
|
||||
console.error('[SessionStore] Removing UNIQUE constraint from session_summaries.sdk_session_id...');
|
||||
|
||||
// Begin transaction
|
||||
this.db.exec('BEGIN TRANSACTION');
|
||||
@@ -153,14 +152,14 @@ export class HooksDatabase {
|
||||
// Commit transaction
|
||||
this.db.exec('COMMIT');
|
||||
|
||||
console.error('[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
|
||||
console.error('[SessionStore] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
this.db.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Migration error (remove UNIQUE constraint):', error.message);
|
||||
console.error('[SessionStore] Migration error (remove UNIQUE constraint):', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +177,7 @@ export class HooksDatabase {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[HooksDatabase] Adding hierarchical fields to observations table...');
|
||||
console.error('[SessionStore] Adding hierarchical fields to observations table...');
|
||||
|
||||
// Add new columns
|
||||
this.db.exec(`
|
||||
@@ -191,9 +190,9 @@ export class HooksDatabase {
|
||||
ALTER TABLE observations ADD COLUMN files_modified TEXT;
|
||||
`);
|
||||
|
||||
console.error('[HooksDatabase] Successfully added hierarchical fields to observations table');
|
||||
console.error('[SessionStore] Successfully added hierarchical fields to observations table');
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Migration error (add hierarchical fields):', error.message);
|
||||
console.error('[SessionStore] Migration error (add hierarchical fields):', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +211,7 @@ export class HooksDatabase {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[HooksDatabase] Making observations.text nullable...');
|
||||
console.error('[SessionStore] Making observations.text nullable...');
|
||||
|
||||
// Begin transaction
|
||||
this.db.exec('BEGIN TRANSACTION');
|
||||
@@ -266,14 +265,14 @@ export class HooksDatabase {
|
||||
// Commit transaction
|
||||
this.db.exec('COMMIT');
|
||||
|
||||
console.error('[HooksDatabase] Successfully made observations.text nullable');
|
||||
console.error('[SessionStore] 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);
|
||||
console.error('[SessionStore] Migration error (make text nullable):', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Export main components
|
||||
export { DatabaseManager, getDatabase, initializeDatabase } from './Database.js';
|
||||
|
||||
// Export hooks database
|
||||
export { HooksDatabase } from './HooksDatabase.js';
|
||||
// Export session store (CRUD operations for sessions, observations, summaries)
|
||||
export { SessionStore } from './SessionStore.js';
|
||||
|
||||
// Export session search (FTS5 and structured search)
|
||||
export { SessionSearch } from './SessionSearch.js';
|
||||
|
||||
// Export types
|
||||
export * from './types.js';
|
||||
|
||||
@@ -362,6 +362,115 @@ export const migration005: Migration = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 006 - Add FTS5 full-text search tables
|
||||
* Creates virtual tables for fast text search on observations and session_summaries
|
||||
*/
|
||||
export const migration006: Migration = {
|
||||
version: 6,
|
||||
up: (db: Database) => {
|
||||
// FTS5 virtual table for observations
|
||||
// Note: This assumes the hierarchical fields (title, subtitle, etc.) already exist
|
||||
// from the inline migrations in SessionStore constructor
|
||||
db.run(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
||||
title,
|
||||
subtitle,
|
||||
narrative,
|
||||
text,
|
||||
facts,
|
||||
concepts,
|
||||
content='observations',
|
||||
content_rowid='id'
|
||||
);
|
||||
`);
|
||||
|
||||
// Populate FTS table with existing data
|
||||
db.run(`
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
SELECT id, title, subtitle, narrative, text, facts, concepts
|
||||
FROM observations;
|
||||
`);
|
||||
|
||||
// Triggers to keep observations_fts in sync
|
||||
db.run(`
|
||||
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
|
||||
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
|
||||
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
|
||||
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
|
||||
END;
|
||||
`);
|
||||
|
||||
// FTS5 virtual table for session_summaries
|
||||
db.run(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS session_summaries_fts USING fts5(
|
||||
request,
|
||||
investigated,
|
||||
learned,
|
||||
completed,
|
||||
next_steps,
|
||||
notes,
|
||||
content='session_summaries',
|
||||
content_rowid='id'
|
||||
);
|
||||
`);
|
||||
|
||||
// Populate FTS table with existing data
|
||||
db.run(`
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
SELECT id, request, investigated, learned, completed, next_steps, notes
|
||||
FROM session_summaries;
|
||||
`);
|
||||
|
||||
// Triggers to keep session_summaries_fts in sync
|
||||
db.run(`
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_ad AFTER DELETE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS session_summaries_au AFTER UPDATE ON session_summaries BEGIN
|
||||
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
|
||||
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
|
||||
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
|
||||
END;
|
||||
`);
|
||||
|
||||
console.log('✅ Created FTS5 virtual tables and triggers for full-text search');
|
||||
},
|
||||
|
||||
down: (db: Database) => {
|
||||
db.run(`
|
||||
DROP TRIGGER IF EXISTS observations_au;
|
||||
DROP TRIGGER IF EXISTS observations_ad;
|
||||
DROP TRIGGER IF EXISTS observations_ai;
|
||||
DROP TABLE IF EXISTS observations_fts;
|
||||
|
||||
DROP TRIGGER IF EXISTS session_summaries_au;
|
||||
DROP TRIGGER IF EXISTS session_summaries_ad;
|
||||
DROP TRIGGER IF EXISTS session_summaries_ai;
|
||||
DROP TABLE IF EXISTS session_summaries_fts;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* All migrations in order
|
||||
*/
|
||||
@@ -370,5 +479,6 @@ export const migrations: Migration[] = [
|
||||
migration002,
|
||||
migration003,
|
||||
migration004,
|
||||
migration005
|
||||
migration005,
|
||||
migration006
|
||||
];
|
||||
@@ -182,3 +182,88 @@ export function normalizeTimestamp(timestamp: string | Date | number | undefined
|
||||
epoch: date.getTime()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK Hooks Database Types
|
||||
*/
|
||||
export interface SDKSessionRow {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string | null;
|
||||
started_at: string;
|
||||
started_at_epoch: number;
|
||||
completed_at: string | null;
|
||||
completed_at_epoch: number | null;
|
||||
status: 'active' | 'completed' | 'failed';
|
||||
worker_port?: number;
|
||||
prompt_counter?: number;
|
||||
}
|
||||
|
||||
export interface ObservationRow {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
|
||||
title: string | null;
|
||||
subtitle: string | null;
|
||||
facts: string | null; // JSON array
|
||||
narrative: string | null;
|
||||
concepts: string | null; // JSON array
|
||||
files_read: string | null; // JSON array
|
||||
files_modified: string | null; // JSON array
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
export interface SessionSummaryRow {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
files_read: string | null; // JSON array
|
||||
files_edited: string | null; // JSON array
|
||||
notes: string | null;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and Filter Types
|
||||
*/
|
||||
export interface DateRange {
|
||||
start?: string | number; // ISO string or epoch
|
||||
end?: string | number; // ISO string or epoch
|
||||
}
|
||||
|
||||
export interface SearchFilters {
|
||||
project?: string;
|
||||
type?: ObservationRow['type'] | ObservationRow['type'][];
|
||||
concepts?: string | string[];
|
||||
files?: string | string[];
|
||||
dateRange?: DateRange;
|
||||
}
|
||||
|
||||
export interface SearchOptions extends SearchFilters {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: 'relevance' | 'date_desc' | 'date_asc';
|
||||
}
|
||||
|
||||
export interface ObservationSearchResult extends ObservationRow {
|
||||
rank?: number; // FTS5 relevance score (lower is better)
|
||||
score?: number; // Normalized score (higher is better, 0-1)
|
||||
}
|
||||
|
||||
export interface SessionSummarySearchResult extends SessionSummaryRow {
|
||||
rank?: number; // FTS5 relevance score (lower is better)
|
||||
score?: number; // Normalized score (higher is better, 0-1)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { SDKUserMessage, SDKSystemMessage } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { HooksDatabase } from './sqlite/HooksDatabase.js';
|
||||
import { SessionStore } from './sqlite/SessionStore.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildFinalizePrompt } from '../sdk/prompts.js';
|
||||
import { parseObservations, parseSummary } from '../sdk/parser.js';
|
||||
import type { SDKSession } from '../sdk/prompts.js';
|
||||
@@ -77,7 +77,7 @@ class WorkerService {
|
||||
this.port = port;
|
||||
|
||||
// Clean up orphaned sessions from previous worker instances
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const cleanedCount = db.cleanupOrphanedSessions();
|
||||
db.close();
|
||||
|
||||
@@ -148,14 +148,14 @@ class WorkerService {
|
||||
this.sessions.set(sessionDbId, session);
|
||||
|
||||
// Update port in database
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
db.setWorkerPort(sessionDbId, this.port!);
|
||||
db.close();
|
||||
|
||||
// Start SDK agent in background
|
||||
session.generatorPromise = this.runSDKAgent(session).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
this.sessions.delete(sessionDbId);
|
||||
@@ -278,7 +278,7 @@ class WorkerService {
|
||||
}
|
||||
|
||||
// Mark as failed since we're aborting
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
|
||||
@@ -313,7 +313,7 @@ class WorkerService {
|
||||
const systemMsg = message as SDKSystemMessage;
|
||||
if (systemMsg.session_id) {
|
||||
// Update in database first, check if it succeeded
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const updated = db.updateSDKSessionId(session.sessionDbId, systemMsg.session_id);
|
||||
db.close();
|
||||
|
||||
@@ -354,7 +354,7 @@ class WorkerService {
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`
|
||||
});
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
db.markSessionCompleted(session.sessionDbId);
|
||||
db.close();
|
||||
|
||||
@@ -411,7 +411,7 @@ class WorkerService {
|
||||
if (message.type === 'summarize') {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
const dbSession = db.getSessionById(session.sessionDbId) as SDKSession | undefined;
|
||||
db.close();
|
||||
|
||||
@@ -487,7 +487,7 @@ class WorkerService {
|
||||
});
|
||||
}
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const db = new SessionStore();
|
||||
for (const obs of observations) {
|
||||
if (session.sdkSessionId) {
|
||||
db.storeObservation(session.sdkSessionId, session.project, obs, promptNumber);
|
||||
|
||||
Reference in New Issue
Block a user