diff --git a/plugin/scripts/cleanup-hook.js b/plugin/scripts/cleanup-hook.js index 2c730b3c..c70d0b01 100755 --- a/plugin/scripts/cleanup-hook.js +++ b/plugin/scripts/cleanup-hook.js @@ -7,5 +7,5 @@ To start manually, run: If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as X}from"fs";import{homedir as B}from"os";import{join as V}from"path";var $=V(B(),".claude-mem","silent.log");function E(e,t,o=""){let i=new Date().toISOString(),_=((new Error().stack||"").split(` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),L=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",p=`[${i}] [${L}] ${e}`;if(t!==void 0)try{p+=` ${JSON.stringify(t)}`}catch(l){p+=` [stringify error: ${l}]`}p+=` -`;try{X($,p)}catch(l){console.error("[silent-debug] Failed to write to log:",l)}return o}async function N(e){E("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(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:t,reason:o}=e;await M();let i=T();try{let n=await fetch(`http://127.0.0.1:${i}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:o}),signal:AbortSignal.timeout(2e3)});if(n.ok){let s=await n.json();E("[cleanup-hook] Session cleanup completed",s)}else E("[cleanup-hook] Session not found or already cleaned up")}catch(n){E("[cleanup-hook] Worker not reachable (non-critical)",{error:n.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(d.isTTY)N(void 0);else{let e="";d.on("data",t=>e+=t),d.on("end",async()=>{let t=e?JSON.parse(e):void 0;await N(t)})} +`;try{X($,p)}catch(l){console.error("[silent-debug] Failed to write to log:",l)}return o}async function N(e){await M(),E("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(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:t,reason:o}=e,i=T();try{let n=await fetch(`http://127.0.0.1:${i}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:o}),signal:AbortSignal.timeout(2e3)});if(n.ok){let s=await n.json();E("[cleanup-hook] Session cleanup completed",s)}else E("[cleanup-hook] Session not found or already cleaned up")}catch(n){E("[cleanup-hook] Worker not reachable (non-critical)",{error:n.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(d.isTTY)N(void 0);else{let e="";d.on("data",t=>e+=t),d.on("end",async()=>{let t=e?JSON.parse(e):void 0;await N(t)})} diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index bc4c9489..93c0f149 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,2 +1,8 @@ #!/usr/bin/env node -import L from"path";import{stdin as u}from"process";import{execSync as f}from"child_process";import N from"path";import{homedir as R}from"os";import{join as r,dirname as l,basename as x}from"path";import{homedir as T}from"os";import{fileURLToPath as g}from"url";function A(){return typeof __dirname<"u"?__dirname:l(g(import.meta.url))}var v=A(),n=process.env.CLAUDE_MEM_DATA_DIR||r(T(),".claude-mem"),E=process.env.CLAUDE_CONFIG_DIR||r(T(),".claude"),k=r(n,"archives"),b=r(n,"logs"),W=r(n,"trash"),F=r(n,"backups"),H=r(n,"settings.json"),X=r(n,"claude-mem.db"),B=r(n,"vector-db"),V=r(E,"settings.json"),j=r(E,"commands"),G=r(E,"CLAUDE.md");import{readFileSync as d,existsSync as M}from"fs";var C=["bugfix","feature","refactor","discovery","decision","change"],D=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=C.join(","),S=D.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let o=this.get(t);return parseInt(o,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!M(t))return this.getAllDefaults();let o=d(t,"utf-8"),i=JSON.parse(o).env||{},c={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))i[a]!==void 0&&(c[a]=i[a]);return c}};function m(){let e=N.join(R(),".claude-mem","settings.json"),t=_.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function U(e,t=1e4){let o=Date.now(),s=100;for(;Date.now()-o /dev/null 2>&1`,{timeout:1e3}),!0}catch{await new Promise(i=>setTimeout(i,s))}return!1}async function O(e){let t=e?.cwd??process.cwd(),o=t?L.basename(t):"unknown-project",s=m();if(!await U(s))throw new Error(`Worker service not available on port ${s} after 10s. Try: npm run worker:restart`);let c=`http://127.0.0.1:${s}/api/context/inject?project=${encodeURIComponent(o)}`;return f(`curl -s "${c}"`,{encoding:"utf-8",timeout:5e3}).trim()}var I=process.argv.includes("--colors");if(u.isTTY||I)O(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";u.on("data",t=>e+=t),u.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,o=await O(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:o}})),process.exit(0)})} +import W from"path";import{stdin as S}from"process";import{execSync as b}from"child_process";import p from"path";import{existsSync as f}from"fs";import{homedir as y}from"os";import{spawnSync as P}from"child_process";import{join as o,dirname as D,basename as F}from"path";import{homedir as m}from"os";import{fileURLToPath as M}from"url";function N(){return typeof __dirname<"u"?__dirname:D(M(import.meta.url))}var R=N(),n=process.env.CLAUDE_MEM_DATA_DIR||o(m(),".claude-mem"),T=process.env.CLAUDE_CONFIG_DIR||o(m(),".claude"),G=o(n,"archives"),K=o(n,"logs"),Y=o(n,"trash"),$=o(n,"backups"),J=o(n,"settings.json"),q=o(n,"claude-mem.db"),z=o(n,"vector-db"),Q=o(T,"settings.json"),Z=o(T,"commands"),tt=o(T,"CLAUDE.md");function u(){return o(R,"..","..")}import{readFileSync as I,existsSync as x}from"fs";var L=["bugfix","feature","refactor","discovery","decision","change"],U=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var O=L.join(","),g=U.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:O,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:g,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!x(t))return this.getAllDefaults();let r=I(t,"utf-8"),s=JSON.parse(r).env||{},i={...this.DEFAULTS};for(let c of Object.keys(this.DEFAULTS))s[c]!==void 0&&(i[c]=s[c]);return i}};var h=100,w=500,k=10;function E(){let e=p.join(y(),".claude-mem","settings.json"),t=_.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function A(){try{let e=E();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(h)})).ok}catch{return!1}}async function v(){try{let e=u(),t=p.join(e,"ecosystem.config.cjs");if(!f(t))throw new Error(`Ecosystem config not found at ${t}`);let r=p.join(e,"node_modules",".bin","pm2"),a=process.platform==="win32"?r+".cmd":r,s=f(a)?a:"pm2",i=P(s,["start",t],{cwd:e,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(i.status!==0)throw new Error(i.stderr||"PM2 start failed");for(let c=0;csetTimeout(d,w)),await A())return!0;return!1}catch{return!1}}async function C(){if(await A())return;if(!await v()){let t=E(),r=u();throw new Error(`Worker service failed to start on port ${t}. + +To start manually, run: + cd ${r} + npx pm2 start ecosystem.config.cjs + +If already running, try: npx pm2 restart claude-mem-worker`)}}async function l(e){await C();let t=e?.cwd??process.cwd(),r=t?W.basename(t):"unknown-project",s=`http://127.0.0.1:${E()}/api/context/inject?project=${encodeURIComponent(r)}`;return b(`curl -s "${s}"`,{encoding:"utf-8",timeout:5e3}).trim()}var H=process.argv.includes("--colors");if(S.isTTY||H)l(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";S.on("data",t=>e+=t),S.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,r=await l(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})} diff --git a/plugin/scripts/new-hook.js b/plugin/scripts/new-hook.js index 25a21dc6..99516982 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -425,4 +425,4 @@ To start manually, run: If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as ae}from"fs";import{homedir as pe}from"os";import{join as ce}from"path";var de=ce(pe(),".claude-mem","silent.log");function g(a,e,s=""){let t=new Date().toISOString(),i=((new Error().stack||"").split(` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=i?`${i[1].split("/").pop()}:${i[2]}`:"unknown",d=`[${t}] [${p}] ${a}`;if(e!==void 0)try{d+=` ${JSON.stringify(e)}`}catch(E){d+=` [stringify error: ${E}]`}d+=` -`;try{ae(de,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var P=100;function _e(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function H(a){if(typeof a!="string")return g("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=_e(a);return e>P&&g("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:P,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function Ee(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;g("[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=ue.basename(s);g("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s}),await X();let o=new R,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=H(t);if(!p||p.trim()===""){g("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(L("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=h(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.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"):_}console.log(L("UserPromptSubmit",!0))}var D="";B.on("data",a=>D+=a);B.on("end",async()=>{let a=D?JSON.parse(D):void 0;await Ee(a)}); +`;try{ae(de,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var P=100;function _e(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function H(a){if(typeof a!="string")return g("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=_e(a);return e>P&&g("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:P,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function Ee(a){if(await X(),!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;g("[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=ue.basename(s);g("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s});let o=new R,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=H(t);if(!p||p.trim()===""){g("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(L("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=h(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.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"):_}console.log(L("UserPromptSubmit",!0))}var D="";B.on("data",a=>D+=a);B.on("end",async()=>{let a=D?JSON.parse(D):void 0;await Ee(a)}); diff --git a/plugin/scripts/save-hook.js b/plugin/scripts/save-hook.js index b2324a08..aaa26214 100755 --- a/plugin/scripts/save-hook.js +++ b/plugin/scripts/save-hook.js @@ -7,4 +7,4 @@ To start manually, run: cd ${e} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}var q=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function Q(n){if(!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:r,tool_response:s}=n;if(q.has(o)){console.log(S("PostToolUse",!0));return}await I();let a=l(),_=E.formatTool(o,r);E.dataIn("HOOK",`PostToolUse: ${_}`,{workerPort:a});try{let c=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:r,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let p=await c.text();throw E.failure("HOOK","Failed to send observation",{status:c.status},p),new Error(`Failed to send observation to worker: ${c.status} ${p}`)}E.debug("HOOK","Observation sent successfully",{toolName:o})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.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"):c}console.log(S("PostToolUse",!0))}var A="";x.on("data",n=>A+=n);x.on("end",async()=>{let n=A?JSON.parse(A):void 0;await Q(n)}); +If already running, try: npx pm2 restart claude-mem-worker`)}}var q=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function Q(n){if(await I(),!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:r,tool_response:s}=n;if(q.has(o)){console.log(S("PostToolUse",!0));return}let a=l(),_=E.formatTool(o,r);E.dataIn("HOOK",`PostToolUse: ${_}`,{workerPort:a});try{let c=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:r,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let p=await c.text();throw E.failure("HOOK","Failed to send observation",{status:c.status},p),new Error(`Failed to send observation to worker: ${c.status} ${p}`)}E.debug("HOOK","Observation sent successfully",{toolName:o})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.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"):c}console.log(S("PostToolUse",!0))}var A="";x.on("data",n=>A+=n);x.on("end",async()=>{let n=A?JSON.parse(A):void 0;await Q(n)}); diff --git a/plugin/scripts/summary-hook.js b/plugin/scripts/summary-hook.js index 30387ba8..90ef739e 100755 --- a/plugin/scripts/summary-hook.js +++ b/plugin/scripts/summary-hook.js @@ -13,4 +13,4 @@ If already running, try: npx pm2 restart claude-mem-worker`)}}function Q(o){if(! `);for(let r=e.length-1;r>=0;r--)try{let n=JSON.parse(e[r]);if(n.type==="assistant"&&n.message?.content){let s="",i=n.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(f=>f.type==="text").map(f=>f.text).join(` `)),s=s.replace(/[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,` -`).trim(),s}}catch{continue}}catch(t){p.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function tt(o){if(!o)throw new Error("summaryHook requires input");let{session_id:t}=o;await U();let e=_(),r=Q(o.transcript_path||""),n=Z(o.transcript_path||"");p.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!r,hasLastAssistantMessage:!!n});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:r,last_assistant_message:n}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw p.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}p.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.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"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(R("Stop",!0))}var C="";I.on("data",o=>C+=o);I.on("end",async()=>{let o=C?JSON.parse(C):void 0;await tt(o)}); +`).trim(),s}}catch{continue}}catch(t){p.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function tt(o){if(await U(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=_(),r=Q(o.transcript_path||""),n=Z(o.transcript_path||"");p.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!r,hasLastAssistantMessage:!!n});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:r,last_assistant_message:n}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw p.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}p.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.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"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(R("Stop",!0))}var C="";I.on("data",o=>C+=o);I.on("end",async()=>{let o=C?JSON.parse(C):void 0;await tt(o)}); diff --git a/src/hooks/cleanup-hook.ts b/src/hooks/cleanup-hook.ts index 120da130..a94f1417 100644 --- a/src/hooks/cleanup-hook.ts +++ b/src/hooks/cleanup-hook.ts @@ -22,6 +22,9 @@ export interface SessionEndInput { * Cleanup Hook Main Logic - Fire-and-forget HTTP client */ async function cleanupHook(input?: SessionEndInput): Promise { + // Ensure worker is running before any other logic + await ensureWorkerRunning(); + silentDebug('[cleanup-hook] Hook fired', { session_id: input?.session_id, cwd: input?.cwd, @@ -44,9 +47,6 @@ async function cleanupHook(input?: SessionEndInput): Promise { const { session_id, reason } = input; - // Ensure worker is running - await ensureWorkerRunning(); - const port = getWorkerPort(); try { diff --git a/src/hooks/context-hook.ts b/src/hooks/context-hook.ts index 4018a9d0..b2a0f337 100644 --- a/src/hooks/context-hook.ts +++ b/src/hooks/context-hook.ts @@ -9,7 +9,7 @@ import path from "path"; import { stdin } from "process"; import { execSync } from "child_process"; -import { getWorkerPort } from "../shared/worker-utils.js"; +import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js"; export interface SessionStartInput { session_id?: string; @@ -20,36 +20,14 @@ export interface SessionStartInput { [key: string]: any; } -async function waitForPort(port: number, maxWaitMs: number = 10000): Promise { - const startTime = Date.now(); - const pollInterval = 100; - - while (Date.now() - startTime < maxWaitMs) { - try { - execSync(`curl -s -f -m 1 "http://127.0.0.1:${port}/api/health" > /dev/null 2>&1`, { - timeout: 1000, - }); - return true; - } catch { - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - } - } - return false; -} - async function contextHook(input?: SessionStartInput): Promise { + // Ensure worker is running before any other logic + await ensureWorkerRunning(); + const cwd = input?.cwd ?? process.cwd(); const project = cwd ? path.basename(cwd) : "unknown-project"; const port = getWorkerPort(); - // Wait for worker to be available - const isAvailable = await waitForPort(port); - if (!isAvailable) { - throw new Error( - `Worker service not available on port ${port} after 10s. Try: npm run worker:restart` - ); - } - const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`; const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 }); return result.trim(); diff --git a/src/hooks/new-hook.ts b/src/hooks/new-hook.ts index e18f51d1..d9e16c09 100644 --- a/src/hooks/new-hook.ts +++ b/src/hooks/new-hook.ts @@ -53,6 +53,9 @@ export interface UserPromptSubmitInput { * New Hook Main Logic */ async function newHook(input?: UserPromptSubmitInput): Promise { + // Ensure worker is running before any other logic + await ensureWorkerRunning(); + if (!input) { throw new Error('newHook requires input'); } @@ -79,9 +82,6 @@ async function newHook(input?: UserPromptSubmitInput): Promise { cwd_was: cwd }); - // Ensure worker is running - await ensureWorkerRunning(); - const db = new SessionStore(); // CRITICAL: Use session_id from hook as THE source of truth diff --git a/src/hooks/save-hook.ts b/src/hooks/save-hook.ts index d6dd96ef..a2a5329a 100644 --- a/src/hooks/save-hook.ts +++ b/src/hooks/save-hook.ts @@ -33,6 +33,9 @@ const SKIP_TOOLS = new Set([ * Save Hook Main Logic - Fire-and-forget HTTP client */ async function saveHook(input?: PostToolUseInput): Promise { + // Ensure worker is running before any other logic + await ensureWorkerRunning(); + if (!input) { throw new Error('saveHook requires input'); } @@ -44,9 +47,6 @@ async function saveHook(input?: PostToolUseInput): Promise { return; } - // Ensure worker is running - await ensureWorkerRunning(); - const port = getWorkerPort(); const toolStr = logger.formatTool(tool_name, tool_input); diff --git a/src/hooks/summary-hook.ts b/src/hooks/summary-hook.ts index 0cfc2c0f..7720a3b2 100644 --- a/src/hooks/summary-hook.ts +++ b/src/hooks/summary-hook.ts @@ -130,15 +130,15 @@ function extractLastAssistantMessage(transcriptPath: string): string { * Summary Hook Main Logic - Fire-and-forget HTTP client */ async function summaryHook(input?: StopInput): Promise { + // Ensure worker is running before any other logic + await ensureWorkerRunning(); + if (!input) { throw new Error('summaryHook requires input'); } const { session_id } = input; - // Ensure worker is running - await ensureWorkerRunning(); - const port = getWorkerPort(); // Extract last user AND assistant messages from transcript