From 52e2debb0fee6b39654b8c540a5897356e2c278f Mon Sep 17 00:00:00 2001 From: Nawapat Buakoet Date: Sat, 3 Jan 2026 14:56:30 +0700 Subject: [PATCH 1/2] fix: add retry logic with exponential backoff to hook fetch calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes transient network errors (ECONNRESET, UND_ERR_SOCKET) that occur when hooks communicate with the worker service. Changes: - Add fetchWithRetry() utility in worker-utils.ts - Update context-hook.ts to use fetchWithRetry (fixes #526) - Update summary-hook.ts to use fetchWithRetry - Update save-hook.ts to use fetchWithRetry - Update new-hook.ts to use fetchWithRetry (2 calls) The retry logic: - Retries up to 3 times on transient errors - Uses exponential backoff (100ms → 200ms → 400ms) - Adds jitter to prevent thundering herd - Handles ECONNRESET, ECONNREFUSED, ETIMEDOUT, UND_ERR_SOCKET Fixes #526 Co-authored-by: Claude Code --- plugin/scripts/context-hook.js | 28 ++++++++-------- plugin/scripts/new-hook.js | 24 +++++++------- plugin/scripts/save-hook.js | 26 +++++++-------- plugin/scripts/summary-hook.js | 22 ++++++------- src/hooks/context-hook.ts | 7 ++-- src/hooks/new-hook.ts | 40 +++++++++++++---------- src/hooks/save-hook.ts | 29 +++++++++-------- src/hooks/summary-hook.ts | 23 ++++++++------ src/shared/worker-utils.ts | 58 ++++++++++++++++++++++++++++++++++ 9 files changed, 163 insertions(+), 94 deletions(-) diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index c7bfc9d2..6737c1d6 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,19 +1,19 @@ #!/usr/bin/env bun -import{stdin as L}from"process";import A from"path";import{homedir as G}from"os";import{readFileSync as K}from"fs";import{readFileSync as $,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as W}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,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 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){try{if(!w(t))return this.getAllDefaults();let r=$(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as x,existsSync as b,mkdirSync as H}from"fs";import{join as O}from"path";var S=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");b(r)||H(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),u=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${u}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(C||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=p(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=C[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${l}`}log(t,r,e,n,o){if(t0&&(T=` {${Object.entries(D).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let C=`[${i}] [${E}] [${u}] ${c}${e}${T}${l}`;if(this.logFilePath)try{x(this.logFilePath,C+` -`,"utf8")}catch(m){process.stderr.write(`[LOGGER] Failed to write to log file: ${m} -`)}else process.stderr.write(C+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let c=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",T={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,T,n),o}},_=new f;var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function h(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i} +${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?g=` +`+JSON.stringify(o,null,2):g=" "+this.formatData(o));let u="";if(n){let{sessionId:M,memorySessionId:T,correlationId:Z,...R}=n;Object.keys(R).length>0&&(u=` {${Object.entries(R).map(([$,v])=>`${$}=${v}`).join(", ")}}`)}let f=`[${E}] [${i}] [${l}] ${a}${e}${u}${g}`;if(this.logFilePath)try{H(this.logFilePath,f+` +`,"utf8")}catch(M){process.stderr.write(`[LOGGER] Failed to write to log file: ${M} +`)}else process.stderr.write(f+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},_=new A;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function N(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} -`;return E+=`To restart the worker: -`,E+=`1. Exit Claude Code completely -`,E+=`2. Run: npm run worker:restart -`,E+="3. Restart Claude Code",r&&(E+=` +`;return i+=`To restart the worker: +`,i+=`1. Exit Claude Code completely +`,i+=`2. Run: npm run worker:restart +`,i+="3. Restart Claude Code",r&&(i+=` -If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n} +If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${E}`),E}var X=A.join(G(),".claude","plugins","marketplaces","thedotmack"),At=d(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function j(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function V(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function Y(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function B(){let s=V(),t=await Y();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await j()){await B();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}import J from"path";function N(s){if(!s||s.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=J.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function y(s){await I();let t=s?.cwd??process.cwd(),r=N(t),n=`http://127.0.0.1:${g()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await fetch(n);if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var z=process.argv.includes("--colors");if(L.isTTY||z)y(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";L.on("data",t=>s+=t),L.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await y(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})} +${i}`),i}var V=m.join(X(),".claude","plugins","marketplaces","thedotmack"),At=h(L.HEALTH_CHECK),S=null;function O(){if(S!==null)return S;let s=m.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function I(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let l=0;l<=e;l++)try{return await fetch(s,t)}catch(a){i=a;let g=a?.cause?.code||"",u=a.message||"";if(!E.some(T=>g.includes(T)||u.includes(T))||l===e)throw a;let M=Math.min(n*Math.pow(2,l)+Math.random()*50,o);_.debug("SYSTEM","Fetch failed, retrying",{attempt:l+1,maxRetries:e,delay:Math.round(M),error:u}),await new Promise(T=>setTimeout(T,M))}throw i}async function Y(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=m.join(V,"package.json");return JSON.parse(j(s,"utf-8")).version}async function J(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function y(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:O(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function P(s){if(!s||s.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=z.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function k(s){await y();let t=s?.cwd??process.cwd(),r=P(t),n=`http://127.0.0.1:${O()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await I(n,{});if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var Q=process.argv.includes("--colors");if(D.isTTY||Q)k(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";D.on("data",t=>s+=t),D.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await k(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 6e930404..e7dce5af 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -1,19 +1,19 @@ #!/usr/bin/env bun -import{stdin as k}from"process";var S=JSON.stringify({continue:!0,suppressOutput:!0});import L from"path";import{homedir as G}from"os";import{readFileSync as X}from"fs";import{readFileSync as v,writeFileSync as w,existsSync as b}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",h="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:h,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 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){try{if(!b(t))return this.getAllDefaults();let r=v(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{w(t,JSON.stringify(n,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as x,mkdirSync as K}from"fs";import{join as T}from"path";var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),M=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");x(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=f[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${s}.${l}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),C=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=u.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"logs");G(r)||X(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=m(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=u.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"settings.json"),n=u.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${i}.${l}`}log(t,r,e,n,o){if(t0&&(p=` {${Object.entries(d).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${a}] [${s}] [${l}] ${_}${e}${p}${c}`;if(this.logFilePath)try{W(this.logFilePath,C+` -`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} -`)}else process.stderr.write(C+` +`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let g="";if(n){let{sessionId:p,memorySessionId:O,correlationId:tt,...h}=n;Object.keys(h).length>0&&(g=` {${Object.entries(h).map(([w,v])=>`${w}=${v}`).join(", ")}}`)}let M=`[${a}] [${i}] [${l}] ${_}${e}${g}${c}`;if(this.logFilePath)try{K(this.logFilePath,M+` +`,"utf8")}catch(p){process.stderr.write(`[LOGGER] Failed to write to log file: ${p} +`)}else process.stderr.write(M+` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",p={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,p,n),o}},E=new M;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function U(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",s=`${o}${a} +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",g={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,g,n),o}},E=new C;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function y(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",i=`${o}${a} -`;return s+=`To restart the worker: -`,s+=`1. Exit Claude Code completely -`,s+=`2. Run: npm run worker:restart -`,s+="3. Restart Claude Code",r&&(s+=` +`;return i+=`To restart the worker: +`,i+=`1. Exit Claude Code completely +`,i+=`2. Run: npm run worker:restart +`,i+="3. Restart Claude Code",r&&(i+=` -If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n} +If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${s}`),s}var j=L.join(G(),".claude","plugins","marketplaces","thedotmack"),mt=U(A.HEALTH_CHECK),O=null;function u(){if(O!==null)return O;let i=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function V(){let i=u();return(await fetch(`http://127.0.0.1:${i}/api/readiness`)).ok}function B(){let i=L.join(j,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=u(),t=await fetch(`http://127.0.0.1:${i}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&E.debug("SYSTEM","Version check",{pluginVersion:i,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await V()){await J();return}}catch(e){E.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function y(i){if(!i||i.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=z.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function q(i){if(await I(),!i)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=i,n=y(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=u();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await fetch(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let s=await a.json(),l=s.sessionDbId,_=s.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:s.skipped}),s.skipped&&s.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(S);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let p=await fetch(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_})});if(!p.ok)throw new Error(`SDK agent start failed: ${p.status}`);console.log(S)}var m="";k.on("data",i=>m+=i);k.on("end",async()=>{let i;try{i=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(i)}); +${i}`),i}var Y=D.join(j(),".claude","plugins","marketplaces","thedotmack"),Lt=I(L.HEALTH_CHECK),T=null;function S(){if(T!==null)return T;let s=D.join(u.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=u.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function d(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,a=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let l=0;l<=e;l++)try{return await fetch(s,t)}catch(_){i=_;let c=_?.cause?.code||"",g=_.message||"";if(!a.some(O=>c.includes(O)||g.includes(O))||l===e)throw _;let p=Math.min(n*Math.pow(2,l)+Math.random()*50,o);E.debug("SYSTEM","Fetch failed, retrying",{attempt:l+1,maxRetries:e,delay:Math.round(p),error:g}),await new Promise(O=>setTimeout(O,p))}throw i}async function B(){let s=S();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function J(){let s=D.join(Y,"package.json");return JSON.parse(V(s,"utf-8")).version}async function q(){let s=S(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let s=J(),t=await q();s!==t&&E.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function P(){for(let r=0;r<75;r++){try{if(await B()){await z();return}}catch(e){E.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(y({port:S(),customPrefix:"Worker did not become ready within 15 seconds."}))}import Q from"path";function k(s){if(!s||s.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=Q.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function Z(s){if(await P(),!s)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=s,n=k(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=S();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await d(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let i=await a.json(),l=i.sessionDbId,_=i.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:i.skipped}),i.skipped&&i.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(f);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let g=await d(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_})});if(!g.ok)throw new Error(`SDK agent start failed: ${g.status}`);console.log(f)}var R="";$.on("data",s=>R+=s);$.on("end",async()=>{let s;try{s=R?JSON.parse(R):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Z(s)}); diff --git a/plugin/scripts/save-hook.js b/plugin/scripts/save-hook.js index b8e8bb26..0a47011c 100755 --- a/plugin/scripts/save-hook.js +++ b/plugin/scripts/save-hook.js @@ -1,19 +1,19 @@ #!/usr/bin/env bun -import{stdin as y}from"process";var U=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as k,writeFileSync as v,existsSync as w}from"fs";import{join as F}from"path";import{homedir as H}from"os";var R="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:F(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,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 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){try{if(!w(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as x}from"fs";import{join as S}from"path";var M=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(M||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"logs");b(r)||x(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=S(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=S(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=M[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${l}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),L=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=p(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t0&&(O=` {${Object.entries(m).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${i}] [${E}] [${l}] ${c}${e}${O}${g}`;if(this.logFilePath)try{W(this.logFilePath,C+` -`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} -`)}else process.stderr.write(C+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let c=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=c?`${c[1].split("/").pop()}:${c[2]}`:"unknown",O={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new f;import A from"path";import{homedir as G}from"os";import{readFileSync as K}from"fs";var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i} +`+JSON.stringify(o,null,2):g=" "+this.formatData(o));let u="";if(n){let{sessionId:T,memorySessionId:O,correlationId:Q,...R}=n;Object.keys(R).length>0&&(u=` {${Object.entries(R).map(([k,v])=>`${k}=${v}`).join(", ")}}`)}let f=`[${E}] [${i}] [${a}] ${_}${e}${u}${g}`;if(this.logFilePath)try{x(this.logFilePath,f+` +`,"utf8")}catch(T){process.stderr.write(`[LOGGER] Failed to write to log file: ${T} +`)}else process.stderr.write(f+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},l=new L;import D from"path";import{homedir as X}from"os";import{readFileSync as V}from"fs";var C={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*C.WINDOWS_MULTIPLIER):s}function N(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} -`;return E+=`To restart the worker: -`,E+=`1. Exit Claude Code completely -`,E+=`2. Run: npm run worker:restart -`,E+="3. Restart Claude Code",r&&(E+=` +`;return i+=`To restart the worker: +`,i+=`1. Exit Claude Code completely +`,i+=`2. Run: npm run worker:restart +`,i+="3. Restart Claude Code",r&&(i+=` -If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n} +If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${E}`),E}var X=A.join(G(),".claude","plugins","marketplaces","thedotmack"),At=h(p.HEALTH_CHECK),T=null;function u(){if(T!==null)return T;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function V(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=A.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function B(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function Y(){let s=j(),t=await B();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await Y();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function J(s){if(await N(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=u(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let l=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!l.ok)throw new Error(`Observation storage failed: ${l.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var L="";y.on("data",s=>L+=s);y.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await J(s)}); +${i}`),i}var j=D.join(X(),".claude","plugins","marketplaces","thedotmack"),Lt=I(C.HEALTH_CHECK),S=null;function M(){if(S!==null)return S;let s=D.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function y(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let a=0;a<=e;a++)try{return await fetch(s,t)}catch(_){i=_;let g=_?.cause?.code||"",u=_.message||"";if(!E.some(O=>g.includes(O)||u.includes(O))||a===e)throw _;let T=Math.min(n*Math.pow(2,a)+Math.random()*50,o);l.debug("SYSTEM","Fetch failed, retrying",{attempt:a+1,maxRetries:e,delay:Math.round(T),error:u}),await new Promise(O=>setTimeout(O,T))}throw i}async function Y(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=D.join(j,"package.json");return JSON.parse(V(s,"utf-8")).version}async function J(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&l.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function P(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function z(s){if(await P(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,E=M(),i=l.formatTool(e,n);if(l.dataIn("HOOK",`PostToolUse: ${i}`,{workerPort:E}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let a=await y(`http://127.0.0.1:${E}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!a.ok)throw new Error(`Observation storage failed: ${a.status}`);l.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var m="";$.on("data",s=>m+=s);$.on("end",async()=>{let s;try{s=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await z(s)}); diff --git a/plugin/scripts/summary-hook.js b/plugin/scripts/summary-hook.js index 14ce5b89..f8ba1ec8 100755 --- a/plugin/scripts/summary-hook.js +++ b/plugin/scripts/summary-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{stdin as $}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as w,writeFileSync as v,existsSync as F}from"fs";import{join as x}from"path";import{homedir as H}from"os";var U="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:x(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,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 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){try{if(!F(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as W,existsSync as b,mkdirSync as G}from"fs";import{join as T}from"path";var M=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(M||{}),p=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");b(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=M[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),L=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"logs");K(r)||X(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=m(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t0&&(O=` {${Object.entries(R).map(([k,P])=>`${k}=${P}`).join(", ")}}`)}let C=`[${E}] [${i}] [${_}] ${a}${e}${O}${l}`;if(this.logFilePath)try{W(this.logFilePath,C+` -`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} -`)}else process.stderr.write(C+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",O={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},c=new p;import L from"path";import{homedir as K}from"os";import{readFileSync as X}from"fs";var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*A.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} +`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let u="";if(n){let{sessionId:S,memorySessionId:f,correlationId:et,...d}=n;Object.keys(d).length>0&&(u=` {${Object.entries(d).map(([w,v])=>`${w}=${v}`).join(", ")}}`)}let T=`[${E}] [${i}] [${a}] ${_}${e}${u}${l}`;if(this.logFilePath)try{G(this.logFilePath,T+` +`,"utf8")}catch(S){process.stderr.write(`[LOGGER] Failed to write to log file: ${S} +`)}else process.stderr.write(T+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},c=new L;import D from"path";import{homedir as V}from"os";import{readFileSync as j}from"fs";var C={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function y(s){return process.platform==="win32"?Math.round(s*C.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,8 +16,8 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var V=L.join(K(),".claude","plugins","marketplaces","thedotmack"),Ct=h(A.HEALTH_CHECK),S=null;function u(){if(S!==null)return S;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function j(){let s=u();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=L.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function Y(){let s=u(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=B(),t=await Y();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await j()){await J();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:u(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as q,existsSync as z}from"fs";function y(s,t,r=!1){if(!s||!z(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=q(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(` -`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(` -`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,` +${i}`),i}var Y=D.join(V(),".claude","plugins","marketplaces","thedotmack"),Dt=y(C.HEALTH_CHECK),O=null;function M(){if(O!==null)return O;let s=D.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function N(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let a=0;a<=e;a++)try{return await fetch(s,t)}catch(_){i=_;let l=_?.cause?.code||"",u=_.message||"";if(!E.some(f=>l.includes(f)||u.includes(f))||a===e)throw _;let S=Math.min(n*Math.pow(2,a)+Math.random()*50,o);c.debug("SYSTEM","Fetch failed, retrying",{attempt:a+1,maxRetries:e,delay:Math.round(S),error:u}),await new Promise(f=>setTimeout(f,S))}throw i}async function B(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function J(){let s=D.join(Y,"package.json");return JSON.parse(j(s,"utf-8")).version}async function q(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let s=J(),t=await q();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function $(){for(let r=0;r<75;r++){try{if(await B()){await z();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as Q,existsSync as Z}from"fs";function k(s,t,r=!1){if(!s||!Z(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=Q(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(` +`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let a="",_=i.message.content;if(typeof _=="string")a=_;else if(Array.isArray(_))a=_.filter(l=>l.type==="text").map(l=>l.text).join(` +`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof _}`);return r&&(a=a.replace(/[\s\S]*?<\/system-reminder>/g,""),a=a.replace(/\n{3,}/g,` -`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Q(s){if(await N(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=u();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=y(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(f),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(f)}var m="";$.on("data",s=>m+=s);$.on("end",async()=>{let s;try{s=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(s)}); +`).trim()),a}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function tt(s){if(await $(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=M();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=k(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await N(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(p),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(p)}var R="";P.on("data",s=>R+=s);P.on("end",async()=>{let s;try{s=R?JSON.parse(R):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await tt(s)}); diff --git a/src/hooks/context-hook.ts b/src/hooks/context-hook.ts index 3dc6ca76..f144fb66 100644 --- a/src/hooks/context-hook.ts +++ b/src/hooks/context-hook.ts @@ -7,7 +7,7 @@ */ import { stdin } from "process"; -import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js"; +import { ensureWorkerRunning, getWorkerPort, fetchWithRetry } from "../shared/worker-utils.js"; import { HOOK_TIMEOUTS } from "../shared/hook-constants.js"; import { getProjectName } from "../utils/project-name.js"; import { logger } from "../utils/logger.js"; @@ -29,9 +29,8 @@ async function contextHook(input?: SessionStartInput): Promise { const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`; - // Note: Removed AbortSignal.timeout due to Windows Bun cleanup issue (libuv assertion) - // Worker service has its own timeouts, so client-side timeout is redundant - const response = await fetch(url); + // Uses fetchWithRetry to handle transient network errors like UND_ERR_SOCKET + const response = await fetchWithRetry(url, {}); if (!response.ok) { throw new Error(`Context generation failed: ${response.status}`); diff --git a/src/hooks/new-hook.ts b/src/hooks/new-hook.ts index 2d61c78b..ea4e6bac 100644 --- a/src/hooks/new-hook.ts +++ b/src/hooks/new-hook.ts @@ -1,6 +1,6 @@ import { stdin } from 'process'; import { STANDARD_HOOK_RESPONSE } from './hook-response.js'; -import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; +import { ensureWorkerRunning, getWorkerPort, fetchWithRetry } from '../shared/worker-utils.js'; import { getProjectName } from '../utils/project-name.js'; import { logger } from '../utils/logger.js'; @@ -32,16 +32,19 @@ async function newHook(input?: UserPromptSubmitInput): Promise { logger.info('HOOK', 'new-hook: Calling /api/sessions/init', { contentSessionId: session_id, project, prompt_length: prompt?.length }); // Initialize session via HTTP - handles DB operations and privacy checks - const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - contentSessionId: session_id, - project, - prompt - }) - // Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion) - }); + // Uses fetchWithRetry to handle transient network errors like ECONNRESET + const initResponse = await fetchWithRetry( + `http://127.0.0.1:${port}/api/sessions/init`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contentSessionId: session_id, + project, + prompt + }) + } + ); if (!initResponse.ok) { throw new Error(`Session initialization failed: ${initResponse.status}`); @@ -69,12 +72,15 @@ async function newHook(input?: UserPromptSubmitInput): Promise { logger.info('HOOK', 'new-hook: Calling /sessions/{sessionDbId}/init', { sessionDbId, promptNumber, userPrompt_length: cleanedPrompt?.length }); // Initialize SDK agent session via HTTP (starts the agent!) - const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ userPrompt: cleanedPrompt, promptNumber }) - // Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion) - }); + // Uses fetchWithRetry to handle transient network errors like ECONNRESET + const response = await fetchWithRetry( + `http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userPrompt: cleanedPrompt, promptNumber }) + } + ); if (!response.ok) { throw new Error(`SDK agent start failed: ${response.status}`); diff --git a/src/hooks/save-hook.ts b/src/hooks/save-hook.ts index c31eafaa..4860064e 100644 --- a/src/hooks/save-hook.ts +++ b/src/hooks/save-hook.ts @@ -9,7 +9,7 @@ import { stdin } from 'process'; import { STANDARD_HOOK_RESPONSE } from './hook-response.js'; import { logger } from '../utils/logger.js'; -import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; +import { ensureWorkerRunning, getWorkerPort, fetchWithRetry } from '../shared/worker-utils.js'; import { HOOK_TIMEOUTS } from '../shared/hook-constants.js'; export interface PostToolUseInput { @@ -47,18 +47,21 @@ async function saveHook(input?: PostToolUseInput): Promise { } // Send to worker - worker handles privacy check and database operations - const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - contentSessionId: session_id, - tool_name, - tool_input, - tool_response, - cwd - }) - // Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion) - }); + // Uses fetchWithRetry to handle transient network errors like ECONNRESET + const response = await fetchWithRetry( + `http://127.0.0.1:${port}/api/sessions/observations`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contentSessionId: session_id, + tool_name, + tool_input, + tool_response, + cwd + }) + } + ); if (!response.ok) { throw new Error(`Observation storage failed: ${response.status}`); diff --git a/src/hooks/summary-hook.ts b/src/hooks/summary-hook.ts index ca26ebc5..6d4f290b 100644 --- a/src/hooks/summary-hook.ts +++ b/src/hooks/summary-hook.ts @@ -12,7 +12,7 @@ import { stdin } from 'process'; import { STANDARD_HOOK_RESPONSE } from './hook-response.js'; import { logger } from '../utils/logger.js'; -import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; +import { ensureWorkerRunning, getWorkerPort, fetchWithRetry } from '../shared/worker-utils.js'; import { HOOK_TIMEOUTS } from '../shared/hook-constants.js'; import { extractLastMessage } from '../shared/transcript-parser.js'; @@ -53,15 +53,18 @@ async function summaryHook(input?: StopInput): Promise { }); // Send to worker - worker handles privacy check and database operations - const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - contentSessionId: session_id, - last_assistant_message: lastAssistantMessage - }) - // Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion) - }); + // Uses fetchWithRetry to handle transient network errors like ECONNRESET + const response = await fetchWithRetry( + `http://127.0.0.1:${port}/api/sessions/summarize`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contentSessionId: session_id, + last_assistant_message: lastAssistantMessage + }) + } + ); if (!response.ok) { console.log(STANDARD_HOOK_RESPONSE); diff --git a/src/shared/worker-utils.ts b/src/shared/worker-utils.ts index b9f05efc..62d92763 100644 --- a/src/shared/worker-utils.ts +++ b/src/shared/worker-utils.ts @@ -56,6 +56,64 @@ export function clearPortCache(): void { cachedHost = null; } +/** + * Fetch with retry logic for transient network errors + * Handles ECONNRESET, ECONNREFUSED, ETIMEDOUT, and socket errors + */ +export async function fetchWithRetry( + url: string, + options: RequestInit, + config: { + maxRetries?: number; + initialDelayMs?: number; + maxDelayMs?: number; + } = {} +): Promise { + const { + maxRetries = 3, + initialDelayMs = 100, + maxDelayMs = 1000 + } = config; + + const retryableErrors = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'UND_ERR_SOCKET']; + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fetch(url, options); + } catch (error) { + lastError = error as Error; + const errorCode = (error as any)?.cause?.code || ''; + const errorMessage = (error as Error).message || ''; + + const isRetryable = retryableErrors.some(code => + errorCode.includes(code) || errorMessage.includes(code) + ); + + if (!isRetryable || attempt === maxRetries) { + throw error; + } + + // Exponential backoff with jitter + const delay = Math.min( + initialDelayMs * Math.pow(2, attempt) + Math.random() * 50, + maxDelayMs + ); + + logger.debug('SYSTEM', 'Fetch failed, retrying', { + attempt: attempt + 1, + maxRetries, + delay: Math.round(delay), + error: errorMessage + }); + + await new Promise(r => setTimeout(r, delay)); + } + } + + throw lastError!; +} + /** * Check if worker is responsive and fully initialized by trying the readiness endpoint * Changed from /health to /api/readiness to ensure MCP initialization is complete From 72465efc8b2ef6aa11fd9e9f05df38a33fad373b Mon Sep 17 00:00:00 2001 From: Nawapat Buakoet Date: Sat, 3 Jan 2026 15:05:01 +0700 Subject: [PATCH 2/2] refactor: address PR review feedback on fetchWithRetry - Remove unused lastError variable - Clarify documentation: "4 total attempts (1 initial + 3 retries)" - Add comment explaining unreachable throw for TypeScript --- plugin/scripts/context-hook.js | 18 +++++++++--------- plugin/scripts/new-hook.js | 12 ++++++------ plugin/scripts/save-hook.js | 18 +++++++++--------- plugin/scripts/summary-hook.js | 16 ++++++++-------- plugin/scripts/user-message-hook.js | 16 ++++++++-------- src/shared/worker-utils.ts | 14 +++++++++----- 6 files changed, 49 insertions(+), 45 deletions(-) diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index 6737c1d6..2cd788fb 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{stdin as D}from"process";import m from"path";import{homedir as X}from"os";import{readFileSync as j}from"fs";import{readFileSync as w,writeFileSync as F,existsSync as x}from"fs";import{join as b}from"path";import{homedir as W}from"os";var d="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:b(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:U,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 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){try{if(!x(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{F(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as H,existsSync as G,mkdirSync as K}from"fs";import{join as p}from"path";var C=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(C||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=p(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=C[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${l}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),C=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=l.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"logs");H(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=f(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=l.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"settings.json"),n=l.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=p[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),g=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${g}`}log(t,r,e,n,o){if(t0&&(u=` {${Object.entries(R).map(([$,v])=>`${$}=${v}`).join(", ")}}`)}let f=`[${E}] [${i}] [${l}] ${a}${e}${u}${g}`;if(this.logFilePath)try{H(this.logFilePath,f+` -`,"utf8")}catch(M){process.stderr.write(`[LOGGER] Failed to write to log file: ${M} -`)}else process.stderr.write(f+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},_=new A;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function N(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} +${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` +`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let M="";if(n){let{sessionId:u,memorySessionId:Q,correlationId:Z,...D}=n;Object.keys(D).length>0&&(M=` {${Object.entries(D).map(([k,$])=>`${k}=${$}`).join(", ")}}`)}let T=`[${E}] [${i}] [${g}] ${_}${e}${M}${c}`;if(this.logFilePath)try{W(this.logFilePath,T+` +`,"utf8")}catch(u){process.stderr.write(`[LOGGER] Failed to write to log file: ${u} +`)}else process.stderr.write(T+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",M={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,M,n),o}},a=new C;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function U(s){return process.platform==="win32"?Math.round(s*A.WINDOWS_MULTIPLIER):s}function h(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?g=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var V=m.join(X(),".claude","plugins","marketplaces","thedotmack"),At=h(L.HEALTH_CHECK),S=null;function O(){if(S!==null)return S;let s=m.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function I(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let l=0;l<=e;l++)try{return await fetch(s,t)}catch(a){i=a;let g=a?.cause?.code||"",u=a.message||"";if(!E.some(T=>g.includes(T)||u.includes(T))||l===e)throw a;let M=Math.min(n*Math.pow(2,l)+Math.random()*50,o);_.debug("SYSTEM","Fetch failed, retrying",{attempt:l+1,maxRetries:e,delay:Math.round(M),error:u}),await new Promise(T=>setTimeout(T,M))}throw i}async function Y(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=m.join(V,"package.json");return JSON.parse(j(s,"utf-8")).version}async function J(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&_.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function y(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){_.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:O(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function P(s){if(!s||s.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=z.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function k(s){await y();let t=s?.cwd??process.cwd(),r=P(t),n=`http://127.0.0.1:${O()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await I(n,{});if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var Q=process.argv.includes("--colors");if(D.isTTY||Q)k(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";D.on("data",t=>s+=t),D.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await k(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})} +${i}`),i}var j=L.join(K(),".claude","plugins","marketplaces","thedotmack"),At=U(A.HEALTH_CHECK),S=null;function O(){if(S!==null)return S;let s=L.join(l.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=l.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function N(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"];for(let i=0;i<=e;i++)try{return await fetch(s,t)}catch(g){let _=g?.cause?.code||"",c=g.message||"";if(!E.some(u=>_.includes(u)||c.includes(u))||i===e)throw g;let T=Math.min(n*Math.pow(2,i)+Math.random()*50,o);a.debug("SYSTEM","Fetch failed, retrying",{attempt:i+1,maxRetries:e,delay:Math.round(T),error:c}),await new Promise(u=>setTimeout(u,T))}throw new Error("Unexpected: exhausted retries without throwing")}async function V(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function Y(){let s=L.join(j,"package.json");return JSON.parse(X(s,"utf-8")).version}async function B(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=Y(),t=await B();s!==t&&a.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function I(){for(let r=0;r<75;r++){try{if(await V()){await J();return}}catch(e){a.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:O(),customPrefix:"Worker did not become ready within 15 seconds."}))}import q from"path";function y(s){if(!s||s.trim()==="")return a.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=q.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return a.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return a.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function P(s){await I();let t=s?.cwd??process.cwd(),r=y(t),n=`http://127.0.0.1:${O()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await N(n,{});if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var z=process.argv.includes("--colors");if(m.isTTY||z)P(void 0).then(s=>{console.log(s),process.exit(0)});else{let s="";m.on("data",t=>s+=t),m.on("end",async()=>{let t;try{t=s.trim()?JSON.parse(s):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await P(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 e7dce5af..5b1fe426 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{stdin as $}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import D from"path";import{homedir as j}from"os";import{readFileSync as V}from"fs";import{readFileSync as b,writeFileSync as F,existsSync as W}from"fs";import{join as H}from"path";import{homedir as x}from"os";var U="bugfix,feature,refactor,discovery,decision,change",N="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var u=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:H(x(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:U,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 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){try{if(!W(t))return this.getAllDefaults();let r=b(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{F(t,JSON.stringify(n,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as K,existsSync as G,mkdirSync as X}from"fs";import{join as m}from"path";var A=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),C=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=u.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"logs");G(r)||X(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=m(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=u.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"settings.json"),n=u.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${i}.${l}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(m||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"logs");K(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=f(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=m[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${i}.${l}`}log(t,r,e,n,o){if(t0&&(g=` {${Object.entries(h).map(([w,v])=>`${w}=${v}`).join(", ")}}`)}let M=`[${a}] [${i}] [${l}] ${_}${e}${g}${c}`;if(this.logFilePath)try{K(this.logFilePath,M+` +`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let u="";if(n){let{sessionId:p,memorySessionId:Z,correlationId:tt,...R}=n;Object.keys(R).length>0&&(u=` {${Object.entries(R).map(([$,w])=>`${$}=${w}`).join(", ")}}`)}let O=`[${a}] [${i}] [${l}] ${_}${e}${u}${c}`;if(this.logFilePath)try{x(this.logFilePath,O+` `,"utf8")}catch(p){process.stderr.write(`[LOGGER] Failed to write to log file: ${p} -`)}else process.stderr.write(M+` +`)}else process.stderr.write(O+` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",g={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,g,n),o}},E=new C;var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function y(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",i=`${o}${a} +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},E=new A;var C={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function N(s){return process.platform==="win32"?Math.round(s*C.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",i=`${o}${a} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var Y=D.join(j(),".claude","plugins","marketplaces","thedotmack"),Lt=I(L.HEALTH_CHECK),T=null;function S(){if(T!==null)return T;let s=D.join(u.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=u.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function d(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,a=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let l=0;l<=e;l++)try{return await fetch(s,t)}catch(_){i=_;let c=_?.cause?.code||"",g=_.message||"";if(!a.some(O=>c.includes(O)||g.includes(O))||l===e)throw _;let p=Math.min(n*Math.pow(2,l)+Math.random()*50,o);E.debug("SYSTEM","Fetch failed, retrying",{attempt:l+1,maxRetries:e,delay:Math.round(p),error:g}),await new Promise(O=>setTimeout(O,p))}throw i}async function B(){let s=S();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function J(){let s=D.join(Y,"package.json");return JSON.parse(V(s,"utf-8")).version}async function q(){let s=S(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let s=J(),t=await q();s!==t&&E.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function P(){for(let r=0;r<75;r++){try{if(await B()){await z();return}}catch(e){E.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(y({port:S(),customPrefix:"Worker did not become ready within 15 seconds."}))}import Q from"path";function k(s){if(!s||s.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=Q.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function Z(s){if(await P(),!s)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=s,n=k(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=S();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await d(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let i=await a.json(),l=i.sessionDbId,_=i.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:i.skipped}),i.skipped&&i.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(f);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let g=await d(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_})});if(!g.ok)throw new Error(`SDK agent start failed: ${g.status}`);console.log(f)}var R="";$.on("data",s=>R+=s);$.on("end",async()=>{let s;try{s=R?JSON.parse(R):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Z(s)}); +${i}`),i}var V=L.join(X(),".claude","plugins","marketplaces","thedotmack"),Lt=N(C.HEALTH_CHECK),T=null;function S(){if(T!==null)return T;let s=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function D(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,a=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"];for(let i=0;i<=e;i++)try{return await fetch(s,t)}catch(l){let _=l?.cause?.code||"",c=l.message||"";if(!a.some(p=>_.includes(p)||c.includes(p))||i===e)throw l;let O=Math.min(n*Math.pow(2,i)+Math.random()*50,o);E.debug("SYSTEM","Fetch failed, retrying",{attempt:i+1,maxRetries:e,delay:Math.round(O),error:c}),await new Promise(p=>setTimeout(p,O))}throw new Error("Unexpected: exhausted retries without throwing")}async function Y(){let s=S();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=L.join(V,"package.json");return JSON.parse(j(s,"utf-8")).version}async function J(){let s=S(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&E.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function y(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){E.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:S(),customPrefix:"Worker did not become ready within 15 seconds."}))}import z from"path";function P(s){if(!s||s.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:s}),"unknown-project";let t=z.basename(s);if(t===""){if(process.platform==="win32"){let e=s.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:s,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:s}),"unknown-project"}return t}async function Q(s){if(await y(),!s)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=s,n=P(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=S();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await D(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e})});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let i=await a.json(),l=i.sessionDbId,_=i.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:i.skipped}),i.skipped&&i.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(M);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let u=await D(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_})});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);console.log(M)}var d="";k.on("data",s=>d+=s);k.on("end",async()=>{let s;try{s=d?JSON.parse(d):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(s)}); diff --git a/plugin/scripts/save-hook.js b/plugin/scripts/save-hook.js index 0a47011c..49f36c04 100755 --- a/plugin/scripts/save-hook.js +++ b/plugin/scripts/save-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{stdin as $}from"process";var U=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as w,writeFileSync as F,existsSync as b}from"fs";import{join as W}from"path";import{homedir as H}from"os";var d="bugfix,feature,refactor,discovery,decision,change",h="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:W(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:h,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 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){try{if(!b(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{F(t,JSON.stringify(n,null,2),"utf-8"),l.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){l.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return l.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as x,existsSync as G,mkdirSync as K}from"fs";import{join as p}from"path";var A=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),L=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=p(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"logs");H(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=f(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=f(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=p[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t0&&(u=` {${Object.entries(R).map(([k,v])=>`${k}=${v}`).join(", ")}}`)}let f=`[${E}] [${i}] [${a}] ${_}${e}${u}${g}`;if(this.logFilePath)try{x(this.logFilePath,f+` -`,"utf8")}catch(T){process.stderr.write(`[LOGGER] Failed to write to log file: ${T} -`)}else process.stderr.write(f+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},l=new L;import D from"path";import{homedir as X}from"os";import{readFileSync as V}from"fs";var C={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function I(s){return process.platform==="win32"?Math.round(s*C.WINDOWS_MULTIPLIER):s}function N(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} +${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` +`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let T="";if(n){let{sessionId:u,memorySessionId:z,correlationId:Q,...m}=n;Object.keys(m).length>0&&(T=` {${Object.entries(m).map(([$,k])=>`${$}=${k}`).join(", ")}}`)}let O=`[${E}] [${i}] [${_}] ${l}${e}${T}${c}`;if(this.logFilePath)try{x(this.logFilePath,O+` +`,"utf8")}catch(u){process.stderr.write(`[LOGGER] Failed to write to log file: ${u} +`)}else process.stderr.write(O+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",T={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,T,n),o}},a=new A;import C from"path";import{homedir as K}from"os";import{readFileSync as X}from"fs";var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?g=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var j=D.join(X(),".claude","plugins","marketplaces","thedotmack"),Lt=I(C.HEALTH_CHECK),S=null;function M(){if(S!==null)return S;let s=D.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function y(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let a=0;a<=e;a++)try{return await fetch(s,t)}catch(_){i=_;let g=_?.cause?.code||"",u=_.message||"";if(!E.some(O=>g.includes(O)||u.includes(O))||a===e)throw _;let T=Math.min(n*Math.pow(2,a)+Math.random()*50,o);l.debug("SYSTEM","Fetch failed, retrying",{attempt:a+1,maxRetries:e,delay:Math.round(T),error:u}),await new Promise(O=>setTimeout(O,T))}throw i}async function Y(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=D.join(j,"package.json");return JSON.parse(V(s,"utf-8")).version}async function J(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&l.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function P(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function z(s){if(await P(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,E=M(),i=l.formatTool(e,n);if(l.dataIn("HOOK",`PostToolUse: ${i}`,{workerPort:E}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let a=await y(`http://127.0.0.1:${E}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!a.ok)throw new Error(`Observation storage failed: ${a.status}`);l.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var m="";$.on("data",s=>m+=s);$.on("end",async()=>{let s;try{s=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await z(s)}); +${i}`),i}var V=C.join(K(),".claude","plugins","marketplaces","thedotmack"),Lt=h(L.HEALTH_CHECK),S=null;function M(){if(S!==null)return S;let s=C.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function N(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"];for(let i=0;i<=e;i++)try{return await fetch(s,t)}catch(_){let l=_?.cause?.code||"",c=_.message||"";if(!E.some(u=>l.includes(u)||c.includes(u))||i===e)throw _;let O=Math.min(n*Math.pow(2,i)+Math.random()*50,o);a.debug("SYSTEM","Fetch failed, retrying",{attempt:i+1,maxRetries:e,delay:Math.round(O),error:c}),await new Promise(u=>setTimeout(u,O))}throw new Error("Unexpected: exhausted retries without throwing")}async function j(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function Y(){let s=C.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function B(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=Y(),t=await B();s!==t&&a.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function y(){for(let r=0;r<75;r++){try{if(await j()){await J();return}}catch(e){a.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}async function q(s){if(await y(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,E=M(),i=a.formatTool(e,n);if(a.dataIn("HOOK",`PostToolUse: ${i}`,{workerPort:E}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let _=await N(`http://127.0.0.1:${E}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r})});if(!_.ok)throw new Error(`Observation storage failed: ${_.status}`);a.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(R)}var D="";P.on("data",s=>D+=s);P.on("end",async()=>{let s;try{s=D?JSON.parse(D):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(s)}); diff --git a/plugin/scripts/summary-hook.js b/plugin/scripts/summary-hook.js index f8ba1ec8..a8b81e46 100755 --- a/plugin/scripts/summary-hook.js +++ b/plugin/scripts/summary-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{stdin as P}from"process";var p=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as F,writeFileSync as x,existsSync as b}from"fs";import{join as W}from"path";import{homedir as H}from"os";var h="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:W(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:h,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,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 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){try{if(!b(t))return this.getAllDefaults();let r=F(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{x(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as G,existsSync as K,mkdirSync as X}from"fs";import{join as m}from"path";var A=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(A||{}),L=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"logs");K(r)||X(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=m(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=m(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=A[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(m||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=p(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=p(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=m[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${a}`}log(t,r,e,n,o){if(t0&&(u=` {${Object.entries(d).map(([w,v])=>`${w}=${v}`).join(", ")}}`)}let T=`[${E}] [${i}] [${a}] ${_}${e}${u}${l}`;if(this.logFilePath)try{G(this.logFilePath,T+` -`,"utf8")}catch(S){process.stderr.write(`[LOGGER] Failed to write to log file: ${S} -`)}else process.stderr.write(T+` +`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let S="";if(n){let{sessionId:u,memorySessionId:tt,correlationId:et,...R}=n;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([P,w])=>`${P}=${w}`).join(", ")}}`)}let f=`[${E}] [${i}] [${a}] ${_}${e}${S}${l}`;if(this.logFilePath)try{H(this.logFilePath,f+` +`,"utf8")}catch(u){process.stderr.write(`[LOGGER] Failed to write to log file: ${u} +`)}else process.stderr.write(f+` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},c=new L;import D from"path";import{homedir as V}from"os";import{readFileSync as j}from"fs";var C={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function y(s){return process.platform==="win32"?Math.round(s*C.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",S={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,S,n),o}},c=new A;import C from"path";import{homedir as X}from"os";import{readFileSync as V}from"fs";var L={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function U(s){return process.platform==="win32"?Math.round(s*L.WINDOWS_MULTIPLIER):s}function y(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,8 +16,8 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var Y=D.join(V(),".claude","plugins","marketplaces","thedotmack"),Dt=y(C.HEALTH_CHECK),O=null;function M(){if(O!==null)return O;let s=D.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function N(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"],i=null;for(let a=0;a<=e;a++)try{return await fetch(s,t)}catch(_){i=_;let l=_?.cause?.code||"",u=_.message||"";if(!E.some(f=>l.includes(f)||u.includes(f))||a===e)throw _;let S=Math.min(n*Math.pow(2,a)+Math.random()*50,o);c.debug("SYSTEM","Fetch failed, retrying",{attempt:a+1,maxRetries:e,delay:Math.round(S),error:u}),await new Promise(f=>setTimeout(f,S))}throw i}async function B(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function J(){let s=D.join(Y,"package.json");return JSON.parse(j(s,"utf-8")).version}async function q(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let s=J(),t=await q();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function $(){for(let r=0;r<75;r++){try{if(await B()){await z();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as Q,existsSync as Z}from"fs";function k(s,t,r=!1){if(!s||!Z(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=Q(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(` +${i}`),i}var j=C.join(X(),".claude","plugins","marketplaces","thedotmack"),Dt=U(L.HEALTH_CHECK),O=null;function M(){if(O!==null)return O;let s=C.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function I(s,t,r={}){let{maxRetries:e=3,initialDelayMs:n=100,maxDelayMs:o=1e3}=r,E=["ECONNRESET","ECONNREFUSED","ETIMEDOUT","UND_ERR_SOCKET"];for(let i=0;i<=e;i++)try{return await fetch(s,t)}catch(a){let _=a?.cause?.code||"",l=a.message||"";if(!E.some(u=>_.includes(u)||l.includes(u))||i===e)throw a;let f=Math.min(n*Math.pow(2,i)+Math.random()*50,o);c.debug("SYSTEM","Fetch failed, retrying",{attempt:i+1,maxRetries:e,delay:Math.round(f),error:l}),await new Promise(u=>setTimeout(u,f))}throw new Error("Unexpected: exhausted retries without throwing")}async function Y(){let s=M();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function B(){let s=C.join(j,"package.json");return JSON.parse(V(s,"utf-8")).version}async function J(){let s=M(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function q(){let s=B(),t=await J();s!==t&&c.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await Y()){await q();return}}catch(e){c.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(y({port:M(),customPrefix:"Worker did not become ready within 15 seconds."}))}import{readFileSync as z,existsSync as Q}from"fs";function $(s,t,r=!1){if(!s||!Q(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=z(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(` `),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let a="",_=i.message.content;if(typeof _=="string")a=_;else if(Array.isArray(_))a=_.filter(l=>l.type==="text").map(l=>l.text).join(` `);else throw new Error(`Unknown message content format in transcript. Type: ${typeof _}`);return r&&(a=a.replace(/[\s\S]*?<\/system-reminder>/g,""),a=a.replace(/\n{3,}/g,` -`).trim()),a}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function tt(s){if(await $(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=M();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=k(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await N(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(p),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(p)}var R="";P.on("data",s=>R+=s);P.on("end",async()=>{let s;try{s=R?JSON.parse(R):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await tt(s)}); +`).trim()),a}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Z(s){if(await N(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=M();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=$(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastAssistantMessage:!!e});let n=await I(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_assistant_message:e})});if(!n.ok)throw console.log(T),new Error(`Summary generation failed: ${n.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(T)}var D="";k.on("data",s=>D+=s);k.on("end",async()=>{let s;try{s=D?JSON.parse(D):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Z(s)}); diff --git a/plugin/scripts/user-message-hook.js b/plugin/scripts/user-message-hook.js index ad1f3349..a6b29429 100755 --- a/plugin/scripts/user-message-hook.js +++ b/plugin/scripts/user-message-hook.js @@ -1,13 +1,13 @@ #!/usr/bin/env bun -import{basename as J}from"path";import L from"path";import{homedir as H}from"os";import{readFileSync as K}from"fs";import{readFileSync as k,writeFileSync as v,existsSync as F}from"fs";import{join as w}from"path";import{homedir as W}from"os";var U="bugfix,feature,refactor,discovery,decision,change",R="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:w(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",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:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:R,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 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){try{if(!F(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),l.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){l.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return l.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as b,existsSync as x,mkdirSync as G}from"fs";import{join as O}from"path";var S=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");x(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=_.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=_.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${T}`}log(t,r,e,n,o){if(t(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(S||{}),f=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"logs");W(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=O(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=a.get("CLAUDE_MEM_DATA_DIR"),r=O(t,"settings.json"),n=a.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=S[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}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.getLevel()===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),u=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${u}`}log(t,r,e,n,o){if(t0&&(u=` {${Object.entries(m).map(([$,P])=>`${$}=${P}`).join(", ")}}`)}let C=`[${E}] [${i}] [${T}] ${a}${e}${u}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+` +`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let M="";if(n){let{sessionId:D,memorySessionId:Q,correlationId:Z,...m}=n;Object.keys(m).length>0&&(M=` {${Object.entries(m).map(([$,P])=>`${$}=${P}`).join(", ")}}`)}let A=`[${E}] [${i}] [${u}] ${_}${e}${M}${c}`;if(this.logFilePath)try{x(this.logFilePath,A+` `,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} -`)}else process.stderr.write(C+` -`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},l=new f;var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function I(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function d(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} +`)}else process.stderr.write(A+` +`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",M={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,M,n),o}},l=new f;var p={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function d(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E} `;return i+=`To restart the worker: `,i+=`1. Exit Claude Code completely @@ -16,12 +16,12 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n} -${i}`),i}var X=L.join(H(),".claude","plugins","marketplaces","thedotmack"),At=I(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=L.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function V(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=L.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function Y(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function B(){let s=j(),t=await Y();s!==t&&l.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await B();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(d({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}await N();var y=g(),z=J(process.cwd()),A=await fetch(`http://127.0.0.1:${y}/api/context/inject?project=${encodeURIComponent(z)}&colors=true`,{method:"GET"});if(!A.ok)throw new Error(`Failed to fetch context: ${A.status}`);var q=await A.text();console.error(` +${i}`),i}var X=L.join(H(),".claude","plugins","marketplaces","thedotmack"),Ct=d(p.HEALTH_CHECK),T=null;function g(){if(T!==null)return T;let s=L.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function V(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`)).ok}function j(){let s=L.join(X,"package.json");return JSON.parse(K(s,"utf-8")).version}async function Y(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`);if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function B(){let s=j(),t=await Y();s!==t&&l.debug("SYSTEM","Version check",{pluginVersion:s,workerVersion:t,note:"Mismatch will be auto-restarted by worker-service start command"})}async function N(){for(let r=0;r<75;r++){try{if(await V()){await B();return}}catch(e){l.debug("SYSTEM","Worker health check failed, will retry",{attempt:r+1,maxRetries:75,error:e instanceof Error?e.message:String(e)})}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:g(),customPrefix:"Worker did not become ready within 15 seconds."}))}await N();var y=g(),q=J(process.cwd()),C=await fetch(`http://127.0.0.1:${y}/api/context/inject?project=${encodeURIComponent(q)}&colors=true`,{method:"GET"});if(!C.ok)throw new Error(`Failed to fetch context: ${C.status}`);var z=await C.text();console.error(` \u{1F4DD} Claude-Mem Context Loaded \u2139\uFE0F Note: This appears as stderr but is informational only -`+q+` +`+z+` \u{1F4A1} New! Wrap all or part of any message with ... to prevent storing sensitive information in your observation history. diff --git a/src/shared/worker-utils.ts b/src/shared/worker-utils.ts index 62d92763..75ed2569 100644 --- a/src/shared/worker-utils.ts +++ b/src/shared/worker-utils.ts @@ -57,8 +57,12 @@ export function clearPortCache(): void { } /** - * Fetch with retry logic for transient network errors - * Handles ECONNRESET, ECONNREFUSED, ETIMEDOUT, and socket errors + * Fetch with retry logic for transient network errors. + * Handles ECONNRESET, ECONNREFUSED, ETIMEDOUT, and socket errors. + * + * With default maxRetries=3, performs up to 4 total attempts: + * - Initial attempt + * - Up to 3 retries with exponential backoff (100ms, 200ms, 400ms) */ export async function fetchWithRetry( url: string, @@ -76,13 +80,11 @@ export async function fetchWithRetry( } = config; const retryableErrors = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'UND_ERR_SOCKET']; - let lastError: Error | null = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fetch(url, options); } catch (error) { - lastError = error as Error; const errorCode = (error as any)?.cause?.code || ''; const errorMessage = (error as Error).message || ''; @@ -90,6 +92,7 @@ export async function fetchWithRetry( errorCode.includes(code) || errorMessage.includes(code) ); + // Throw immediately for non-retryable errors or if we've exhausted retries if (!isRetryable || attempt === maxRetries) { throw error; } @@ -111,7 +114,8 @@ export async function fetchWithRetry( } } - throw lastError!; + // This line is unreachable but satisfies TypeScript's control flow analysis + throw new Error('Unexpected: exhausted retries without throwing'); } /**