diff --git a/plugin/scripts/context-generator.cjs b/plugin/scripts/context-generator.cjs index 35822784..7c4993f8 100644 --- a/plugin/scripts/context-generator.cjs +++ b/plugin/scripts/context-generator.cjs @@ -218,7 +218,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje FROM sdk_sessions WHERE project IS NOT NULL AND project != '' ORDER BY project ASC - `).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(` + `).all().map(t=>t.project)}getLatestUserPrompt(e){return this.db.prepare(` + SELECT + up.*, + s.sdk_session_id, + s.project + FROM user_prompts up + JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id + WHERE up.claude_session_id = ? + ORDER BY up.created_at_epoch DESC + LIMIT 1 + `).get(e)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(` SELECT * FROM ( SELECT s.sdk_session_id, diff --git a/plugin/scripts/new-hook.js b/plugin/scripts/new-hook.js index 868add94..2bbab5ca 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -1,7 +1,7 @@ #!/usr/bin/env node import ie from"path";import{stdin as X}from"process";import Y from"better-sqlite3";import{join as l,dirname as B,basename as ce}from"path";import{homedir as C}from"os";import{existsSync as le,mkdirSync as $}from"fs";import{fileURLToPath as j}from"url";function W(){return typeof __dirname<"u"?__dirname:B(j(import.meta.url))}var G=W(),E=process.env.CLAUDE_MEM_DATA_DIR||l(C(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||l(C(),".claude"),Te=l(E,"archives"),ge=l(E,"logs"),be=l(E,"trash"),Se=l(E,"backups"),Re=l(E,"settings.json"),D=l(E,"claude-mem.db"),he=l(E,"vector-db"),fe=l(f,"settings.json"),Ne=l(f,"commands"),Oe=l(f,"CLAUDE.md");function k(a){$(a,{recursive:!0})}function N(){return l(G,"..","..")}var O=(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))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e0&&(m=` {${Object.entries(d).map(([P,H])=>`${P}=${H}`).join(", ")}}`)}let T=`[${i}] [${o}] [${p}] ${c}${t}${m}${_}`;e===3?console.error(T):console.log(T)}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`})}},x=new I;var R=class{db;constructor(){k(E),this.db=new Y(D),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` +`+JSON.stringify(n,null,2):_=" "+this.formatData(n));let u="";if(r){let{sessionId:g,sdkSessionId:S,correlationId:m,...d}=r;Object.keys(d).length>0&&(u=` {${Object.entries(d).map(([P,H])=>`${P}=${H}`).join(", ")}}`)}let T=`[${i}] [${o}] [${p}] ${c}${t}${u}${_}`;e===3?console.error(T):console.log(T)}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`})}},x=new I;var R=class{db;constructor(){k(E),this.db=new Y(D),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -219,7 +219,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje FROM sdk_sessions WHERE project IS NOT NULL AND project != '' ORDER BY project ASC - `).all().map(t=>t.project)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(` + `).all().map(t=>t.project)}getLatestUserPrompt(e){return this.db.prepare(` + SELECT + up.*, + s.sdk_session_id, + s.project + FROM user_prompts up + JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id + WHERE up.claude_session_id = ? + ORDER BY up.created_at_epoch DESC + LIMIT 1 + `).get(e)}getRecentSessionsWithStatus(e,s=3){return this.db.prepare(` SELECT * FROM ( SELECT s.sdk_session_id, @@ -328,23 +338,23 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let m=this.db.prepare(` + `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` INSERT INTO observations (sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,i.toISOString(),o);return{id:Number(m.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(` + `).run(e,s,t.type,t.title,t.subtitle,JSON.stringify(t.facts),t.narrative,JSON.stringify(t.concepts),JSON.stringify(t.files_read),JSON.stringify(t.files_modified),r||null,n,i.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}storeSummary(e,s,t,r,n=0){let i=new Date,o=i.getTime();this.db.prepare(` SELECT id FROM sdk_sessions WHERE sdk_session_id = ? `).get(e)||(this.db.prepare(` INSERT INTO sdk_sessions (claude_session_id, sdk_session_id, project, started_at, started_at_epoch, status) VALUES (?, ?, ?, ?, ?, 'active') - `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let m=this.db.prepare(` + `).run(e,e,s,i.toISOString(),o),console.error(`[SessionStore] Auto-created session record for session_id: ${e}`));let u=this.db.prepare(` INSERT INTO session_summaries (sdk_session_id, project, request, investigated, learned, completed, next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,i.toISOString(),o);return{id:Number(m.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` + `).run(e,s,t.request,t.investigated,t.learned,t.completed,t.next_steps,t.notes,r||null,n,i.toISOString(),o);return{id:Number(u.lastInsertRowid),createdAtEpoch:o}}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(` UPDATE sdk_sessions SET status = 'completed', completed_at = ?, completed_at_epoch = ? WHERE id = ? @@ -379,7 +389,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE id >= ? ${i} ORDER BY id ASC LIMIT ? - `;try{let u=this.db.prepare(g).all(e,...o,t+1),d=this.db.prepare(S).all(e,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let g=` + `;try{let m=this.db.prepare(g).all(e,...o,t+1),d=this.db.prepare(S).all(e,...o,r+1);if(m.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=m.length>0?m[m.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(m){return console.error("[SessionStore] Error getting boundary observations:",m.message),{observations:[],sessions:[],prompts:[]}}}else{let g=` SELECT created_at_epoch FROM observations WHERE created_at_epoch <= ? ${i} @@ -391,12 +401,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE created_at_epoch >= ? ${i} ORDER BY created_at_epoch ASC LIMIT ? - `;try{let u=this.db.prepare(g).all(s,...o,t),d=this.db.prepare(S).all(s,...o,r+1);if(u.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let _=` + `;try{let m=this.db.prepare(g).all(s,...o,t),d=this.db.prepare(S).all(s,...o,r+1);if(m.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=m.length>0?m[m.length-1].created_at_epoch:s,c=d.length>0?d[d.length-1].created_at_epoch:s}catch(m){return console.error("[SessionStore] Error getting boundary timestamps:",m.message),{observations:[],sessions:[],prompts:[]}}}let _=` SELECT * FROM observations WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i} ORDER BY created_at_epoch ASC - `,m=` + `,u=` SELECT * FROM session_summaries WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i} @@ -407,7 +417,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")} ORDER BY up.created_at_epoch ASC - `;try{let g=this.db.prepare(_).all(p,c,...o),S=this.db.prepare(m).all(p,c,...o),u=this.db.prepare(T).all(p,c,...o);return{observations:g,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:u.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function L(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;isetTimeout(o,z)),await U())return!0;return!1}catch{return!1}}async function w(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}. + `;try{let g=this.db.prepare(_).all(p,c,...o),S=this.db.prepare(u).all(p,c,...o),m=this.db.prepare(T).all(p,c,...o);return{observations:g,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:m.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(g){return console.error("[SessionStore] Error querying timeline records:",g.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function L(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import A from"path";import{homedir as V}from"os";import{existsSync as y,readFileSync as q}from"fs";import{spawnSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=A.join(V(),".claude-mem","settings.json");if(y(a)){let e=JSON.parse(q(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=N(),e=A.join(a,"ecosystem.config.cjs");if(!y(e))throw new Error(`Ecosystem config not found at ${e}`);let s=A.join(a,"node_modules",".bin","pm2"),t=process.platform==="win32"?s+".cmd":s,r=y(t)?t:"pm2",n=J(r,["start",e],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let i=0;isetTimeout(o,z)),await U())return!0;return!1}catch{return!1}}async function w(){if(await U())return;if(!await ee()){let e=h(),s=N();throw new Error(`Worker service failed to start on port ${e}. To start manually, run: cd ${s} @@ -415,4 +425,4 @@ To start manually, run: If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as se}from"fs";import{homedir as te}from"os";import{join as re}from"path";var ne=re(te(),".claude-mem","silent.log");function b(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",c=`[${t}] [${p}] ${a}`;if(e!==void 0)try{c+=` ${JSON.stringify(e)}`}catch(_){c+=` [stringify error: ${_}]`}c+=` -`;try{se(ne,c)}catch(_){console.error("[silent-debug] Failed to write to log:",_)}return s}var M=100;function oe(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function F(a){if(typeof a!="string")return b("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=oe(a);return e>M&&b("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:M,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function ae(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;b("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=ie.basename(s);b("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s}),await w();let n=new R,i=n.createSDKSession(e,r,t),o=n.incrementPromptCounter(i),p=F(t);if(!p||p.trim()===""){b("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:o,originalLength:t.length}),n.close(),console.error(`[new-hook] Session ${i}, prompt #${o} (fully private - skipped)`),console.log(L("UserPromptSubmit",!0));return}n.saveUserPrompt(e,o,p),console.error(`[new-hook] Session ${i}, prompt #${o}`),n.close();let c=h(),_=t.startsWith("/")?t.substring(1):t;try{let m=await fetch(`http://127.0.0.1:${c}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:_,promptNumber:o}),signal:AbortSignal.timeout(5e3)});if(!m.ok){let T=await m.text();throw new Error(`Failed to initialize session: ${m.status} ${T}`)}}catch(m){throw m.cause?.code==="ECONNREFUSED"||m.name==="TimeoutError"||m.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):m}console.log(L("UserPromptSubmit",!0))}var v="";X.on("data",a=>v+=a);X.on("end",async()=>{let a=v?JSON.parse(v):void 0;await ae(a)}); +`;try{se(ne,c)}catch(_){console.error("[silent-debug] Failed to write to log:",_)}return s}var M=100;function oe(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function F(a){if(typeof a!="string")return b("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=oe(a);return e>M&&b("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:M,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function ae(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;b("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=ie.basename(s);b("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s}),await w();let n=new R,i=n.createSDKSession(e,r,t),o=n.incrementPromptCounter(i),p=F(t);if(!p||p.trim()===""){b("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:o,originalLength:t.length}),n.close(),console.error(`[new-hook] Session ${i}, prompt #${o} (fully private - skipped)`),console.log(L("UserPromptSubmit",!0));return}n.saveUserPrompt(e,o,p),console.error(`[new-hook] Session ${i}, prompt #${o}`),n.close();let c=h(),_=t.startsWith("/")?t.substring(1):t;try{let u=await fetch(`http://127.0.0.1:${c}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:_,promptNumber:o}),signal:AbortSignal.timeout(5e3)});if(!u.ok){let T=await u.text();throw new Error(`Failed to initialize session: ${u.status} ${T}`)}}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}console.log(L("UserPromptSubmit",!0))}var v="";X.on("data",a=>v+=a);X.on("end",async()=>{let a=v?JSON.parse(v):void 0;await ae(a)}); diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 16182986..f1f3a882 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -269,7 +269,17 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let r=Obje FROM sdk_sessions WHERE project IS NOT NULL AND project != '' ORDER BY project ASC - `).all().map(t=>t.project)}getRecentSessionsWithStatus(e,r=3){return this.db.prepare(` + `).all().map(t=>t.project)}getLatestUserPrompt(e){return this.db.prepare(` + SELECT + up.*, + s.sdk_session_id, + s.project + FROM user_prompts up + JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id + WHERE up.claude_session_id = ? + ORDER BY up.created_at_epoch DESC + LIMIT 1 + `).get(e)}getRecentSessionsWithStatus(e,r=3){return this.db.prepare(` SELECT * FROM ( SELECT s.sdk_session_id, @@ -1021,17 +1031,7 @@ Other tips: `)}formatSessionResult(e){let r=e.request||`Session ${e.sdk_session_id?.substring(0,8)||"unknown"}`,t=[];t.push(`## ${r}`),t.push(`*Source: claude-mem://session/${e.sdk_session_id}*`),t.push(""),e.completed&&(t.push(`**Completed:** ${e.completed}`),t.push("")),e.learned&&(t.push(`**Learned:** ${e.learned}`),t.push("")),e.investigated&&(t.push(`**Investigated:** ${e.investigated}`),t.push("")),e.next_steps&&(t.push(`**Next Steps:** ${e.next_steps}`),t.push("")),e.notes&&(t.push(`**Notes:** ${e.notes}`),t.push(""));let s=[];if(e.files_read||e.files_edited){let n=[];if(e.files_read)try{n.push(...JSON.parse(e.files_read))}catch{}if(e.files_edited)try{n.push(...JSON.parse(e.files_edited))}catch{}n.length>0&&s.push(`Files: ${[...new Set(n)].join(", ")}`)}let i=new Date(e.created_at_epoch).toLocaleDateString();return s.push(`Date: ${i}`),s.length>0&&(t.push("---"),t.push(s.join(" | "))),t.join(` `)}formatUserPromptResult(e){let r=[];r.push(`## User Prompt #${e.prompt_number}`),r.push(`*Source: claude-mem://user-prompt/${e.id}*`),r.push(""),r.push(e.prompt_text),r.push(""),r.push("---");let t=new Date(e.created_at_epoch).toLocaleString();return r.push(`Date: ${t}`),r.join(` `)}};var El=class{buildTimeline(e){let r=[...e.observations.map(t=>({type:"observation",data:t,epoch:t.created_at_epoch})),...e.sessions.map(t=>({type:"session",data:t,epoch:t.created_at_epoch})),...e.prompts.map(t=>({type:"prompt",data:t,epoch:t.created_at_epoch}))];return r.sort((t,s)=>t.epoch-s.epoch),r}filterByDepth(e,r,t,s,i){if(e.length===0)return e;let n=-1;if(typeof r=="number")n=e.findIndex(c=>c.type==="observation"&&c.data.id===r);else if(typeof r=="string"&&r.startsWith("S")){let c=parseInt(r.slice(1),10);n=e.findIndex(u=>u.type==="session"&&u.data.id===c)}else n=e.findIndex(c=>c.epoch>=t),n===-1&&(n=e.length-1);if(n===-1)return e;let o=Math.max(0,n-s),l=Math.min(e.length,n+i+1);return e.slice(o,l)}formatTimeline(e,r,t,s,i){if(e.length===0)return t?`Found observation matching "${t}", but no timeline context available.`:"No timeline items found";let n=[];if(t&&r){let c=e.find(p=>p.type==="observation"&&p.data.id===r),u=c?c.data.title||"Untitled":"Unknown";n.push(`# Timeline for query: "${t}"`),n.push(`**Anchor:** Observation #${r} - ${u}`)}else r?n.push(`# Timeline around anchor: ${r}`):n.push("# Timeline");s!==void 0&&i!==void 0?n.push(`**Window:** ${s} records before \u2192 ${i} records after | **Items:** ${e.length}`):n.push(`**Items:** ${e.length}`),n.push(""),n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u{1F9E0} decision"),n.push("");let o=new Map;for(let c of e){let u=this.formatDate(c.epoch);o.has(u)||o.set(u,[]),o.get(u).push(c)}let l=Array.from(o.entries()).sort((c,u)=>{let p=new Date(c[0]).getTime(),f=new Date(u[0]).getTime();return p-f});for(let[c,u]of l){n.push(`### ${c}`),n.push("");let p=null,f="",d=!1;for(let v of u){let h=typeof r=="number"&&v.type==="observation"&&v.data.id===r||typeof r=="string"&&r.startsWith("S")&&v.type==="session"&&`S${v.data.id}`===r;if(v.type==="session"){d&&(n.push(""),d=!1,p=null,f="");let m=v.data,y=m.request||"Session summary",g=`claude-mem://session-summary/${m.id}`,b=h?" \u2190 **ANCHOR**":"";n.push(`**\u{1F3AF} #S${m.id}** ${y} (${this.formatDateTime(v.epoch)}) [\u2192](${g})${b}`),n.push("")}else if(v.type==="prompt"){d&&(n.push(""),d=!1,p=null,f="");let m=v.data,y=m.prompt_text.length>100?m.prompt_text.substring(0,100)+"...":m.prompt_text;n.push(`**\u{1F4AC} User Prompt #${m.prompt_number}** (${this.formatDateTime(v.epoch)})`),n.push(`> ${y}`),n.push("")}else if(v.type==="observation"){let m=v.data,y="General";y!==p&&(d&&n.push(""),n.push(`**${y}**`),n.push("| ID | Time | T | Title | Tokens |"),n.push("|----|------|---|-------|--------|"),p=y,d=!0,f="");let g=this.getTypeIcon(m.type),b=this.formatTime(v.epoch),w=m.title||"Untitled",P=this.estimateTokens(m.narrative),k=b!==f?b:"\u2033";f=b;let D=h?" \u2190 **ANCHOR**":"";n.push(`| #${m.id} | ${k} | ${g} | ${w}${D} | ~${P} |`)}}d&&n.push("")}return n.join(` -`)}getTypeIcon(e){switch(e){case"bugfix":return"\u{1F534}";case"feature":return"\u{1F7E3}";case"refactor":return"\u{1F504}";case"change":return"\u2705";case"discovery":return"\u{1F535}";case"decision":return"\u{1F9E0}";default:return"\u2022"}}formatDate(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}formatTime(e){return new Date(e).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}formatDateTime(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}estimateTokens(e){return e?Math.ceil(e.length/4):0}};var mf=vt(zp(),1),Sw=vt(Ew(),1),ww=vt(require("path"),1);Ia();_t();function Tw(a){let e=[];e.push(mf.default.json({limit:"50mb"})),e.push((0,Sw.default)()),e.push((s,i,n)=>{if(s.path.startsWith("/health")||s.path==="/"||s.path.includes("."))return n();let o=Date.now(),l=`${s.method}-${Date.now()}`,c=a(s.method,s.path,s.body);H.info("HTTP",`\u2192 ${s.method} ${s.path}`,{requestId:l},c);let u=i.send.bind(i);i.send=function(p){let f=Date.now()-o;return H.info("HTTP",`\u2190 ${i.statusCode} ${s.path}`,{requestId:l,duration:`${f}ms`}),u(p)},n()});let r=zr(),t=ww.default.join(r,"plugin","ui");return e.push(mf.default.static(t)),e}function Rw(a,e,r){if(!r||Object.keys(r).length===0||e.includes("/init"))return"";if(e.includes("/observations")){let t=r.tool_name||"?",s=r.tool_input;return`tool=${H.formatTool(t,s)}`}return e.includes("/summarize")?"requesting summary":""}var Pw=vt(require("path"),1),Ow=require("fs");Ia();_t();var Sl=class{constructor(e,r,t){this.sseBroadcaster=e;this.dbManager=r;this.sessionManager=t}setupRoutes(e){e.get("/health",this.handleHealth.bind(this)),e.get("/",this.handleViewerUI.bind(this)),e.get("/stream",this.handleSSEStream.bind(this))}handleHealth(e,r){r.json({status:"ok",timestamp:Date.now()})}handleViewerUI(e,r){try{let t=zr(),s=Pw.default.join(t,"plugin","ui","viewer.html"),i=(0,Ow.readFileSync)(s,"utf-8");r.setHeader("Content-Type","text/html"),r.send(i)}catch(t){H.failure("WORKER","Viewer UI error",{},t),r.status(500).json({error:"Failed to load viewer UI"})}}handleSSEStream(e,r){r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),this.sseBroadcaster.addClient(r);let t=this.dbManager.getSessionStore().getAllProjects();this.sseBroadcaster.broadcast({type:"initial_load",projects:t,timestamp:Date.now()});let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalActiveWork();this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:s,queueDepth:i})}};_t();var Cw=100;function E6(a){let e=(a.match(//g)||[]).length,r=(a.match(//g)||[]).length;return e+r}function hf(a){if(typeof a!="string")return ie("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=E6(a);return e>Cw&&ie("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:Cw,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}var wl=class{constructor(e,r,t,s,i){this.sessionManager=e;this.dbManager=r;this.sdkAgent=t;this.sseBroadcaster=s;this.workerService=i}ensureGeneratorRunning(e,r){let t=this.sessionManager.getSession(e);t&&!t.generatorPromise&&(H.info("SESSION",`Generator auto-starting (${r})`,{sessionId:e,queueDepth:t.pendingMessages.length}),t.generatorPromise=this.sdkAgent.startSession(t,this.workerService).catch(s=>{H.failure("SDK","SDK agent error",{sessionId:e},s)}).finally(()=>{H.info("SESSION","Generator finished",{sessionId:e}),t.generatorPromise=null,this.workerService.broadcastProcessingStatus()}))}setupRoutes(e){e.post("/sessions/:sessionDbId/init",this.handleSessionInit.bind(this)),e.post("/sessions/:sessionDbId/observations",this.handleObservations.bind(this)),e.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),e.get("/sessions/:sessionDbId/status",this.handleSessionStatus.bind(this)),e.delete("/sessions/:sessionDbId",this.handleSessionDelete.bind(this)),e.post("/sessions/:sessionDbId/complete",this.handleSessionComplete.bind(this)),e.post("/api/sessions/observations",this.handleObservationsByClaudeId.bind(this)),e.post("/api/sessions/summarize",this.handleSummarizeByClaudeId.bind(this)),e.post("/api/sessions/complete",this.handleSessionCompleteByClaudeId.bind(this))}handleSessionInit(e,r){try{let t=parseInt(e.params.sessionDbId,10),{userPrompt:s,promptNumber:i}=e.body,n=this.sessionManager.initializeSession(t,s,i),l=this.dbManager.getSessionStore().db.prepare(` - SELECT - up.*, - s.sdk_session_id, - s.project - FROM user_prompts up - JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id - WHERE up.claude_session_id = ? - ORDER BY up.created_at_epoch DESC - LIMIT 1 - `).get(n.claudeSessionId);if(l){this.sseBroadcaster.broadcast({type:"new_prompt",prompt:{id:l.id,claude_session_id:l.claude_session_id,project:l.project,prompt_number:l.prompt_number,prompt_text:l.prompt_text,created_at_epoch:l.created_at_epoch}}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:!0});let c=Date.now(),u=l.prompt_text;this.dbManager.getChromaSync().syncUserPrompt(l.id,l.sdk_session_id,l.project,u,l.prompt_number,l.created_at_epoch).then(()=>{let p=Date.now()-c,f=u.length>60?u.substring(0,60)+"...":u;H.debug("CHROMA","User prompt synced",{promptId:l.id,duration:`${p}ms`,prompt:f})}).catch(p=>{H.error("CHROMA","Failed to sync user_prompt",{promptId:l.id,sessionId:t},p)})}this.workerService.broadcastProcessingStatus(),H.info("SESSION","Generator starting",{sessionId:t,project:n.project,promptNum:n.lastPromptNumber}),n.generatorPromise=this.sdkAgent.startSession(n,this.workerService).catch(c=>{H.failure("SDK","SDK agent error",{sessionId:t},c)}).finally(()=>{H.info("SESSION","Generator finished",{sessionId:t}),n.generatorPromise=null,this.workerService.broadcastProcessingStatus()}),this.sseBroadcaster.broadcast({type:"session_started",sessionDbId:t,project:n.project}),r.json({status:"initialized",sessionDbId:t,port:On()})}catch(t){H.failure("WORKER","Session init failed",{},t),r.status(500).json({error:t.message})}}handleObservations(e,r){try{let t=parseInt(e.params.sessionDbId,10),{tool_name:s,tool_input:i,tool_response:n,prompt_number:o,cwd:l}=e.body;this.sessionManager.queueObservation(t,{tool_name:s,tool_input:i,tool_response:n,prompt_number:o,cwd:l}),this.ensureGeneratorRunning(t,"observation"),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:t}),r.json({status:"queued"})}catch(t){H.failure("WORKER","Observation queuing failed",{},t),r.status(500).json({error:t.message})}}handleSummarize(e,r){try{let t=parseInt(e.params.sessionDbId,10),{last_user_message:s,last_assistant_message:i}=e.body;this.sessionManager.queueSummarize(t,s,i),this.ensureGeneratorRunning(t,"summarize"),this.workerService.broadcastProcessingStatus(),r.json({status:"queued"})}catch(t){H.failure("WORKER","Summarize queuing failed",{},t),r.status(500).json({error:t.message})}}handleSessionStatus(e,r){try{let t=parseInt(e.params.sessionDbId,10),s=this.sessionManager.getSession(t);if(!s){r.json({status:"not_found"});return}r.json({status:"active",sessionDbId:t,project:s.project,queueLength:s.pendingMessages.length,uptime:Date.now()-s.startTime})}catch(t){H.failure("WORKER","Session status failed",{},t),r.status(500).json({error:t.message})}}async handleSessionDelete(e,r){try{let t=parseInt(e.params.sessionDbId,10);await this.sessionManager.deleteSession(t),this.dbManager.markSessionComplete(t),this.sseBroadcaster.broadcast({type:"session_completed",sessionDbId:t}),r.json({status:"deleted"})}catch(t){H.failure("WORKER","Session delete failed",{},t),r.status(500).json({error:t.message})}}async handleSessionComplete(e,r){try{let t=parseInt(e.params.sessionDbId,10);if(isNaN(t)){r.status(400).json({success:!1,error:"Invalid session ID"});return}await this.sessionManager.deleteSession(t),this.dbManager.markSessionComplete(t),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:t}),r.json({success:!0})}catch(t){H.failure("WORKER","Session complete failed",{},t),r.status(500).json({success:!1,error:String(t)})}}handleObservationsByClaudeId(e,r){try{let{claudeSessionId:t,tool_name:s,tool_input:i,tool_response:n,cwd:o}=e.body;if(!t){r.status(400).json({error:"Missing claudeSessionId"});return}let l=this.dbManager.getSessionStore(),c=l.createSDKSession(t,"",""),u=l.getPromptCounter(c),p=l.getUserPrompt(t,u);if(!p||p.trim()===""){H.debug("HOOK","Skipping observation - user prompt was entirely private",{sessionId:c,promptNumber:u,tool_name:s}),r.json({status:"skipped",reason:"private"});return}let f="{}",d="{}";try{f=i!==void 0?hf(JSON.stringify(i)):"{}"}catch{f='{"error": "Failed to serialize tool_input"}'}try{d=n!==void 0?hf(JSON.stringify(n)):"{}"}catch{d='{"error": "Failed to serialize tool_response"}'}this.sessionManager.queueObservation(c,{tool_name:s,tool_input:f,tool_response:d,prompt_number:u,cwd:o||""}),this.ensureGeneratorRunning(c,"observation"),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:c}),r.json({status:"queued"})}catch(t){H.failure("WORKER","Observation by claudeId failed",{},t),r.status(500).json({error:t.message})}}handleSummarizeByClaudeId(e,r){try{let{claudeSessionId:t,last_user_message:s,last_assistant_message:i}=e.body;if(!t){r.status(400).json({error:"Missing claudeSessionId"});return}let n=this.dbManager.getSessionStore(),o=n.createSDKSession(t,"",""),l=n.getPromptCounter(o),c=n.getUserPrompt(t,l);if(!c||c.trim()===""){H.debug("HOOK","Skipping summary - user prompt was entirely private",{sessionId:o,promptNumber:l}),r.json({status:"skipped",reason:"private"});return}this.sessionManager.queueSummarize(o,s||"",i),this.ensureGeneratorRunning(o,"summarize"),this.workerService.broadcastProcessingStatus(),r.json({status:"queued"})}catch(t){H.failure("WORKER","Summarize by claudeId failed",{},t),r.status(500).json({error:t.message})}}async handleSessionCompleteByClaudeId(e,r){try{let{claudeSessionId:t}=e.body;if(!t){r.status(400).json({success:!1,error:"Missing claudeSessionId"});return}let i=this.dbManager.getSessionStore().findActiveSDKSession(t);if(!i){r.json({success:!0,message:"No active session found"});return}let n=i.id;await this.sessionManager.deleteSession(n),this.dbManager.markSessionComplete(n),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:n}),r.json({success:!0})}catch(t){H.failure("WORKER","Session complete by claudeId failed",{},t),r.status(500).json({success:!1,error:String(t)})}}};var vf=vt(require("path"),1),Un=require("fs"),kw=require("os");Ia();_t();var Tl=class{constructor(e,r,t,s,i,n){this.paginationHelper=e;this.dbManager=r;this.sessionManager=t;this.sseBroadcaster=s;this.workerService=i;this.startTime=n}setupRoutes(e){e.get("/api/observations",this.handleGetObservations.bind(this)),e.get("/api/summaries",this.handleGetSummaries.bind(this)),e.get("/api/prompts",this.handleGetPrompts.bind(this)),e.get("/api/observation/:id",this.handleGetObservationById.bind(this)),e.get("/api/session/:id",this.handleGetSessionById.bind(this)),e.get("/api/prompt/:id",this.handleGetPromptById.bind(this)),e.get("/api/stats",this.handleGetStats.bind(this)),e.get("/api/projects",this.handleGetProjects.bind(this)),e.get("/api/processing-status",this.handleGetProcessingStatus.bind(this)),e.post("/api/processing",this.handleSetProcessing.bind(this))}handleGetObservations(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getObservations(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get observations failed",{},t),r.status(500).json({error:t.message})}}handleGetSummaries(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getSummaries(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get summaries failed",{},t),r.status(500).json({error:t.message})}}handleGetPrompts(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getPrompts(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get prompts failed",{},t),r.status(500).json({error:t.message})}}handleGetObservationById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid observation ID"});return}let i=this.dbManager.getSessionStore().getObservationById(t);if(!i){r.status(404).json({error:`Observation #${t} not found`});return}r.json(i)}catch(t){H.failure("WORKER","Get observation by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetSessionById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid session ID"});return}let i=this.dbManager.getSessionStore().getSessionSummariesByIds([t]);if(i.length===0){r.status(404).json({error:`Session #${t} not found`});return}r.json(i[0])}catch(t){H.failure("WORKER","Get session by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetPromptById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid prompt ID"});return}let i=this.dbManager.getSessionStore().getUserPromptsByIds([t]);if(i.length===0){r.status(404).json({error:`Prompt #${t} not found`});return}r.json(i[0])}catch(t){H.failure("WORKER","Get prompt by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetStats(e,r){try{let t=this.dbManager.getSessionStore().db,s=zr(),i=vf.default.join(s,"package.json"),o=JSON.parse((0,Un.readFileSync)(i,"utf-8")).version,l=t.prepare("SELECT COUNT(*) as count FROM observations").get(),c=t.prepare("SELECT COUNT(*) as count FROM sdk_sessions").get(),u=t.prepare("SELECT COUNT(*) as count FROM session_summaries").get(),p=vf.default.join((0,kw.homedir)(),".claude-mem","claude-mem.db"),f=0;(0,Un.existsSync)(p)&&(f=(0,Un.statSync)(p).size);let d=Math.floor((Date.now()-this.startTime)/1e3),v=this.sessionManager.getActiveSessionCount(),h=this.sseBroadcaster.getClientCount();r.json({worker:{version:o,uptime:d,activeSessions:v,sseClients:h,port:On()},database:{path:p,size:f,observations:l.count,sessions:c.count,summaries:u.count}})}catch(t){H.failure("WORKER","Get stats failed",{},t),r.status(500).json({error:t.message})}}handleGetProjects(e,r){try{let i=this.dbManager.getSessionStore().db.prepare(` +`)}getTypeIcon(e){switch(e){case"bugfix":return"\u{1F534}";case"feature":return"\u{1F7E3}";case"refactor":return"\u{1F504}";case"change":return"\u2705";case"discovery":return"\u{1F535}";case"decision":return"\u{1F9E0}";default:return"\u2022"}}formatDate(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric"})}formatTime(e){return new Date(e).toLocaleString("en-US",{hour:"numeric",minute:"2-digit",hour12:!0})}formatDateTime(e){return new Date(e).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit",hour12:!0})}estimateTokens(e){return e?Math.ceil(e.length/4):0}};var mf=vt(zp(),1),Sw=vt(Ew(),1),ww=vt(require("path"),1);Ia();_t();function Tw(a){let e=[];e.push(mf.default.json({limit:"50mb"})),e.push((0,Sw.default)()),e.push((s,i,n)=>{if(s.path.startsWith("/health")||s.path==="/"||s.path.includes("."))return n();let o=Date.now(),l=`${s.method}-${Date.now()}`,c=a(s.method,s.path,s.body);H.info("HTTP",`\u2192 ${s.method} ${s.path}`,{requestId:l},c);let u=i.send.bind(i);i.send=function(p){let f=Date.now()-o;return H.info("HTTP",`\u2190 ${i.statusCode} ${s.path}`,{requestId:l,duration:`${f}ms`}),u(p)},n()});let r=zr(),t=ww.default.join(r,"plugin","ui");return e.push(mf.default.static(t)),e}function Rw(a,e,r){if(!r||Object.keys(r).length===0||e.includes("/init"))return"";if(e.includes("/observations")){let t=r.tool_name||"?",s=r.tool_input;return`tool=${H.formatTool(t,s)}`}return e.includes("/summarize")?"requesting summary":""}var Pw=vt(require("path"),1),Ow=require("fs");Ia();_t();var Sl=class{constructor(e,r,t){this.sseBroadcaster=e;this.dbManager=r;this.sessionManager=t}setupRoutes(e){e.get("/health",this.handleHealth.bind(this)),e.get("/",this.handleViewerUI.bind(this)),e.get("/stream",this.handleSSEStream.bind(this))}handleHealth(e,r){r.json({status:"ok",timestamp:Date.now()})}handleViewerUI(e,r){try{let t=zr(),s=Pw.default.join(t,"plugin","ui","viewer.html"),i=(0,Ow.readFileSync)(s,"utf-8");r.setHeader("Content-Type","text/html"),r.send(i)}catch(t){H.failure("WORKER","Viewer UI error",{},t),r.status(500).json({error:"Failed to load viewer UI"})}}handleSSEStream(e,r){r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),this.sseBroadcaster.addClient(r);let t=this.dbManager.getSessionStore().getAllProjects();this.sseBroadcaster.broadcast({type:"initial_load",projects:t,timestamp:Date.now()});let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalActiveWork();this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:s,queueDepth:i})}};_t();var Cw=100;function E6(a){let e=(a.match(//g)||[]).length,r=(a.match(//g)||[]).length;return e+r}function hf(a){if(typeof a!="string")return ie("[tag-stripping] received non-string for JSON context:",{type:typeof a}),"{}";let e=E6(a);return e>Cw&&ie("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:Cw,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}var wl=class{constructor(e,r,t,s,i){this.sessionManager=e;this.dbManager=r;this.sdkAgent=t;this.sseBroadcaster=s;this.workerService=i}ensureGeneratorRunning(e,r){let t=this.sessionManager.getSession(e);t&&!t.generatorPromise&&(H.info("SESSION",`Generator auto-starting (${r})`,{sessionId:e,queueDepth:t.pendingMessages.length}),t.generatorPromise=this.sdkAgent.startSession(t,this.workerService).catch(s=>{H.failure("SDK","SDK agent error",{sessionId:e},s)}).finally(()=>{H.info("SESSION","Generator finished",{sessionId:e}),t.generatorPromise=null,this.workerService.broadcastProcessingStatus()}))}setupRoutes(e){e.post("/sessions/:sessionDbId/init",this.handleSessionInit.bind(this)),e.post("/sessions/:sessionDbId/observations",this.handleObservations.bind(this)),e.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),e.get("/sessions/:sessionDbId/status",this.handleSessionStatus.bind(this)),e.delete("/sessions/:sessionDbId",this.handleSessionDelete.bind(this)),e.post("/sessions/:sessionDbId/complete",this.handleSessionComplete.bind(this)),e.post("/api/sessions/observations",this.handleObservationsByClaudeId.bind(this)),e.post("/api/sessions/summarize",this.handleSummarizeByClaudeId.bind(this)),e.post("/api/sessions/complete",this.handleSessionCompleteByClaudeId.bind(this))}handleSessionInit(e,r){try{let t=parseInt(e.params.sessionDbId,10),{userPrompt:s,promptNumber:i}=e.body,n=this.sessionManager.initializeSession(t,s,i),o=this.dbManager.getSessionStore().getLatestUserPrompt(n.claudeSessionId);if(o){this.sseBroadcaster.broadcast({type:"new_prompt",prompt:{id:o.id,claude_session_id:o.claude_session_id,project:o.project,prompt_number:o.prompt_number,prompt_text:o.prompt_text,created_at_epoch:o.created_at_epoch}}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:!0});let l=Date.now(),c=o.prompt_text;this.dbManager.getChromaSync().syncUserPrompt(o.id,o.sdk_session_id,o.project,c,o.prompt_number,o.created_at_epoch).then(()=>{let u=Date.now()-l,p=c.length>60?c.substring(0,60)+"...":c;H.debug("CHROMA","User prompt synced",{promptId:o.id,duration:`${u}ms`,prompt:p})}).catch(u=>{H.error("CHROMA","Failed to sync user_prompt",{promptId:o.id,sessionId:t},u)})}this.workerService.broadcastProcessingStatus(),H.info("SESSION","Generator starting",{sessionId:t,project:n.project,promptNum:n.lastPromptNumber}),n.generatorPromise=this.sdkAgent.startSession(n,this.workerService).catch(l=>{H.failure("SDK","SDK agent error",{sessionId:t},l)}).finally(()=>{H.info("SESSION","Generator finished",{sessionId:t}),n.generatorPromise=null,this.workerService.broadcastProcessingStatus()}),this.sseBroadcaster.broadcast({type:"session_started",sessionDbId:t,project:n.project}),r.json({status:"initialized",sessionDbId:t,port:On()})}catch(t){H.failure("WORKER","Session init failed",{},t),r.status(500).json({error:t.message})}}handleObservations(e,r){try{let t=parseInt(e.params.sessionDbId,10),{tool_name:s,tool_input:i,tool_response:n,prompt_number:o,cwd:l}=e.body;this.sessionManager.queueObservation(t,{tool_name:s,tool_input:i,tool_response:n,prompt_number:o,cwd:l}),this.ensureGeneratorRunning(t,"observation"),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:t}),r.json({status:"queued"})}catch(t){H.failure("WORKER","Observation queuing failed",{},t),r.status(500).json({error:t.message})}}handleSummarize(e,r){try{let t=parseInt(e.params.sessionDbId,10),{last_user_message:s,last_assistant_message:i}=e.body;this.sessionManager.queueSummarize(t,s,i),this.ensureGeneratorRunning(t,"summarize"),this.workerService.broadcastProcessingStatus(),r.json({status:"queued"})}catch(t){H.failure("WORKER","Summarize queuing failed",{},t),r.status(500).json({error:t.message})}}handleSessionStatus(e,r){try{let t=parseInt(e.params.sessionDbId,10),s=this.sessionManager.getSession(t);if(!s){r.json({status:"not_found"});return}r.json({status:"active",sessionDbId:t,project:s.project,queueLength:s.pendingMessages.length,uptime:Date.now()-s.startTime})}catch(t){H.failure("WORKER","Session status failed",{},t),r.status(500).json({error:t.message})}}async handleSessionDelete(e,r){try{let t=parseInt(e.params.sessionDbId,10);await this.sessionManager.deleteSession(t),this.dbManager.markSessionComplete(t),this.sseBroadcaster.broadcast({type:"session_completed",sessionDbId:t}),r.json({status:"deleted"})}catch(t){H.failure("WORKER","Session delete failed",{},t),r.status(500).json({error:t.message})}}async handleSessionComplete(e,r){try{let t=parseInt(e.params.sessionDbId,10);if(isNaN(t)){r.status(400).json({success:!1,error:"Invalid session ID"});return}await this.sessionManager.deleteSession(t),this.dbManager.markSessionComplete(t),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:t}),r.json({success:!0})}catch(t){H.failure("WORKER","Session complete failed",{},t),r.status(500).json({success:!1,error:String(t)})}}handleObservationsByClaudeId(e,r){try{let{claudeSessionId:t,tool_name:s,tool_input:i,tool_response:n,cwd:o}=e.body;if(!t){r.status(400).json({error:"Missing claudeSessionId"});return}let l=this.dbManager.getSessionStore(),c=l.createSDKSession(t,"",""),u=l.getPromptCounter(c),p=l.getUserPrompt(t,u);if(!p||p.trim()===""){H.debug("HOOK","Skipping observation - user prompt was entirely private",{sessionId:c,promptNumber:u,tool_name:s}),r.json({status:"skipped",reason:"private"});return}let f="{}",d="{}";try{f=i!==void 0?hf(JSON.stringify(i)):"{}"}catch{f='{"error": "Failed to serialize tool_input"}'}try{d=n!==void 0?hf(JSON.stringify(n)):"{}"}catch{d='{"error": "Failed to serialize tool_response"}'}this.sessionManager.queueObservation(c,{tool_name:s,tool_input:f,tool_response:d,prompt_number:u,cwd:o||""}),this.ensureGeneratorRunning(c,"observation"),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"observation_queued",sessionDbId:c}),r.json({status:"queued"})}catch(t){H.failure("WORKER","Observation by claudeId failed",{},t),r.status(500).json({error:t.message})}}handleSummarizeByClaudeId(e,r){try{let{claudeSessionId:t,last_user_message:s,last_assistant_message:i}=e.body;if(!t){r.status(400).json({error:"Missing claudeSessionId"});return}let n=this.dbManager.getSessionStore(),o=n.createSDKSession(t,"",""),l=n.getPromptCounter(o),c=n.getUserPrompt(t,l);if(!c||c.trim()===""){H.debug("HOOK","Skipping summary - user prompt was entirely private",{sessionId:o,promptNumber:l}),r.json({status:"skipped",reason:"private"});return}this.sessionManager.queueSummarize(o,s||"",i),this.ensureGeneratorRunning(o,"summarize"),this.workerService.broadcastProcessingStatus(),r.json({status:"queued"})}catch(t){H.failure("WORKER","Summarize by claudeId failed",{},t),r.status(500).json({error:t.message})}}async handleSessionCompleteByClaudeId(e,r){try{let{claudeSessionId:t}=e.body;if(!t){r.status(400).json({success:!1,error:"Missing claudeSessionId"});return}let i=this.dbManager.getSessionStore().findActiveSDKSession(t);if(!i){r.json({success:!0,message:"No active session found"});return}let n=i.id;await this.sessionManager.deleteSession(n),this.dbManager.markSessionComplete(n),this.workerService.broadcastProcessingStatus(),this.sseBroadcaster.broadcast({type:"session_completed",timestamp:Date.now(),sessionDbId:n}),r.json({success:!0})}catch(t){H.failure("WORKER","Session complete by claudeId failed",{},t),r.status(500).json({success:!1,error:String(t)})}}};var vf=vt(require("path"),1),Un=require("fs"),kw=require("os");Ia();_t();var Tl=class{constructor(e,r,t,s,i,n){this.paginationHelper=e;this.dbManager=r;this.sessionManager=t;this.sseBroadcaster=s;this.workerService=i;this.startTime=n}setupRoutes(e){e.get("/api/observations",this.handleGetObservations.bind(this)),e.get("/api/summaries",this.handleGetSummaries.bind(this)),e.get("/api/prompts",this.handleGetPrompts.bind(this)),e.get("/api/observation/:id",this.handleGetObservationById.bind(this)),e.get("/api/session/:id",this.handleGetSessionById.bind(this)),e.get("/api/prompt/:id",this.handleGetPromptById.bind(this)),e.get("/api/stats",this.handleGetStats.bind(this)),e.get("/api/projects",this.handleGetProjects.bind(this)),e.get("/api/processing-status",this.handleGetProcessingStatus.bind(this)),e.post("/api/processing",this.handleSetProcessing.bind(this))}handleGetObservations(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getObservations(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get observations failed",{},t),r.status(500).json({error:t.message})}}handleGetSummaries(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getSummaries(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get summaries failed",{},t),r.status(500).json({error:t.message})}}handleGetPrompts(e,r){try{let{offset:t,limit:s,project:i}=this.parsePaginationParams(e),n=this.paginationHelper.getPrompts(t,s,i);r.json(n)}catch(t){H.failure("WORKER","Get prompts failed",{},t),r.status(500).json({error:t.message})}}handleGetObservationById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid observation ID"});return}let i=this.dbManager.getSessionStore().getObservationById(t);if(!i){r.status(404).json({error:`Observation #${t} not found`});return}r.json(i)}catch(t){H.failure("WORKER","Get observation by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetSessionById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid session ID"});return}let i=this.dbManager.getSessionStore().getSessionSummariesByIds([t]);if(i.length===0){r.status(404).json({error:`Session #${t} not found`});return}r.json(i[0])}catch(t){H.failure("WORKER","Get session by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetPromptById(e,r){try{let t=parseInt(e.params.id,10);if(isNaN(t)){r.status(400).json({error:"Invalid prompt ID"});return}let i=this.dbManager.getSessionStore().getUserPromptsByIds([t]);if(i.length===0){r.status(404).json({error:`Prompt #${t} not found`});return}r.json(i[0])}catch(t){H.failure("WORKER","Get prompt by ID failed",{},t),r.status(500).json({error:t.message})}}handleGetStats(e,r){try{let t=this.dbManager.getSessionStore().db,s=zr(),i=vf.default.join(s,"package.json"),o=JSON.parse((0,Un.readFileSync)(i,"utf-8")).version,l=t.prepare("SELECT COUNT(*) as count FROM observations").get(),c=t.prepare("SELECT COUNT(*) as count FROM sdk_sessions").get(),u=t.prepare("SELECT COUNT(*) as count FROM session_summaries").get(),p=vf.default.join((0,kw.homedir)(),".claude-mem","claude-mem.db"),f=0;(0,Un.existsSync)(p)&&(f=(0,Un.statSync)(p).size);let d=Math.floor((Date.now()-this.startTime)/1e3),v=this.sessionManager.getActiveSessionCount(),h=this.sseBroadcaster.getClientCount();r.json({worker:{version:o,uptime:d,activeSessions:v,sseClients:h,port:On()},database:{path:p,size:f,observations:l.count,sessions:c.count,summaries:u.count}})}catch(t){H.failure("WORKER","Get stats failed",{},t),r.status(500).json({error:t.message})}}handleGetProjects(e,r){try{let i=this.dbManager.getSessionStore().db.prepare(` SELECT DISTINCT project FROM observations WHERE project IS NOT NULL diff --git a/src/services/sqlite/SessionStore.ts b/src/services/sqlite/SessionStore.ts index 5c58e883..434bc916 100644 --- a/src/services/sqlite/SessionStore.ts +++ b/src/services/sqlite/SessionStore.ts @@ -703,6 +703,34 @@ export class SessionStore { return rows.map(row => row.project); } + /** + * Get latest user prompt with session info for a Claude session + * Used for syncing prompts to Chroma during session initialization + */ + getLatestUserPrompt(claudeSessionId: string): { + id: number; + claude_session_id: string; + sdk_session_id: string; + project: string; + prompt_number: number; + prompt_text: string; + created_at_epoch: number; + } | undefined { + const stmt = this.db.prepare(` + SELECT + up.*, + s.sdk_session_id, + s.project + FROM user_prompts up + JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id + WHERE up.claude_session_id = ? + ORDER BY up.created_at_epoch DESC + LIMIT 1 + `); + + return stmt.get(claudeSessionId) as any; + } + /** * Get recent sessions with their status and summary info */ diff --git a/src/services/worker/http/routes/SessionRoutes.ts b/src/services/worker/http/routes/SessionRoutes.ts index ebe01361..bea9499e 100644 --- a/src/services/worker/http/routes/SessionRoutes.ts +++ b/src/services/worker/http/routes/SessionRoutes.ts @@ -73,18 +73,7 @@ export class SessionRoutes { const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber); // Get the latest user_prompt for this session to sync to Chroma - const db = this.dbManager.getSessionStore().db; - const latestPrompt = db.prepare(` - SELECT - up.*, - s.sdk_session_id, - s.project - FROM user_prompts up - JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id - WHERE up.claude_session_id = ? - ORDER BY up.created_at_epoch DESC - LIMIT 1 - `).get(session.claudeSessionId) as any; + const latestPrompt = this.dbManager.getSessionStore().getLatestUserPrompt(session.claudeSessionId); // Broadcast new prompt to SSE clients (for web UI) if (latestPrompt) {