Files
claude-mem/plugin/scripts/summary-hook.js
Alex Newman c3761a2204 Refactor silent debugging to happy path error handling
- Replaced instances of silentDebug with happy_path_error__with_fallback across multiple files to improve error logging and handling.
- Updated the utility function to provide clearer semantics for error handling when expected values are missing.
- Introduced a script to find potential silent failures in the codebase that may need to be addressed with the new error handling approach.
2025-12-09 15:09:44 -05:00

19 lines
9.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
import{stdin as x}from"process";import{readFileSync as H,existsSync as $}from"fs";function W(r,t,e){return r==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function M(r,t,e={}){let n=W(r,t,e);return JSON.stringify(n)}import{join as X}from"path";import{homedir as B}from"os";import{existsSync as G,readFileSync as Y}from"fs";import{appendFileSync as F}from"fs";import{homedir as K}from"os";import{join as j}from"path";var V=j(K(),".claude-mem","silent.log");function g(r,t,e=""){let n=new Date().toISOString(),a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),E=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u=`[${n}] [HAPPY-PATH-ERROR] [${E}] ${r}`;if(t!==void 0)try{u+=` ${JSON.stringify(t)}`}catch(_){u+=` [stringify error: ${_}]`}u+=`
`;try{F(V,u)}catch(_){console.error("[silent-debug] Failed to write to log:",_)}return e}var m=X(B(),".claude-mem","settings.json");function R(r,t){try{if(G(m)){let n=JSON.parse(Y(m,"utf-8")).env?.[r];if(n!==void 0)return n}}catch(e){g("Failed to load settings file",{error:e,settingsPath:m,key:r})}return process.env[r]||t}var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),d=class{level;useColor;constructor(){let t=R("CLAUDE_MEM_LOG_LEVEL","INFO").toUpperCase();this.level=T[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let n=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&n.command){let o=n.command.length>50?n.command.substring(0,50)+"...":n.command;return`${t}(${o})`}if(t==="Read"&&n.file_path){let o=n.file_path.split("/").pop()||n.file_path;return`${t}(${o})`}if(t==="Edit"&&n.file_path){let o=n.file_path.split("/").pop()||n.file_path;return`${t}(${o})`}if(t==="Write"&&n.file_path){let o=n.file_path.split("/").pop()||n.file_path;return`${t}(${o})`}return t}catch{return t}}log(t,e,n,o,s){if(t<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),a=T[t].padEnd(5),E=e.padEnd(6),u="";o?.correlationId?u=`[${o.correlationId}] `:o?.sessionId&&(u=`[session-${o.sessionId}] `);let _="";s!=null&&(this.level===0&&typeof s=="object"?_=`
`+JSON.stringify(s,null,2):_=" "+this.formatData(s));let A="";if(o){let{sessionId:ct,sdkSessionId:pt,correlationId:ut,...L}=o;Object.keys(L).length>0&&(A=` {${Object.entries(L).map(([b,v])=>`${b}=${v}`).join(", ")}}`)}let h=`[${i}] [${a}] [${E}] ${u}${n}${A}${_}`;t===3?console.error(h):console.log(h)}debug(t,e,n,o){this.log(0,t,e,n,o)}info(t,e,n,o){this.log(1,t,e,n,o)}warn(t,e,n,o){this.log(2,t,e,n,o)}error(t,e,n,o){this.log(3,t,e,n,o)}dataIn(t,e,n,o){this.info(t,`\u2192 ${e}`,n,o)}dataOut(t,e,n,o){this.info(t,`\u2190 ${e}`,n,o)}success(t,e,n,o){this.info(t,`\u2713 ${e}`,n,o)}failure(t,e,n,o){this.error(t,`\u2717 ${e}`,n,o)}timing(t,e,n,o){this.info(t,`\u23F1 ${e}`,o,{duration:`${n}ms`})}},c=new d;import f from"path";import{existsSync as y}from"fs";import{homedir as P}from"os";import{spawnSync as I}from"child_process";import{readFileSync as z,existsSync as Q}from"fs";import{join as Z}from"path";import{homedir as tt}from"os";var J=["bugfix","feature","refactor","discovery","decision","change"],q=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var D=J.join(","),N=q.join(",");var S=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_DATA_DIR:Z(tt(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",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:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:N,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 e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!Q(t))return this.getAllDefaults();let e=z(t,"utf-8"),o=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))o[i]!==void 0&&(s[i]=o[i]);return s}};var l={DEFAULT:5e3,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,WINDOWS_MULTIPLIER:1.5};function U(r){return process.platform==="win32"?Math.round(r*l.WINDOWS_MULTIPLIER):r}var p=f.join(P(),".claude","plugins","marketplaces","thedotmack"),et=U(l.HEALTH_CHECK),rt=l.WORKER_STARTUP_WAIT,nt=l.WORKER_STARTUP_RETRIES;function O(){let r=f.join(P(),".claude-mem","settings.json"),t=S.loadFromFile(r);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function k(){try{let r=O();return(await fetch(`http://127.0.0.1:${r}/health`,{signal:AbortSignal.timeout(et)})).ok}catch(r){return c.debug("SYSTEM","Worker health check failed",{error:r instanceof Error?r.message:String(r),errorType:r?.constructor?.name}),!1}}async function ot(){try{let r=f.join(p,"plugin","scripts","worker-service.cjs");if(!y(r))throw new Error(`Worker script not found at ${r}`);if(process.platform==="win32"){let t=I("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${r}' -WorkingDirectory '${p}' -WindowStyle Hidden`],{cwd:p,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=f.join(p,"ecosystem.config.cjs");if(!y(t))throw new Error(`Ecosystem config not found at ${t}`);let e=f.join(p,"node_modules",".bin","pm2"),n=y(e)?e:"pm2",o=I(n,["start",t],{cwd:p,stdio:"pipe",encoding:"utf-8"});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed")}for(let t=0;t<nt;t++)if(await new Promise(e=>setTimeout(e,rt)),await k())return!0;return!1}catch(r){return c.error("SYSTEM","Failed to start worker",{platform:process.platform,workerScript:f.join(p,"plugin","scripts","worker-service.cjs"),error:r instanceof Error?r.message:String(r),marketplaceRoot:p}),!1}}async function w(){if(await k())return;if(!await ot()){let t=O();throw new Error(`Worker service failed to start on port ${t}.
To start manually, run:
cd ${p}
npx pm2 start ecosystem.config.cjs
If already running, try: npx pm2 restart claude-mem-worker`)}}function st(r){if(!r||!$(r))return"";try{let t=H(r,"utf-8").trim();if(!t)return"";let e=t.split(`
`);for(let n=e.length-1;n>=0;n--)try{let o=JSON.parse(e[n]);if(o.type==="user"&&o.message?.content){let s=o.message.content;if(typeof s=="string")return s;if(Array.isArray(s))return s.filter(a=>a.type==="text").map(a=>a.text).join(`
`)}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:r},t)}return""}function it(r){if(!r||!$(r))return"";try{let t=H(r,"utf-8").trim();if(!t)return"";let e=t.split(`
`);for(let n=e.length-1;n>=0;n--)try{let o=JSON.parse(e[n]);if(o.type==="assistant"&&o.message?.content){let s="",i=o.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(E=>E.type==="text").map(E=>E.text).join(`
`)),s=s.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,`
`).trim(),s}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:r},t)}return""}async function at(r){if(await w(),!r)throw new Error("summaryHook requires input");let{session_id:t}=r,e=O(),n=g("Missing transcript_path in Stop hook input",{session_id:t},r.transcript_path||""),o=st(n),s=it(n);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!o,hasLastAssistantMessage:!!s});try{let i=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:o,last_assistant_message:s}),signal:AbortSignal.timeout(l.DEFAULT)});if(!i.ok){let a=await i.text();throw c.failure("HOOK","Failed to generate summary",{status:i.status},a),new Error(`Failed to request summary from worker: ${i.status} ${a}`)}c.debug("HOOK","Summary request sent successfully")}catch(i){throw i.cause?.code==="ECONNREFUSED"||i.name==="TimeoutError"||i.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"):i}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(M("Stop",!0))}var C="";x.on("data",r=>C+=r);x.on("end",async()=>{let r=C?JSON.parse(C):void 0;await at(r)});