From 1f2e5f1a9c85a9ec652567d3dbbd7f918de959e4 Mon Sep 17 00:00:00 2001 From: kat-bell <224458406+kat-bell@users.noreply.github.com> Date: Tue, 9 Dec 2025 05:00:44 -0600 Subject: [PATCH 1/2] fix(windows): Comprehensive fixes for Windows plugin installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR addresses issue #193 affecting Windows installations of claude-mem. ## Bug 1: Missing ecosystem.config.cjs in packaged plugin **Problem**: The ecosystem.config.cjs file was not included in the plugin package, causing PM2 to fail when trying to start the worker from cache. **Fix**: Added `plugin/ecosystem.config.cjs` with correct path for packaged structure (`./scripts/worker-service.cjs` instead of `./plugin/scripts/`). ## Bug 2: Incorrect MCP Server Path (src/services/worker-service.ts) **Problem**: Path `__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs'` only worked in dev structure, failed in packaged plugin. **Error produced**: ``` Error: Cannot find module 'C:\Users\...\claude-mem\plugin\scripts\mcp-server.cjs' [ERROR] [SYSTEM] Background initialization failed MCP error -32000: Connection closed ``` **Fix**: Changed to `path.join(__dirname, 'mcp-server.cjs')` since mcp-server.cjs is in the same directory as worker-service.cjs after bundling. ## Bug 3: Missing smart-install.js in plugin package **Problem**: smart-install.js was referenced in hooks.json but not included in the plugin/ directory for cache deployment. **Fix**: Added `plugin/scripts/smart-install.js` that uses `createRequire()` to resolve modules from MARKETPLACE_ROOT. ## Bug 4: hooks.json incorrect path **Problem**: Referenced `/../scripts/smart-install.js` but CLAUDE_PLUGIN_ROOT points to the plugin/ directory. **Fix**: Changed to `/scripts/smart-install.js`. ## Bug 5: Windows Worker Startup - Visible Console Windows **Problem**: PM2 ignores windowsHide option on Windows, opening visible console windows when starting the worker service. **Fix**: Use PowerShell `Start-Process -WindowStyle Hidden` on Windows while keeping PM2 for Unix systems (src/shared/worker-utils.ts). ## Additional Improvements - Increased worker startup timeouts for Windows (500ms health check, 1000ms wait between retries, 15 retries = 15s total vs previous 5s) - Added `windowsHide: true` to root ecosystem.config.cjs for PM2 ## Note on Assertion Failure The Windows libuv assertion failure `!(handle->flags & UV_HANDLE_CLOSING)` at `src\win\async.c:76` is a known upstream issue in Claude Code (Issue #7579), triggered by fetch() calls on Windows. This is NOT caused by worker spawning and cannot be fixed in claude-mem. Tested on Windows 11 with Node.js v24. Fixes #193 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- ecosystem.config.cjs | 2 + plugin/ecosystem.config.cjs | 43 +++ plugin/hooks/hooks.json | 2 +- plugin/scripts/cleanup-hook.js | 12 +- plugin/scripts/context-hook.js | 6 +- plugin/scripts/new-hook.js | 20 +- plugin/scripts/save-hook.js | 10 +- plugin/scripts/smart-install.js | 389 ++++++++++++++++++++++++++++ plugin/scripts/summary-hook.js | 18 +- plugin/scripts/user-message-hook.js | 14 +- plugin/scripts/worker-service.cjs | 2 +- scripts/smart-install.js | 58 ++--- src/services/worker-service.ts | 2 +- src/shared/worker-utils.ts | 72 +++-- 14 files changed, 543 insertions(+), 107 deletions(-) create mode 100644 plugin/ecosystem.config.cjs create mode 100644 plugin/scripts/smart-install.js diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index a1e36daf..8af7129a 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -14,6 +14,8 @@ module.exports = { { name: 'claude-mem-worker', script: './plugin/scripts/worker-service.cjs', + // Windows: prevent visible console windows + windowsHide: true, // INTENTIONAL: Watch mode enables auto-restart on plugin updates // // Why this is enabled: diff --git a/plugin/ecosystem.config.cjs b/plugin/ecosystem.config.cjs new file mode 100644 index 00000000..1204c688 --- /dev/null +++ b/plugin/ecosystem.config.cjs @@ -0,0 +1,43 @@ +/** + * PM2 Ecosystem Configuration for claude-mem Worker Service (Packaged Plugin) + * + * NOTE: This config is for the packaged/cache version of the plugin. + * The script path is relative to the cache directory structure. + * + * Usage: + * pm2 start ecosystem.config.cjs + * pm2 stop claude-mem-worker + * pm2 restart claude-mem-worker + * pm2 logs claude-mem-worker + * pm2 status + */ + +module.exports = { + apps: [ + { + name: 'claude-mem-worker', + // Packaged structure: cache/thedotmack/claude-mem/X.X.X/scripts/worker-service.cjs + script: './scripts/worker-service.cjs', + // Windows: prevent visible console windows + windowsHide: true, + // INTENTIONAL: Watch mode enables auto-restart on plugin updates + // + // Why this is enabled: + // - When plugin updates, files change + // - Watch mode detects these changes and auto-restarts the worker + // - Users get the latest code without manually running `pm2 restart` + // + // This is a feature, not a bug - it ensures users always run the + // latest version after plugin updates. + watch: true, + ignore_watch: [ + 'node_modules', + 'logs', + '*.log', + '*.db', + '*.db-*', + '.git' + ] + } + ] +}; diff --git a/plugin/hooks/hooks.json b/plugin/hooks/hooks.json index 99a3a8e0..f183eb99 100644 --- a/plugin/hooks/hooks.json +++ b/plugin/hooks/hooks.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js", "timeout": 300 }, { diff --git a/plugin/scripts/cleanup-hook.js b/plugin/scripts/cleanup-hook.js index 25665f3b..dc616196 100755 --- a/plugin/scripts/cleanup-hook.js +++ b/plugin/scripts/cleanup-hook.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -import{stdin as u}from"process";import T from"path";import{existsSync as f}from"fs";import{homedir as d}from"os";import{spawnSync as U}from"child_process";import{readFileSync as D,existsSync as h}from"fs";var N=["bugfix","feature","refactor","discovery","decision","change"],L=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=N.join(","),g=L.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:g,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let n=this.get(t);return parseInt(n,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!h(t))return this.getAllDefaults();let n=D(t,"utf-8"),o=JSON.parse(n).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))o[i]!==void 0&&(s[i]=o[i]);return s}};var E=T.join(d(),".claude","plugins","marketplaces","thedotmack"),y=100,R=500,I=10;function l(){let e=T.join(d(),".claude-mem","settings.json"),t=_.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function A(){try{let e=l();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(y)})).ok}catch{return!1}}async function P(){try{let e=T.join(E,"ecosystem.config.cjs");if(!f(e))throw new Error(`Ecosystem config not found at ${e}`);let t=T.join(E,"node_modules",".bin","pm2"),n=process.platform==="win32"?t+".cmd":t,r=f(n)?n:"pm2",o=U(r,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed");for(let s=0;ssetTimeout(i,R)),await A())return!0;return!1}catch{return!1}}async function C(){if(await A())return;if(!await P()){let t=l();throw new Error(`Worker service failed to start on port ${t}. +import{stdin as p}from"process";import a from"path";import{existsSync as O}from"fs";import{homedir as A}from"os";import{spawnSync as g}from"child_process";import{readFileSync as h,existsSync as y}from"fs";var L=["bugfix","feature","refactor","discovery","decision","change"],D=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var d=L.join(","),f=D.join(",");var T=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:d,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:f,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let o=this.get(t);return parseInt(o,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!y(t))return this.getAllDefaults();let o=h(t,"utf-8"),r=JSON.parse(o).env||{},i={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))r[E]!==void 0&&(i[E]=r[E]);return i}};var n=a.join(A(),".claude","plugins","marketplaces","thedotmack"),U=500,R=1e3,w=15;function l(){let e=a.join(A(),".claude-mem","settings.json"),t=T.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=l();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(U)})).ok}catch{return!1}}async function I(){try{let e=a.join(n,"plugin","scripts","worker-service.cjs");if(!O(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=g("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${n}' -WindowStyle Hidden`],{cwd:n,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=a.join(n,"ecosystem.config.cjs");if(!O(t))throw new Error(`Ecosystem config not found at ${t}`);let o=a.join(n,"node_modules",".bin","pm2"),s=O(o)?o:"pm2",r=g(s,["start",t],{cwd:n,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;tsetTimeout(o,R)),await C())return!0;return!1}catch{return!1}}async function m(){if(await C())return;if(!await I()){let t=l();throw new Error(`Worker service failed to start on port ${t}. To start manually, run: - cd ${E} + cd ${n} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as k}from"fs";import{homedir as w}from"os";import{join as x}from"path";var W=x(w(),".claude-mem","silent.log");function a(e,t,n=""){let r=new Date().toISOString(),S=((new Error().stack||"").split(` -`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),M=S?`${S[1].split("/").pop()}:${S[2]}`:"unknown",c=`[${r}] [${M}] ${e}`;if(t!==void 0)try{c+=` ${JSON.stringify(t)}`}catch(O){c+=` [stringify error: ${O}]`}c+=` -`;try{k(W,c)}catch(O){console.error("[silent-debug] Failed to write to log:",O)}return n}async function m(e){await C(),a("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` -Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:t,reason:n}=e,r=l();try{let o=await fetch(`http://127.0.0.1:${r}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:n}),signal:AbortSignal.timeout(2e3)});if(o.ok){let s=await o.json();a("[cleanup-hook] Session cleanup completed",s)}else a("[cleanup-hook] Session not found or already cleaned up")}catch(o){a("[cleanup-hook] Worker not reachable (non-critical)",{error:o.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(u.isTTY)m(void 0);else{let e="";u.on("data",t=>e+=t),u.on("end",async()=>{let t=e?JSON.parse(e):void 0;await m(t)})} +If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as P}from"fs";import{homedir as k}from"os";import{join as W}from"path";var x=W(k(),".claude-mem","silent.log");function c(e,t,o=""){let s=new Date().toISOString(),S=((new Error().stack||"").split(` +`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),N=S?`${S[1].split("/").pop()}:${S[2]}`:"unknown",_=`[${s}] [${N}] ${e}`;if(t!==void 0)try{_+=` ${JSON.stringify(t)}`}catch(u){_+=` [stringify error: ${u}]`}_+=` +`;try{P(x,_)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return o}async function M(e){await m(),c("[cleanup-hook] Hook fired",{session_id:e?.session_id,cwd:e?.cwd,reason:e?.reason}),e||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(` +Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:t,reason:o}=e,s=l();try{let r=await fetch(`http://127.0.0.1:${s}/api/sessions/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,reason:o}),signal:AbortSignal.timeout(2e3)});if(r.ok){let i=await r.json();c("[cleanup-hook] Session cleanup completed",i)}else c("[cleanup-hook] Session not found or already cleaned up")}catch(r){c("[cleanup-hook] Worker not reachable (non-critical)",{error:r.message})}console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(p.isTTY)M(void 0);else{let e="";p.on("data",t=>e+=t),p.on("end",async()=>{let t=e?JSON.parse(e):void 0;await M(t)})} diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index 5353c5e6..7dd82942 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,8 +1,8 @@ #!/usr/bin/env node -import R from"path";import{stdin as T}from"process";import{execSync as y}from"child_process";import _ from"path";import{existsSync as u}from"fs";import{homedir as p}from"os";import{spawnSync as m}from"child_process";import{readFileSync as N,existsSync as g}from"fs";var M=["bugfix","feature","refactor","discovery","decision","change"],l=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var O=M.join(","),S=l.join(",");var i=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:O,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!g(t))return this.getAllDefaults();let r=N(t,"utf-8"),s=JSON.parse(r).env||{},n={...this.DEFAULTS};for(let o of Object.keys(this.DEFAULTS))s[o]!==void 0&&(n[o]=s[o]);return n}};var E=_.join(p(),".claude","plugins","marketplaces","thedotmack"),d=100,D=500,L=10;function a(){let e=_.join(p(),".claude-mem","settings.json"),t=i.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=a();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function U(){try{let e=_.join(E,"ecosystem.config.cjs");if(!u(e))throw new Error(`Ecosystem config not found at ${e}`);let t=_.join(E,"node_modules",".bin","pm2"),r=process.platform==="win32"?t+".cmd":t,c=u(r)?r:"pm2",s=m(c,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(s.status!==0)throw new Error(s.stderr||"PM2 start failed");for(let n=0;nsetTimeout(o,D)),await C())return!0;return!1}catch{return!1}}async function A(){if(await C())return;if(!await U()){let t=a();throw new Error(`Worker service failed to start on port ${t}. +import R from"path";import{stdin as S}from"process";import{execSync as y}from"child_process";import n from"path";import{existsSync as T}from"fs";import{homedir as f}from"os";import{spawnSync as p}from"child_process";import{readFileSync as N,existsSync as m}from"fs";var M=["bugfix","feature","refactor","discovery","decision","change"],g=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var O=M.join(","),u=g.join(",");var i=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:O,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS: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 process.env[t]||this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!m(t))return this.getAllDefaults();let r=N(t,"utf-8"),o=JSON.parse(r).env||{},a={...this.DEFAULTS};for(let c of Object.keys(this.DEFAULTS))o[c]!==void 0&&(a[c]=o[c]);return a}};var s=n.join(f(),".claude","plugins","marketplaces","thedotmack"),d=500,D=1e3,L=15;function E(){let e=n.join(f(),".claude-mem","settings.json"),t=i.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function l(){try{let e=E();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(d)})).ok}catch{return!1}}async function U(){try{let e=n.join(s,"plugin","scripts","worker-service.cjs");if(!T(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=p("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${s}' -WindowStyle Hidden`],{cwd:s,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=n.join(s,"ecosystem.config.cjs");if(!T(t))throw new Error(`Ecosystem config not found at ${t}`);let r=n.join(s,"node_modules",".bin","pm2"),_=T(r)?r:"pm2",o=p(_,["start",t],{cwd:s,stdio:"pipe",encoding:"utf-8"});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed")}for(let t=0;tsetTimeout(r,D)),await l())return!0;return!1}catch{return!1}}async function A(){if(await l())return;if(!await U()){let t=E();throw new Error(`Worker service failed to start on port ${t}. To start manually, run: - cd ${E} + cd ${s} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}async function f(e){await A();let t=e?.cwd??process.cwd(),r=t?R.basename(t):"unknown-project",s=`http://127.0.0.1:${a()}/api/context/inject?project=${encodeURIComponent(r)}`;return y(`curl -s "${s}"`,{encoding:"utf-8",timeout:5e3}).trim()}var I=process.argv.includes("--colors");if(T.isTTY||I)f(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";T.on("data",t=>e+=t),T.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,r=await f(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})} +If already running, try: npx pm2 restart claude-mem-worker`)}}async function C(e){await A();let t=e?.cwd??process.cwd(),r=t?R.basename(t):"unknown-project",o=`http://127.0.0.1:${E()}/api/context/inject?project=${encodeURIComponent(r)}`;return y(`curl -s "${o}"`,{encoding:"utf-8",timeout:5e3}).trim()}var h=process.argv.includes("--colors");if(S.isTTY||h)C(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";S.on("data",t=>e+=t),S.on("end",async()=>{let t=e.trim()?JSON.parse(e):void 0,r=await C(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 28adb6e4..507aaafd 100755 --- a/plugin/scripts/new-hook.js +++ b/plugin/scripts/new-hook.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import _e from"path";import{stdin as W}from"process";import q from"better-sqlite3";import{join as m,dirname as G,basename as le}from"path";import{homedir as v}from"os";import{existsSync as be,mkdirSync as Y}from"fs";import{fileURLToPath as V}from"url";function K(){return typeof __dirname<"u"?__dirname:G(V(import.meta.url))}var Oe=K(),l=process.env.CLAUDE_MEM_DATA_DIR||m(v(),".claude-mem"),I=process.env.CLAUDE_CONFIG_DIR||m(v(),".claude"),he=m(l,"archives"),Ne=m(l,"logs"),fe=m(l,"trash"),Ie=m(l,"backups"),Ae=m(l,"settings.json"),y=m(l,"claude-mem.db"),Le=m(l,"vector-db"),Ce=m(I,"settings.json"),De=m(I,"commands"),ve=m(I,"CLAUDE.md");function k(a){Y(a,{recursive:!0})}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;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=A[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} +import _e from"path";import{stdin as j}from"process";import J from"better-sqlite3";import{join as m,dirname as Y,basename as le}from"path";import{homedir as y}from"os";import{existsSync as be,mkdirSync as V}from"fs";import{fileURLToPath as K}from"url";function q(){return typeof __dirname<"u"?__dirname:Y(K(import.meta.url))}var Oe=q(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),I=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),he=m(l,"archives"),fe=m(l,"logs"),Ne=m(l,"trash"),Ie=m(l,"backups"),Ae=m(l,"settings.json"),k=m(l,"claude-mem.db"),Le=m(l,"vector-db"),Ce=m(I,"settings.json"),De=m(I,"commands"),ve=m(I,"CLAUDE.md");function x(a){V(a,{recursive:!0})}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;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=A[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message} ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e0&&(_=` {${Object.entries(c).map(([j,$])=>`${j}=${$}`).join(", ")}}`)}let T=`[${n}] [${i}] [${p}] ${d}${t}${_}${E}`;e===3?console.error(T):console.log(T)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},M=new L;var R=class{db;constructor(){k(l),this.db=new q(y),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` +`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let _="";if(r){let{sessionId:S,sdkSessionId:R,correlationId:u,...c}=r;Object.keys(c).length>0&&(_=` {${Object.entries(c).map(([$,G])=>`${$}=${G}`).join(", ")}}`)}let T=`[${n}] [${i}] [${p}] ${d}${t}${_}${E}`;e===3?console.error(T):console.log(T)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},M=new L;var h=class{db;constructor(){x(l),this.db=new J(k),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(` CREATE TABLE IF NOT EXISTS schema_versions ( id INTEGER PRIMARY KEY, version INTEGER UNIQUE NOT NULL, @@ -383,25 +383,25 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje WHERE id <= ? ${n} ORDER BY id DESC LIMIT ? - `,b=` + `,R=` SELECT id, created_at_epoch FROM observations WHERE id >= ? ${n} ORDER BY id ASC LIMIT ? - `;try{let u=this.db.prepare(S).all(e,...i,t+1),c=this.db.prepare(b).all(e,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let S=` + `;try{let u=this.db.prepare(S).all(e,...i,t+1),c=this.db.prepare(R).all(e,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary observations:",u.message),{observations:[],sessions:[],prompts:[]}}}else{let S=` SELECT created_at_epoch FROM observations WHERE created_at_epoch <= ? ${n} ORDER BY created_at_epoch DESC LIMIT ? - `,b=` + `,R=` SELECT created_at_epoch FROM observations WHERE created_at_epoch >= ? ${n} ORDER BY created_at_epoch ASC LIMIT ? - `;try{let u=this.db.prepare(S).all(s,...i,t),c=this.db.prepare(b).all(s,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let E=` + `;try{let u=this.db.prepare(S).all(s,...i,t),c=this.db.prepare(R).all(s,...i,r+1);if(u.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};p=u.length>0?u[u.length-1].created_at_epoch:s,d=c.length>0?c[c.length-1].created_at_epoch:s}catch(u){return console.error("[SessionStore] Error getting boundary timestamps:",u.message),{observations:[],sessions:[],prompts:[]}}}let E=` SELECT * FROM observations WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${n} @@ -417,12 +417,12 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${n.replace("project","s.project")} ORDER BY up.created_at_epoch ASC - `;try{let S=this.db.prepare(E).all(p,d,...i),b=this.db.prepare(_).all(p,d,...i),u=this.db.prepare(T).all(p,d,...i);return{observations:S,sessions:b.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:u.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(S){return console.error("[SessionStore] Error querying timeline records:",S.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function J(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(a,e,s={}){let t=J(a,e,s);return JSON.stringify(t)}import N from"path";import{existsSync as w}from"fs";import{homedir as F}from"os";import{spawnSync as se}from"child_process";import{readFileSync as Z,existsSync as ee}from"fs";var Q=["bugfix","feature","refactor","discovery","decision","change"],z=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var U=Q.join(","),x=z.join(",");var O=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:x,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(e){return process.env[e]||this.DEFAULTS[e]}static getInt(e){let s=this.get(e);return parseInt(s,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){if(!ee(e))return this.getAllDefaults();let s=Z(e,"utf-8"),r=JSON.parse(s).env||{},o={...this.DEFAULTS};for(let n of Object.keys(this.DEFAULTS))r[n]!==void 0&&(o[n]=r[n]);return o}};var h=N.join(F(),".claude","plugins","marketplaces","thedotmack"),te=100,re=500,oe=10;function f(){let a=N.join(F(),".claude-mem","settings.json"),e=O.loadFromFile(a);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}async function X(){try{let a=f();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(te)})).ok}catch{return!1}}async function ne(){try{let a=N.join(h,"ecosystem.config.cjs");if(!w(a))throw new Error(`Ecosystem config not found at ${a}`);let e=N.join(h,"node_modules",".bin","pm2"),s=process.platform==="win32"?e+".cmd":e,t=w(s)?s:"pm2",r=se(t,["start",a],{cwd:h,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed");for(let o=0;osetTimeout(n,re)),await X())return!0;return!1}catch{return!1}}async function P(){if(await X())return;if(!await ne()){let e=f();throw new Error(`Worker service failed to start on port ${e}. + `;try{let S=this.db.prepare(E).all(p,d,...i),R=this.db.prepare(_).all(p,d,...i),u=this.db.prepare(T).all(p,d,...i);return{observations:S,sessions:R.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:u.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(S){return console.error("[SessionStore] Error querying timeline records:",S.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function Q(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(a,e,s={}){let t=Q(a,e,s);return JSON.stringify(t)}import O from"path";import{existsSync as D}from"fs";import{homedir as P}from"os";import{spawnSync as F}from"child_process";import{readFileSync as ee,existsSync as se}from"fs";var z=["bugfix","feature","refactor","discovery","decision","change"],Z=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var U=z.join(","),w=Z.join(",");var f=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:U,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:w,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(e){return process.env[e]||this.DEFAULTS[e]}static getInt(e){let s=this.get(e);return parseInt(s,10)}static getBool(e){return this.get(e)==="true"}static loadFromFile(e){if(!se(e))return this.getAllDefaults();let s=ee(e,"utf-8"),r=JSON.parse(s).env||{},o={...this.DEFAULTS};for(let n of Object.keys(this.DEFAULTS))r[n]!==void 0&&(o[n]=r[n]);return o}};var g=O.join(P(),".claude","plugins","marketplaces","thedotmack"),te=500,re=1e3,oe=15;function N(){let a=O.join(P(),".claude-mem","settings.json"),e=f.loadFromFile(a);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}async function X(){try{let a=N();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(te)})).ok}catch{return!1}}async function ne(){try{let a=O.join(g,"plugin","scripts","worker-service.cjs");if(!D(a))throw new Error(`Worker script not found at ${a}`);if(process.platform==="win32"){let e=F("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${a}' -WorkingDirectory '${g}' -WindowStyle Hidden`],{cwd:g,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(e.status!==0)throw new Error(e.stderr||"PowerShell Start-Process failed")}else{let e=O.join(g,"ecosystem.config.cjs");if(!D(e))throw new Error(`Ecosystem config not found at ${e}`);let s=O.join(g,"node_modules",".bin","pm2"),t=D(s)?s:"pm2",r=F(t,["start",e],{cwd:g,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let e=0;esetTimeout(s,re)),await X())return!0;return!1}catch{return!1}}async function H(){if(await X())return;if(!await ne()){let e=N();throw new Error(`Worker service failed to start on port ${e}. To start manually, run: - cd ${h} + cd ${g} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as ie}from"fs";import{homedir as ae}from"os";import{join as pe}from"path";var ce=pe(ae(),".claude-mem","silent.log");function g(a,e,s=""){let t=new Date().toISOString(),i=((new Error().stack||"").split(` +If already running, try: npx pm2 restart claude-mem-worker`)}}import{appendFileSync as ie}from"fs";import{homedir as ae}from"os";import{join as pe}from"path";var ce=pe(ae(),".claude-mem","silent.log");function b(a,e,s=""){let t=new Date().toISOString(),i=((new Error().stack||"").split(` `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),p=i?`${i[1].split("/").pop()}:${i[2]}`:"unknown",d=`[${t}] [${p}] ${a}`;if(e!==void 0)try{d+=` ${JSON.stringify(e)}`}catch(E){d+=` [stringify error: ${E}]`}d+=` -`;try{ie(ce,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var H=100;function de(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function B(a){if(typeof a!="string")return g("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=de(a);return e>H&&g("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:H,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function ue(a){if(await P(),!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;g("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=_e.basename(s);g("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s});let o=new R,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=B(t);if(!p||p.trim()===""){g("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(C("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=f(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(C("UserPromptSubmit",!0))}var D="";W.on("data",a=>D+=a);W.on("end",async()=>{let a=D?JSON.parse(D):void 0;await ue(a)}); +`;try{ie(ce,d)}catch(E){console.error("[silent-debug] Failed to write to log:",E)}return s}var B=100;function de(a){let e=(a.match(//g)||[]).length,s=(a.match(//g)||[]).length;return e+s}function W(a){if(typeof a!="string")return b("[tag-stripping] received non-string for prompt context:",{type:typeof a}),"";let e=de(a);return e>B&&b("[tag-stripping] tag count exceeds limit, truncating:",{tagCount:e,maxAllowed:B,contentLength:a.length}),a.replace(/[\s\S]*?<\/claude-mem-context>/g,"").replace(/[\s\S]*?<\/private>/g,"").trim()}async function ue(a){if(await H(),!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a;b("[new-hook] Input received",{session_id:e,cwd:s,cwd_type:typeof s,cwd_length:s?.length,has_cwd:!!s,prompt_length:t?.length});let r=_e.basename(s);b("[new-hook] Project extracted",{project:r,project_type:typeof r,project_length:r?.length,is_empty:r==="",cwd_was:s});let o=new h,n=o.createSDKSession(e,r,t),i=o.incrementPromptCounter(n),p=W(t);if(!p||p.trim()===""){b("[new-hook] Prompt entirely private, skipping memory operations",{session_id:e,promptNumber:i,originalLength:t.length}),o.close(),console.error(`[new-hook] Session ${n}, prompt #${i} (fully private - skipped)`),console.log(C("UserPromptSubmit",!0));return}o.saveUserPrompt(e,i,p),console.error(`[new-hook] Session ${n}, prompt #${i}`),o.close();let d=N(),E=t.startsWith("/")?t.substring(1):t;try{let _=await fetch(`http://127.0.0.1:${d}/sessions/${n}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:E,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!_.ok){let T=await _.text();throw new Error(`Failed to initialize session: ${_.status} ${T}`)}}catch(_){throw _.cause?.code==="ECONNREFUSED"||_.name==="TimeoutError"||_.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):_}console.log(C("UserPromptSubmit",!0))}var v="";j.on("data",a=>v+=a);j.on("end",async()=>{let a=v?JSON.parse(v):void 0;await ue(a)}); diff --git a/plugin/scripts/save-hook.js b/plugin/scripts/save-hook.js index 708c90b1..0a4006c8 100755 --- a/plugin/scripts/save-hook.js +++ b/plugin/scripts/save-hook.js @@ -1,10 +1,10 @@ #!/usr/bin/env node -import{stdin as U}from"process";function P(r,t,e){return r==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function S(r,t,e={}){let o=P(r,t,e);return JSON.stringify(o)}var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),g=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let o=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&o.command){let n=o.command.length>50?o.command.substring(0,50)+"...":o.command;return`${t}(${n})`}if(t==="Read"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}if(t==="Edit"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}if(t==="Write"&&o.file_path){let n=o.file_path.split("/").pop()||o.file_path;return`${t}(${n})`}return t}catch{return t}}log(t,e,o,n,s){if(t0&&(C=` {${Object.entries(A).map(([D,I])=>`${D}=${I}`).join(", ")}}`)}let d=`[${a}] [${f}] [${i}] ${u}${o}${C}${O}`;t===3?console.error(d):console.log(d)}debug(t,e,o,n){this.log(0,t,e,o,n)}info(t,e,o,n){this.log(1,t,e,o,n)}warn(t,e,o,n){this.log(2,t,e,o,n)}error(t,e,o,n){this.log(3,t,e,o,n)}dataIn(t,e,o,n){this.info(t,`\u2192 ${e}`,o,n)}dataOut(t,e,o,n){this.info(t,`\u2190 ${e}`,o,n)}success(t,e,o,n){this.info(t,`\u2713 ${e}`,o,n)}failure(t,e,o,n){this.error(t,`\u2717 ${e}`,o,n)}timing(t,e,o,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${o}ms`})}},c=new g;import _ from"path";import{existsSync as h}from"fs";import{homedir as R}from"os";import{spawnSync as x}from"child_process";import{readFileSync as v,existsSync as w}from"fs";var b=["bugfix","feature","refactor","discovery","decision","change"],k=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var y=b.join(","),M=k.join(",");var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:y,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!w(t))return this.getAllDefaults();let e=v(t,"utf-8"),n=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(s[a]=n[a]);return s}};var E=_.join(R(),".claude","plugins","marketplaces","thedotmack"),$=100,H=500,W=10;function l(){let r=_.join(R(),".claude-mem","settings.json"),t=p.loadFromFile(r);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let r=l();return(await fetch(`http://127.0.0.1:${r}/health`,{signal:AbortSignal.timeout($)})).ok}catch{return!1}}async function F(){try{let r=_.join(E,"ecosystem.config.cjs");if(!h(r))throw new Error(`Ecosystem config not found at ${r}`);let t=_.join(E,"node_modules",".bin","pm2"),e=process.platform==="win32"?t+".cmd":t,o=h(e)?e:"pm2",n=x(o,["start",r],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(n.status!==0)throw new Error(n.stderr||"PM2 start failed");for(let s=0;ssetTimeout(a,H)),await L())return!0;return!1}catch{return!1}}async function N(){if(await L())return;if(!await F()){let t=l();throw new Error(`Worker service failed to start on port ${t}. +import{stdin as D}from"process";function w(n,t,e){return n==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:n==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:n==="UserPromptSubmit"||n==="PostToolUse"?{continue:!0,suppressOutput:!0}:n==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function S(n,t,e={}){let o=w(n,t,e);return JSON.stringify(o)}var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),g=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=T[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let o=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&o.command){let r=o.command.length>50?o.command.substring(0,50)+"...":o.command;return`${t}(${r})`}if(t==="Read"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}if(t==="Edit"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}if(t==="Write"&&o.file_path){let r=o.file_path.split("/").pop()||o.file_path;return`${t}(${r})`}return t}catch{return t}}log(t,e,o,r,s){if(t0&&(d=` {${Object.entries(h).map(([I,P])=>`${I}=${P}`).join(", ")}}`)}let A=`[${a}] [${f}] [${i}] ${c}${o}${d}${O}`;t===3?console.error(A):console.log(A)}debug(t,e,o,r){this.log(0,t,e,o,r)}info(t,e,o,r){this.log(1,t,e,o,r)}warn(t,e,o,r){this.log(2,t,e,o,r)}error(t,e,o,r){this.log(3,t,e,o,r)}dataIn(t,e,o,r){this.info(t,`\u2192 ${e}`,o,r)}dataOut(t,e,o,r){this.info(t,`\u2190 ${e}`,o,r)}success(t,e,o,r){this.info(t,`\u2713 ${e}`,o,r)}failure(t,e,o,r){this.error(t,`\u2717 ${e}`,o,r)}timing(t,e,o,r){this.info(t,`\u23F1 ${e}`,r,{duration:`${o}ms`})}},p=new g;import E from"path";import{existsSync as m}from"fs";import{homedir as N}from"os";import{spawnSync as R}from"child_process";import{readFileSync as v,existsSync as $}from"fs";var k=["bugfix","feature","refactor","discovery","decision","change"],b=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var y=k.join(","),M=b.join(",");var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:y,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!$(t))return this.getAllDefaults();let e=v(t,"utf-8"),r=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))r[a]!==void 0&&(s[a]=r[a]);return s}};var u=E.join(N(),".claude","plugins","marketplaces","thedotmack"),x=500,H=1e3,W=15;function l(){let n=E.join(N(),".claude-mem","settings.json"),t=_.loadFromFile(n);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let n=l();return(await fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(x)})).ok}catch{return!1}}async function F(){try{let n=E.join(u,"plugin","scripts","worker-service.cjs");if(!m(n))throw new Error(`Worker script not found at ${n}`);if(process.platform==="win32"){let t=R("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${n}' -WorkingDirectory '${u}' -WindowStyle Hidden`],{cwd:u,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=E.join(u,"ecosystem.config.cjs");if(!m(t))throw new Error(`Ecosystem config not found at ${t}`);let e=E.join(u,"node_modules",".bin","pm2"),o=m(e)?e:"pm2",r=R(o,["start",t],{cwd:u,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;tsetTimeout(e,H)),await L())return!0;return!1}catch{return!1}}async function U(){if(await L())return;if(!await F()){let t=l();throw new Error(`Worker service failed to start on port ${t}. To start manually, run: - cd ${E} + cd ${u} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}var X=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function B(r){if(await N(),!r)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:n,tool_response:s}=r;if(X.has(o)){console.log(S("PostToolUse",!0));return}let a=l(),f=c.formatTool(o,n);c.dataIn("HOOK",`PostToolUse: ${f}`,{workerPort:a});try{let i=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:n,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let u=await i.text();throw c.failure("HOOK","Failed to send observation",{status:i.status},u),new Error(`Failed to send observation to worker: ${i.status} ${u}`)}c.debug("HOOK","Observation sent successfully",{toolName:o})}catch(i){throw i.cause?.code==="ECONNREFUSED"||i.name==="TimeoutError"||i.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):i}console.log(S("PostToolUse",!0))}var m="";U.on("data",r=>m+=r);U.on("end",async()=>{let r=m?JSON.parse(m):void 0;await B(r)}); +If already running, try: npx pm2 restart claude-mem-worker`)}}var X=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function B(n){if(await U(),!n)throw new Error("saveHook requires input");let{session_id:t,cwd:e,tool_name:o,tool_input:r,tool_response:s}=n;if(X.has(o)){console.log(S("PostToolUse",!0));return}let a=l(),f=p.formatTool(o,r);p.dataIn("HOOK",`PostToolUse: ${f}`,{workerPort:a});try{let i=await fetch(`http://127.0.0.1:${a}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:o,tool_input:r,tool_response:s,cwd:e||""}),signal:AbortSignal.timeout(2e3)});if(!i.ok){let c=await i.text();throw p.failure("HOOK","Failed to send observation",{status:i.status},c),new Error(`Failed to send observation to worker: ${i.status} ${c}`)}p.debug("HOOK","Observation sent successfully",{toolName:o})}catch(i){throw i.cause?.code==="ECONNREFUSED"||i.name==="TimeoutError"||i.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):i}console.log(S("PostToolUse",!0))}var C="";D.on("data",n=>C+=n);D.on("end",async()=>{let n=C?JSON.parse(C):void 0;await B(n)}); diff --git a/plugin/scripts/smart-install.js b/plugin/scripts/smart-install.js new file mode 100644 index 00000000..ee79f332 --- /dev/null +++ b/plugin/scripts/smart-install.js @@ -0,0 +1,389 @@ +#!/usr/bin/env node + +/** + * Smart Install Script for claude-mem + * + * Features: + * - Only runs npm install when necessary (version change or missing deps) + * - Caches installation state with version marker + * - Provides helpful Windows-specific error messages + * - Cross-platform compatible (pure Node.js) + * - Fast when already installed (just version check) + */ + +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { execSync, spawnSync, spawn } from 'child_process'; +import { join } from 'path'; +import { homedir } from 'os'; +import { createRequire } from 'module'; + +// CRITICAL: Always use marketplace directory for ALL operations +// This script may run from the cache directory (plugin/scripts/) but must +// operate on the marketplace directory where package.json and node_modules live. +// This ensures cross-platform compatibility and avoids cache directory confusion. +const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); + +// Use MARKETPLACE_ROOT for all paths - this script can be deployed anywhere +// but always operates on the marketplace directory +const PLUGIN_ROOT = MARKETPLACE_ROOT; +const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json'); +const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version'); +const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules'); +const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3'); + +// Colors for output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + cyan: '\x1b[36m', + dim: '\x1b[2m', +}; + +function log(message, color = colors.reset) { + console.error(`${color}${message}${colors.reset}`); +} + +function getPackageVersion() { + try { + const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf-8')); + return packageJson.version; + } catch (error) { + log(`āš ļø Failed to read package.json: ${error.message}`, colors.yellow); + return null; + } +} + +function getNodeVersion() { + return process.version; // e.g., "v22.21.1" +} + +function getInstalledVersion() { + try { + if (existsSync(VERSION_MARKER_PATH)) { + const content = readFileSync(VERSION_MARKER_PATH, 'utf-8').trim(); + + // Try parsing as JSON (new format) + try { + const marker = JSON.parse(content); + return { + packageVersion: marker.packageVersion, + nodeVersion: marker.nodeVersion, + installedAt: marker.installedAt + }; + } catch { + // Fallback: old format (plain text version string) + return { + packageVersion: content, + nodeVersion: null, // Unknown + installedAt: null + }; + } + } + } catch (error) { + // Marker doesn't exist or can't be read + } + return null; +} + +function setInstalledVersion(packageVersion, nodeVersion) { + try { + const marker = { + packageVersion, + nodeVersion, + installedAt: new Date().toISOString() + }; + writeFileSync(VERSION_MARKER_PATH, JSON.stringify(marker, null, 2), 'utf-8'); + } catch (error) { + log(`āš ļø Failed to write version marker: ${error.message}`, colors.yellow); + } +} + +function needsInstall() { + // Check if node_modules exists + if (!existsSync(NODE_MODULES_PATH)) { + log('šŸ“¦ Dependencies not found - first time setup', colors.cyan); + return true; + } + + // Check if better-sqlite3 is installed + if (!existsSync(BETTER_SQLITE3_PATH)) { + log('šŸ“¦ better-sqlite3 missing - reinstalling', colors.cyan); + return true; + } + + // Check version marker + const currentPackageVersion = getPackageVersion(); + const currentNodeVersion = getNodeVersion(); + const installed = getInstalledVersion(); + + if (!installed) { + log('šŸ“¦ No version marker found - installing', colors.cyan); + return true; + } + + // Check package version + if (currentPackageVersion !== installed.packageVersion) { + log(`šŸ“¦ Version changed (${installed.packageVersion} → ${currentPackageVersion}) - updating`, colors.cyan); + return true; + } + + // Check Node.js version + if (installed.nodeVersion && currentNodeVersion !== installed.nodeVersion) { + log(`šŸ“¦ Node.js version changed (${installed.nodeVersion} → ${currentNodeVersion}) - rebuilding native modules`, colors.cyan); + return true; + } + + // If old format (no nodeVersion), assume needs install + if (!installed.nodeVersion) { + log('šŸ“¦ Old version marker format - updating', colors.cyan); + return true; + } + + // All good - no install needed + log(`āœ“ Dependencies already installed (v${currentPackageVersion})`, colors.dim); + return false; +} + +/** + * Verify that better-sqlite3 native module loads correctly + * This catches ABI mismatches and corrupted builds + */ +async function verifyNativeModules() { + try { + log('šŸ” Verifying native modules...', colors.dim); + + // CRITICAL: Use createRequire() to resolve from MARKETPLACE_ROOT + // This script may run from cache but must load modules from marketplace's node_modules + const require = createRequire(join(MARKETPLACE_ROOT, 'package.json')); + const Database = require('better-sqlite3'); + + // Try to create a test in-memory database + const db = new Database(':memory:'); + + // Run a simple query to ensure it works + const result = db.prepare('SELECT 1 + 1 as result').get(); + + // Clean up + db.close(); + + if (result.result !== 2) { + throw new Error('SQLite math check failed'); + } + + log('āœ“ Native modules verified', colors.dim); + return true; + + } catch (error) { + if (error.code === 'ERR_DLOPEN_FAILED') { + log('āš ļø Native module ABI mismatch detected', colors.yellow); + return false; + } + + // Other errors are unexpected - log and fail + log(`āŒ Native module verification failed: ${error.message}`, colors.red); + return false; + } +} + +function getWindowsErrorHelp(errorOutput) { + // Detect Python version at runtime + let pythonStatus = ' Python not detected or version unknown'; + try { + const pythonVersion = execSync('python --version', { encoding: 'utf-8', stdio: 'pipe' }).trim(); + const versionMatch = pythonVersion.match(/Python\s+([\d.]+)/); + if (versionMatch) { + pythonStatus = ` You have ${versionMatch[0]} installed āœ“`; + } + } catch (error) { + // Python not available or failed to detect - use default message + } + + const help = [ + '', + '╔══════════════════════════════════════════════════════════════════════╗', + 'ā•‘ Windows Installation Help ā•‘', + 'ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•', + '', + 'šŸ“‹ better-sqlite3 requires build tools to compile native modules.', + '', + 'šŸ”§ Option 1: Install Visual Studio Build Tools (Recommended)', + ' 1. Download: https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022', + ' 2. Install "Desktop development with C++"', + ' 3. Restart your terminal', + ' 4. Try again', + '', + 'šŸ”§ Option 2: Install via npm (automated)', + ' Run as Administrator:', + ' npm install --global windows-build-tools', + '', + 'šŸ Python Requirement:', + ' Python 3.6+ is required.', + pythonStatus, + '', + ]; + + // Check for specific error patterns + if (errorOutput.includes('MSBuild.exe')) { + help.push('āŒ MSBuild not found - install Visual Studio Build Tools'); + } + if (errorOutput.includes('MSVS')) { + help.push('āŒ Visual Studio not detected - install Build Tools'); + } + if (errorOutput.includes('permission') || errorOutput.includes('EPERM')) { + help.push('āŒ Permission denied - try running as Administrator'); + } + + help.push(''); + help.push('šŸ“– Full documentation: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md'); + help.push(''); + + return help.join('\n'); +} + +async function runNpmInstall() { + const isWindows = process.platform === 'win32'; + + log('', colors.cyan); + log('šŸ”Ø Installing dependencies...', colors.bright); + log('', colors.reset); + + // Try normal install first, then retry with force if it fails + const strategies = [ + { command: 'npm install', label: 'normal' }, + { command: 'npm install --force', label: 'with force flag' }, + ]; + + let lastError = null; + + for (const { command, label } of strategies) { + try { + log(`Attempting install ${label}...`, colors.dim); + + // Run npm install silently + execSync(command, { + cwd: PLUGIN_ROOT, + stdio: 'pipe', // Silent output unless error + encoding: 'utf-8', + }); + + // Verify better-sqlite3 was installed + if (!existsSync(BETTER_SQLITE3_PATH)) { + throw new Error('better-sqlite3 installation verification failed'); + } + + // NEW: Verify native modules actually work + const nativeModulesWork = await verifyNativeModules(); + if (!nativeModulesWork) { + throw new Error('Native modules failed to load after install'); + } + + const packageVersion = getPackageVersion(); + const nodeVersion = getNodeVersion(); + setInstalledVersion(packageVersion, nodeVersion); + + log('', colors.green); + log('āœ… Dependencies installed successfully!', colors.bright); + log(` Package version: ${packageVersion}`, colors.dim); + log(` Node.js version: ${nodeVersion}`, colors.dim); + log('', colors.reset); + + return true; + + } catch (error) { + lastError = error; + // Continue to next strategy + } + } + + // All strategies failed - show error + log('', colors.red); + log('āŒ Installation failed after retrying!', colors.bright); + log('', colors.reset); + + // Provide Windows-specific help + if (isWindows && lastError && lastError.message && lastError.message.includes('better-sqlite3')) { + log(getWindowsErrorHelp(lastError.message), colors.yellow); + } + + // Show generic error info with troubleshooting steps + if (lastError) { + if (lastError.stderr) { + log('Error output:', colors.dim); + log(lastError.stderr.toString(), colors.red); + } else if (lastError.message) { + log(lastError.message, colors.red); + } + + log('', colors.yellow); + log('šŸ“‹ Troubleshooting Steps:', colors.bright); + log('', colors.reset); + log('1. Check your internet connection', colors.yellow); + log('2. Try running: npm cache clean --force', colors.yellow); + log('3. Try running: npm install (in plugin directory)', colors.yellow); + log('4. Check npm version: npm --version (requires npm 7+)', colors.yellow); + log('5. Try updating npm: npm install -g npm@latest', colors.yellow); + log('', colors.reset); + } + + return false; +} + +/** + * Check if we should fail when worker startup fails + * Returns true if worker failed AND dependencies are missing + */ +function shouldFailOnWorkerStartup(workerStarted) { + return !workerStarted && !existsSync(NODE_MODULES_PATH); +} + +async function main() { + try { + // Check if we need to install dependencies + const installNeeded = needsInstall(); + + if (installNeeded) { + // Run installation (now async) + const installSuccess = await runNpmInstall(); + + if (!installSuccess) { + log('', colors.red); + log('āš ļø Installation failed', colors.yellow); + log('', colors.reset); + process.exit(1); + } + } else { + // NEW: Even if install not needed, verify native modules work + const nativeModulesWork = await verifyNativeModules(); + + if (!nativeModulesWork) { + log('šŸ“¦ Native modules need rebuild - reinstalling', colors.cyan); + const installSuccess = await runNpmInstall(); + + if (!installSuccess) { + log('', colors.red); + log('āš ļø Native module rebuild failed', colors.yellow); + log('', colors.reset); + process.exit(1); + } + } + } + + // NOTE: Worker auto-start disabled in smart-install.js + // The context-hook.js calls ensureWorkerRunning() which handles worker startup + // This avoids potential process management conflicts during plugin initialization + log('āœ… Installation complete', colors.green); + + // Success - dependencies installed (if needed) + process.exit(0); + + } catch (error) { + log(`āŒ Unexpected error: ${error.message}`, colors.red); + log('', colors.reset); + process.exit(1); + } +} + +main(); diff --git a/plugin/scripts/summary-hook.js b/plugin/scripts/summary-hook.js index f0742e2f..c87815c0 100755 --- a/plugin/scripts/summary-hook.js +++ b/plugin/scripts/summary-hook.js @@ -1,16 +1,16 @@ #!/usr/bin/env node -import{stdin as D}from"process";import{readFileSync as U,existsSync as I}from"fs";function k(o,t,e){return o==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function d(o,t,e={}){let n=k(o,t,e);return JSON.stringify(n)}var O=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(O||{}),S=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} -${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let n=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&n.command){let r=n.command.length>50?n.command.substring(0,50)+"...":n.command;return`${t}(${r})`}if(t==="Read"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Edit"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Write"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}return t}catch{return t}}log(t,e,n,r,s){if(t0&&(T=` {${Object.entries(C).map(([x,b])=>`${x}=${b}`).join(", ")}}`)}let y=`[${i}] [${a}] [${u}] ${l}${n}${T}${g}`;t===3?console.error(y):console.log(y)}debug(t,e,n,r){this.log(0,t,e,n,r)}info(t,e,n,r){this.log(1,t,e,n,r)}warn(t,e,n,r){this.log(2,t,e,n,r)}error(t,e,n,r){this.log(3,t,e,n,r)}dataIn(t,e,n,r){this.info(t,`\u2192 ${e}`,n,r)}dataOut(t,e,n,r){this.info(t,`\u2190 ${e}`,n,r)}success(t,e,n,r){this.info(t,`\u2713 ${e}`,n,r)}failure(t,e,n,r){this.error(t,`\u2717 ${e}`,n,r)}timing(t,e,n,r){this.info(t,`\u23F1 ${e}`,r,{duration:`${n}ms`})}},c=new S;import f from"path";import{existsSync as M}from"fs";import{homedir as R}from"os";import{spawnSync as H}from"child_process";import{readFileSync as w,existsSync as $}from"fs";var P=["bugfix","feature","refactor","discovery","decision","change"],v=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var A=P.join(","),h=v.join(",");var p=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:A,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 process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!$(t))return this.getAllDefaults();let e=w(t,"utf-8"),r=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))r[i]!==void 0&&(s[i]=r[i]);return s}};var E=f.join(R(),".claude","plugins","marketplaces","thedotmack"),W=100,F=500,X=10;function _(){let o=f.join(R(),".claude-mem","settings.json"),t=p.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function L(){try{let o=_();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function j(){try{let o=f.join(E,"ecosystem.config.cjs");if(!M(o))throw new Error(`Ecosystem config not found at ${o}`);let t=f.join(E,"node_modules",".bin","pm2"),e=process.platform==="win32"?t+".cmd":t,n=M(e)?e:"pm2",r=H(n,["start",o],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed");for(let s=0;ssetTimeout(i,F)),await L())return!0;return!1}catch{return!1}}async function N(){if(await L())return;if(!await j()){let t=_();throw new Error(`Worker service failed to start on port ${t}. +import{stdin as U}from"process";import{readFileSync as I,existsSync as x}from"fs";function P(o,t,e){return o==="PreCompact"?t?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:e.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?t&&e.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:e.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:t,suppressOutput:!0,...e.reason&&!t?{stopReason:e.reason}:{}}}function A(o,t,e={}){let n=P(o,t,e);return JSON.stringify(n)}var O=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(O||{}),S=class{level;useColor;constructor(){let t=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[t]??1,this.useColor=process.stdout.isTTY??!1}correlationId(t,e){return`obs-${t}-${e}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.level===0?`${t.message} +${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let e=Object.keys(t);return e.length===0?"{}":e.length<=3?JSON.stringify(t):`{${e.length} keys: ${e.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,e){if(!e)return t;try{let n=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&n.command){let r=n.command.length>50?n.command.substring(0,50)+"...":n.command;return`${t}(${r})`}if(t==="Read"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Edit"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}if(t==="Write"&&n.file_path){let r=n.file_path.split("/").pop()||n.file_path;return`${t}(${r})`}return t}catch{return t}}log(t,e,n,r,s){if(t0&&(d=` {${Object.entries(C).map(([w,k])=>`${w}=${k}`).join(", ")}}`)}let y=`[${i}] [${c}] [${p}] ${_}${n}${d}${g}`;t===3?console.error(y):console.log(y)}debug(t,e,n,r){this.log(0,t,e,n,r)}info(t,e,n,r){this.log(1,t,e,n,r)}warn(t,e,n,r){this.log(2,t,e,n,r)}error(t,e,n,r){this.log(3,t,e,n,r)}dataIn(t,e,n,r){this.info(t,`\u2192 ${e}`,n,r)}dataOut(t,e,n,r){this.info(t,`\u2190 ${e}`,n,r)}success(t,e,n,r){this.info(t,`\u2713 ${e}`,n,r)}failure(t,e,n,r){this.error(t,`\u2717 ${e}`,n,r)}timing(t,e,n,r){this.info(t,`\u23F1 ${e}`,r,{duration:`${n}ms`})}},u=new S;import E from"path";import{existsSync as m}from"fs";import{homedir as L}from"os";import{spawnSync as N}from"child_process";import{readFileSync as v,existsSync as H}from"fs";var b=["bugfix","feature","refactor","discovery","decision","change"],$=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var h=b.join(","),M=$.join(",");var f=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:h,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:M,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!H(t))return this.getAllDefaults();let e=v(t,"utf-8"),r=JSON.parse(e).env||{},s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))r[i]!==void 0&&(s[i]=r[i]);return s}};var a=E.join(L(),".claude","plugins","marketplaces","thedotmack"),W=500,F=1e3,j=15;function l(){let o=E.join(L(),".claude-mem","settings.json"),t=f.loadFromFile(o);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function R(){try{let o=l();return(await fetch(`http://127.0.0.1:${o}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function X(){try{let o=E.join(a,"plugin","scripts","worker-service.cjs");if(!m(o))throw new Error(`Worker script not found at ${o}`);if(process.platform==="win32"){let t=N("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${o}' -WorkingDirectory '${a}' -WindowStyle Hidden`],{cwd:a,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=E.join(a,"ecosystem.config.cjs");if(!m(t))throw new Error(`Ecosystem config not found at ${t}`);let e=E.join(a,"node_modules",".bin","pm2"),n=m(e)?e:"pm2",r=N(n,["start",t],{cwd:a,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;tsetTimeout(e,F)),await R())return!0;return!1}catch{return!1}}async function D(){if(await R())return;if(!await X()){let t=l();throw new Error(`Worker service failed to start on port ${t}. To start manually, run: - cd ${E} + cd ${a} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}function B(o){if(!o||!I(o))return"";try{let t=U(o,"utf-8").trim();if(!t)return"";let e=t.split(` -`);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="user"&&r.message?.content){let s=r.message.content;if(typeof s=="string")return s;if(Array.isArray(s))return s.filter(a=>a.type==="text").map(a=>a.text).join(` -`)}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function K(o){if(!o||!I(o))return"";try{let t=U(o,"utf-8").trim();if(!t)return"";let e=t.split(` -`);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="assistant"&&r.message?.content){let s="",i=r.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(u=>u.type==="text").map(u=>u.text).join(` +If already running, try: npx pm2 restart claude-mem-worker`)}}function B(o){if(!o||!x(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(` +`);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="user"&&r.message?.content){let s=r.message.content;if(typeof s=="string")return s;if(Array.isArray(s))return s.filter(c=>c.type==="text").map(c=>c.text).join(` +`)}}catch{continue}}catch(t){u.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}function K(o){if(!o||!x(o))return"";try{let t=I(o,"utf-8").trim();if(!t)return"";let e=t.split(` +`);for(let n=e.length-1;n>=0;n--)try{let r=JSON.parse(e[n]);if(r.type==="assistant"&&r.message?.content){let s="",i=r.message.content;return typeof i=="string"?s=i:Array.isArray(i)&&(s=i.filter(p=>p.type==="text").map(p=>p.text).join(` `)),s=s.replace(/[\s\S]*?<\/system-reminder>/g,""),s=s.replace(/\n{3,}/g,` -`).trim(),s}}catch{continue}}catch(t){c.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function V(o){if(await N(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=_(),n=B(o.transcript_path||""),r=K(o.transcript_path||"");c.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!n,hasLastAssistantMessage:!!r});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:n,last_assistant_message:r}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw c.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}c.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(d("Stop",!0))}var m="";D.on("data",o=>m+=o);D.on("end",async()=>{let o=m?JSON.parse(m):void 0;await V(o)}); +`).trim(),s}}catch{continue}}catch(t){u.error("HOOK","Failed to read transcript",{transcriptPath:o},t)}return""}async function V(o){if(await D(),!o)throw new Error("summaryHook requires input");let{session_id:t}=o,e=l(),n=B(o.transcript_path||""),r=K(o.transcript_path||"");u.dataIn("HOOK","Stop: Requesting summary",{workerPort:e,hasLastUserMessage:!!n,hasLastAssistantMessage:!!r});try{let s=await fetch(`http://127.0.0.1:${e}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:n,last_assistant_message:r}),signal:AbortSignal.timeout(2e3)});if(!s.ok){let i=await s.text();throw u.failure("HOOK","Failed to generate summary",{status:s.status},i),new Error(`Failed to request summary from worker: ${s.status} ${i}`)}u.debug("HOOK","Summary request sent successfully")}catch(s){throw s.cause?.code==="ECONNREFUSED"||s.name==="TimeoutError"||s.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):s}finally{fetch(`http://127.0.0.1:${e}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})}).catch(()=>{})}console.log(A("Stop",!0))}var T="";U.on("data",o=>T+=o);U.on("end",async()=>{let o=T?JSON.parse(T):void 0;await V(o)}); diff --git a/plugin/scripts/user-message-hook.js b/plugin/scripts/user-message-hook.js index 12000110..fca44957 100755 --- a/plugin/scripts/user-message-hook.js +++ b/plugin/scripts/user-message-hook.js @@ -1,11 +1,11 @@ #!/usr/bin/env node -import{join as M,basename as k}from"path";import{homedir as v}from"os";import{existsSync as x}from"fs";import c from"path";import{existsSync as m}from"fs";import{homedir as C}from"os";import{spawnSync as R}from"child_process";import{readFileSync as L,existsSync as y}from"fs";var h=["bugfix","feature","refactor","discovery","decision","change"],U=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var p=h.join(","),S=U.join(",");var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:p,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:S,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let n=this.get(t);return parseInt(n,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!y(t))return this.getAllDefaults();let n=L(t,"utf-8"),o=JSON.parse(n).env||{},r={...this.DEFAULTS};for(let s of Object.keys(this.DEFAULTS))o[s]!==void 0&&(r[s]=o[s]);return r}};var E=c.join(C(),".claude","plugins","marketplaces","thedotmack"),w=100,I=500,P=10;function _(){let e=c.join(C(),".claude-mem","settings.json"),t=a.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function f(){try{let e=_();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(w)})).ok}catch{return!1}}async function W(){try{let e=c.join(E,"ecosystem.config.cjs");if(!m(e))throw new Error(`Ecosystem config not found at ${e}`);let t=c.join(E,"node_modules",".bin","pm2"),n=process.platform==="win32"?t+".cmd":t,i=m(n)?n:"pm2",o=R(i,["start",e],{cwd:E,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(o.status!==0)throw new Error(o.stderr||"PM2 start failed");for(let r=0;rsetTimeout(s,I)),await f())return!0;return!1}catch{return!1}}async function A(){if(await f())return;if(!await W()){let t=_();throw new Error(`Worker service failed to start on port ${t}. +import{join as M,basename as W}from"path";import{homedir as v}from"os";import{existsSync as x}from"fs";import i from"path";import{existsSync as u}from"fs";import{homedir as f}from"os";import{spawnSync as d}from"child_process";import{readFileSync as y,existsSync as w}from"fs";var L=["bugfix","feature","refactor","discovery","decision","change"],U=["how-it-works","why-it-exists","what-changed","problem-solution","gotcha","pattern","trade-off"];var S=L.join(","),m=U.join(",");var E=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-haiku-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:S,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:m,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return process.env[t]||this.DEFAULTS[t]}static getInt(t){let o=this.get(t);return parseInt(o,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){if(!w(t))return this.getAllDefaults();let o=y(t,"utf-8"),r=JSON.parse(o).env||{},c={...this.DEFAULTS};for(let s of Object.keys(this.DEFAULTS))r[s]!==void 0&&(c[s]=r[s]);return c}};var n=i.join(f(),".claude","plugins","marketplaces","thedotmack"),R=500,P=1e3,I=15;function _(){let e=i.join(f(),".claude-mem","settings.json"),t=E.loadFromFile(e);return parseInt(t.CLAUDE_MEM_WORKER_PORT,10)}async function C(){try{let e=_();return(await fetch(`http://127.0.0.1:${e}/health`,{signal:AbortSignal.timeout(R)})).ok}catch{return!1}}async function k(){try{let e=i.join(n,"plugin","scripts","worker-service.cjs");if(!u(e))throw new Error(`Worker script not found at ${e}`);if(process.platform==="win32"){let t=d("powershell.exe",["-NoProfile","-NonInteractive","-Command",`Start-Process -FilePath 'node' -ArgumentList '${e}' -WorkingDirectory '${n}' -WindowStyle Hidden`],{cwd:n,stdio:"pipe",encoding:"utf-8",windowsHide:!0});if(t.status!==0)throw new Error(t.stderr||"PowerShell Start-Process failed")}else{let t=i.join(n,"ecosystem.config.cjs");if(!u(t))throw new Error(`Ecosystem config not found at ${t}`);let o=i.join(n,"node_modules",".bin","pm2"),a=u(o)?o:"pm2",r=d(a,["start",t],{cwd:n,stdio:"pipe",encoding:"utf-8"});if(r.status!==0)throw new Error(r.stderr||"PM2 start failed")}for(let t=0;tsetTimeout(o,P)),await C())return!0;return!1}catch{return!1}}async function A(){if(await C())return;if(!await k()){let t=_();throw new Error(`Worker service failed to start on port ${t}. To start manually, run: - cd ${E} + cd ${n} npx pm2 start ecosystem.config.cjs -If already running, try: npx pm2 restart claude-mem-worker`)}}var b=M(v(),".claude","plugins","marketplaces","thedotmack"),X=M(b,"node_modules");x(X)||(console.error(` +If already running, try: npx pm2 restart claude-mem-worker`)}}var b=M(v(),".claude","plugins","marketplaces","thedotmack"),F=M(b,"node_modules");x(F)||(console.error(` --- \u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for user messages in Claude Code UI until a better method is provided. @@ -23,7 +23,7 @@ Dependencies have been installed in the background. This only happens once. Thank you for installing Claude-Mem! This message was not added to your startup context, so you can continue working as normal. -`),process.exit(3));try{await A();let e=_(),t=k(process.cwd()),n=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!n.ok)throw new Error(`Worker error ${n.status}`);let i=await n.text(),o=new Date,r=new Date("2025-12-06T00:00:00Z"),s=new Date("2025-12-05T05:00:00Z"),u="";o=1&&O<=5,D=l>=17&&l<19;N&&D?T=` +`);let T="";if(r=1&&O<=5,D=p>=17&&p<19;N&&D?T=` \u{1F534} LIVE NOW: AMA w/ Dev (@thedotmack) until 7pm EST `:T=` \u2013 LIVE AMA w/ Dev (@thedotmack) Dec 1st\u20135th, 5pm to 7pm EST @@ -42,10 +42,10 @@ This message was not added to your startup context, so you can continue working \u{1F4DD} Claude-Mem Context Loaded \u2139\uFE0F Note: This appears as stderr but is informational only -`+i+` +`+a+` \u{1F4A1} New! Wrap all or part of any message with ... to prevent storing sensitive information in your observation history. -\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+u+T+` +\u{1F4AC} Community https://discord.gg/J4wttp9vDu`+l+T+` \u{1F4FA} Watch live in browser http://localhost:${e}/ `)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3); diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 7ac60a5a..5b046b06 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1037,7 +1037,7 @@ Other tips: WHERE project IS NOT NULL GROUP BY project ORDER BY MAX(created_at_epoch) DESC - `).all().map(o=>o.project);t.json({projects:n})});handleGetProcessingStatus=this.wrapHandler((r,t)=>{let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalActiveWork();t.json({isProcessing:s,queueDepth:i})});handleSetProcessing=this.wrapHandler((r,t)=>{this.workerService.broadcastProcessingStatus();let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalQueueDepth(),n=this.sessionManager.getActiveSessionCount();t.json({status:"ok",isProcessing:s})});parsePaginationParams(r){let t=parseInt(r.query.offset,10)||0,s=Math.min(parseInt(r.query.limit,10)||20,100),i=r.query.project;return{offset:t,limit:s,project:i}}};var Al=class extends gr{constructor(r){super();this.searchManager=r}setupRoutes(r){r.get("/api/search",this.handleUnifiedSearch.bind(this)),r.get("/api/timeline",this.handleUnifiedTimeline.bind(this)),r.get("/api/decisions",this.handleDecisions.bind(this)),r.get("/api/changes",this.handleChanges.bind(this)),r.get("/api/how-it-works",this.handleHowItWorks.bind(this)),r.get("/api/search/observations",this.handleSearchObservations.bind(this)),r.get("/api/search/sessions",this.handleSearchSessions.bind(this)),r.get("/api/search/prompts",this.handleSearchPrompts.bind(this)),r.get("/api/search/by-concept",this.handleSearchByConcept.bind(this)),r.get("/api/search/by-file",this.handleSearchByFile.bind(this)),r.get("/api/search/by-type",this.handleSearchByType.bind(this)),r.get("/api/context/recent",this.handleGetRecentContext.bind(this)),r.get("/api/context/timeline",this.handleGetContextTimeline.bind(this)),r.get("/api/context/preview",this.handleContextPreview.bind(this)),r.get("/api/context/inject",this.handleContextInject.bind(this)),r.get("/api/timeline/by-query",this.handleGetTimelineByQuery.bind(this)),r.get("/api/search/help",this.handleSearchHelp.bind(this))}handleUnifiedSearch=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.search(r.query);t.json(s.content)});handleUnifiedTimeline=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.timeline(r.query);t.json(s.content)});handleDecisions=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.decisions(r.query);t.json(s.content)});handleChanges=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.changes(r.query);t.json(s.content)});handleHowItWorks=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.howItWorks(r.query);t.json(s.content)});handleSearchObservations=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchObservations(r.query);t.json(s.content)});handleSearchSessions=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchSessions(r.query);t.json(s.content)});handleSearchPrompts=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchUserPrompts(r.query);t.json(s.content)});handleSearchByConcept=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByConcept(r.query);t.json(s.content)});handleSearchByFile=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByFile(r.query);t.json(s.content)});handleSearchByType=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByType(r.query);t.json(s.content)});handleGetRecentContext=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getRecentContext(r.query);t.json(s.content)});handleGetContextTimeline=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getContextTimeline(r.query);t.json(s.content)});handleContextPreview=this.wrapHandler(async(r,t)=>{let s=r.query.project;if(!s){this.badRequest(t,"Project parameter is required");return}let{generateContext:i}=await Promise.resolve().then(()=>(wf(),Ef)),n=`/preview/${s}`,o=await i({session_id:"preview-"+Date.now(),cwd:n},!0);t.setHeader("Content-Type","text/plain; charset=utf-8"),t.send(o)});handleContextInject=this.wrapHandler(async(r,t)=>{let s=r.query.project,i=r.query.colors==="true";if(!s){this.badRequest(t,"Project parameter is required");return}let{generateContext:n}=await Promise.resolve().then(()=>(wf(),Ef)),o=`/context/${s}`,l=await n({session_id:"context-inject-"+Date.now(),cwd:o},i);t.setHeader("Content-Type","text/plain; charset=utf-8"),t.send(l)});handleGetTimelineByQuery=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getTimelineByQuery(r.query);t.json(s.content)});handleSearchHelp=this.wrapHandler((r,t)=>{t.json({title:"Claude-Mem Search API",description:"HTTP API for searching persistent memory",endpoints:[{path:"/api/search/observations",method:"GET",description:"Search observations using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/sessions",method:"GET",description:"Search session summaries using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)"}},{path:"/api/search/prompts",method:"GET",description:"Search user prompts using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/by-concept",method:"GET",description:"Find observations by concept tag",parameters:{concept:"Concept tag (required): discovery, decision, bugfix, feature, refactor",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-file",method:"GET",description:"Find observations and sessions by file path",parameters:{filePath:"File path or partial path (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results per type (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-type",method:"GET",description:"Find observations by type",parameters:{type:"Observation type (required): discovery, decision, bugfix, feature, refactor",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/context/recent",method:"GET",description:"Get recent session context including summaries and observations",parameters:{project:"Project name (default: current directory)",limit:"Number of recent sessions (default: 3)"}},{path:"/api/context/timeline",method:"GET",description:"Get unified timeline around a specific point in time",parameters:{anchor:'Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp (required)',depth_before:"Number of records before anchor (default: 10)",depth_after:"Number of records after anchor (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/timeline/by-query",method:"GET",description:"Search for best match, then get timeline around it",parameters:{query:"Search query (required)",mode:'Search mode: "auto", "observations", or "sessions" (default: "auto")',depth_before:"Number of records before match (default: 10)",depth_after:"Number of records after match (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/help",method:"GET",description:"Get this help documentation"}],examples:['curl "http://localhost:37777/api/search/observations?query=authentication&format=index&limit=5"','curl "http://localhost:37777/api/search/by-type?type=bugfix&limit=10"','curl "http://localhost:37777/api/context/recent?project=claude-mem&limit=3"','curl "http://localhost:37777/api/context/timeline?anchor=123&depth_before=5&depth_after=5"']})})};var Wn=gt(require("path"),1),br=require("fs"),Rf=require("os");xs();wt();var Tf=require("child_process"),Fs=require("fs"),Fw=require("os"),Ji=require("path");wt();var Yi=(0,Ji.join)((0,Fw.homedir)(),".claude","plugins","marketplaces","thedotmack");function yr(a){return(0,Tf.execSync)(`git ${a}`,{cwd:Yi,encoding:"utf-8",timeout:3e4}).trim()}function Uw(a,e=6e4){return(0,Tf.execSync)(a,{cwd:Yi,encoding:"utf-8",timeout:e}).trim()}function Nl(){let a=(0,Ji.join)(Yi,".git");if(!(0,Fs.existsSync)(a))return{branch:null,isBeta:!1,isGitRepo:!1,isDirty:!1,canSwitch:!1,error:"Installed plugin is not a git repository"};try{let e=yr("rev-parse --abbrev-ref HEAD"),t=yr("status --porcelain").length>0,s=e.startsWith("beta");return{branch:e,isBeta:s,isGitRepo:!0,isDirty:t,canSwitch:!0}}catch(e){return W.error("BRANCH","Failed to get branch info",{},e),{branch:null,isBeta:!1,isGitRepo:!0,isDirty:!1,canSwitch:!1,error:e.message}}}async function zw(a){let e=Nl();if(!e.isGitRepo)return{success:!1,error:"Installed plugin is not a git repository. Please reinstall."};if(e.branch===a)return{success:!0,branch:a,message:`Already on branch ${a}`};try{W.info("BRANCH","Starting branch switch",{from:e.branch,to:a}),W.debug("BRANCH","Discarding local changes"),yr("checkout -- ."),yr("clean -fd"),W.debug("BRANCH","Fetching from origin"),yr("fetch origin"),W.debug("BRANCH","Checking out branch",{branch:a});try{yr(`checkout ${a}`)}catch{yr(`checkout -b ${a} origin/${a}`)}W.debug("BRANCH","Pulling latest"),yr(`pull origin ${a}`);let r=(0,Ji.join)(Yi,".install-version");return(0,Fs.existsSync)(r)&&(0,Fs.unlinkSync)(r),W.debug("BRANCH","Running npm install"),Uw("npm install",12e4),W.success("BRANCH","Branch switch complete",{branch:a}),{success:!0,branch:a,message:`Switched to ${a}. Worker will restart automatically.`}}catch(r){W.error("BRANCH","Branch switch failed",{targetBranch:a},r);try{e.branch&&yr(`checkout ${e.branch}`)}catch{}return{success:!1,error:`Branch switch failed: ${r.message}`}}}async function Bw(){let a=Nl();if(!a.isGitRepo||!a.branch)return{success:!1,error:"Cannot pull updates: not a git repository"};try{W.info("BRANCH","Pulling updates",{branch:a.branch}),yr("checkout -- ."),yr("fetch origin"),yr(`pull origin ${a.branch}`);let e=(0,Ji.join)(Yi,".install-version");return(0,Fs.existsSync)(e)&&(0,Fs.unlinkSync)(e),Uw("npm install",12e4),W.success("BRANCH","Updates pulled",{branch:a.branch}),{success:!0,branch:a.branch,message:`Updated ${a.branch}. Worker will restart automatically.`}}catch(e){return W.error("BRANCH","Pull failed",{},e),{success:!1,error:`Pull failed: ${e.message}`}}}Zc();zi();var Dl=class extends gr{constructor(r){super();this.settingsManager=r}setupRoutes(r){r.get("/api/settings",this.handleGetSettings.bind(this)),r.post("/api/settings",this.handleUpdateSettings.bind(this)),r.get("/api/mcp/status",this.handleGetMcpStatus.bind(this)),r.post("/api/mcp/toggle",this.handleToggleMcp.bind(this)),r.get("/api/branch/status",this.handleGetBranchStatus.bind(this)),r.post("/api/branch/switch",this.handleSwitchBranch.bind(this)),r.post("/api/branch/update",this.handleUpdateBranch.bind(this))}handleGetSettings=this.wrapHandler((r,t)=>{let s=Wn.default.join((0,Rf.homedir)(),".claude-mem","settings.json"),i=Br.loadFromFile(s);t.json(i)});handleUpdateSettings=this.wrapHandler((r,t)=>{if(r.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let l=parseInt(r.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(isNaN(l)||l<1||l>200){t.status(400).json({success:!1,error:"CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200"});return}}if(r.body.CLAUDE_MEM_WORKER_PORT){let l=parseInt(r.body.CLAUDE_MEM_WORKER_PORT,10);if(isNaN(l)||l<1024||l>65535){t.status(400).json({success:!1,error:"CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535"});return}}let s=this.validateContextSettings(r.body);if(!s.valid){t.status(400).json({success:!1,error:s.error});return}let i=Wn.default.join((0,Rf.homedir)(),".claude-mem","settings.json"),n={env:{}};if((0,br.existsSync)(i)){let l=(0,br.readFileSync)(i,"utf-8");n=JSON.parse(l),n.env||(n.env={})}let o=["CLAUDE_MEM_MODEL","CLAUDE_MEM_CONTEXT_OBSERVATIONS","CLAUDE_MEM_WORKER_PORT","CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES","CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS","CLAUDE_MEM_CONTEXT_FULL_COUNT","CLAUDE_MEM_CONTEXT_FULL_FIELD","CLAUDE_MEM_CONTEXT_SESSION_COUNT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let l of o)r.body[l]!==void 0&&(n.env[l]=r.body[l]);(0,br.writeFileSync)(i,JSON.stringify(n,null,2),"utf-8"),W.info("WORKER","Settings updated"),t.json({success:!0,message:"Settings updated successfully"})});handleGetMcpStatus=this.wrapHandler((r,t)=>{let s=this.isMcpEnabled();t.json({enabled:s})});handleToggleMcp=this.wrapHandler((r,t)=>{let{enabled:s}=r.body;if(typeof s!="boolean"){this.badRequest(t,"enabled must be a boolean");return}this.toggleMcp(s),t.json({success:!0,enabled:this.isMcpEnabled()})});handleGetBranchStatus=this.wrapHandler((r,t)=>{let s=Nl();t.json(s)});handleSwitchBranch=this.wrapHandler(async(r,t)=>{let{branch:s}=r.body;if(!s){t.status(400).json({success:!1,error:"Missing branch parameter"});return}let i=["main","beta/7.0"];if(!i.includes(s)){t.status(400).json({success:!1,error:`Invalid branch. Allowed: ${i.join(", ")}`});return}W.info("WORKER","Branch switch requested",{branch:s});let n=await zw(s);n.success&&setTimeout(()=>{W.info("WORKER","Restarting worker after branch switch"),process.exit(0)},1e3),t.json(n)});handleUpdateBranch=this.wrapHandler(async(r,t)=>{W.info("WORKER","Branch update requested");let s=await Bw();s.success&&setTimeout(()=>{W.info("WORKER","Restarting worker after branch update"),process.exit(0)},1e3),t.json(s)});validateContextSettings(r){let t=["CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let s of t)if(r[s]&&!["true","false"].includes(r[s]))return{valid:!1,error:`${s} must be "true" or "false"`};if(r.CLAUDE_MEM_CONTEXT_FULL_COUNT){let s=parseInt(r.CLAUDE_MEM_CONTEXT_FULL_COUNT,10);if(isNaN(s)||s<0||s>20)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20"}}if(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT){let s=parseInt(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT,10);if(isNaN(s)||s<1||s>50)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50"}}if(r.CLAUDE_MEM_CONTEXT_FULL_FIELD&&!["narrative","facts"].includes(r.CLAUDE_MEM_CONTEXT_FULL_FIELD))return{valid:!1,error:'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"'};if(r.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES){let s=r.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES.split(",").map(i=>i.trim());for(let i of s)if(i&&!Cn.includes(i))return{valid:!1,error:`Invalid observation type: ${i}. Valid types: ${Cn.join(", ")}`}}if(r.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS){let s=r.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS.split(",").map(i=>i.trim());for(let i of s)if(i&&!In.includes(i))return{valid:!1,error:`Invalid observation concept: ${i}. Valid concepts: ${In.join(", ")}`}}return{valid:!0}}isMcpEnabled(){let r=sa(),t=Wn.default.join(r,"plugin",".mcp.json");return(0,br.existsSync)(t)}toggleMcp(r){try{let t=sa(),s=Wn.default.join(t,"plugin",".mcp.json"),i=Wn.default.join(t,"plugin",".mcp.json.disabled");r&&(0,br.existsSync)(i)?((0,br.renameSync)(i,s),W.info("WORKER","MCP search server enabled")):!r&&(0,br.existsSync)(s)?((0,br.renameSync)(s,i),W.info("WORKER","MCP search server disabled")):W.debug("WORKER","MCP toggle no-op (already in desired state)",{enabled:r})}catch(t){throw W.failure("WORKER","Failed to toggle MCP",{enabled:r},t),t}}};var $l=class{app;server=null;startTime=Date.now();mcpClient;dbManager;sessionManager;sseBroadcaster;sdkAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;constructor(){this.app=(0,Hw.default)(),this.dbManager=new tl,this.sessionManager=new rl(this.dbManager),this.sseBroadcaster=new al,this.sdkAgent=new bl(this.dbManager,this.sessionManager),this.paginationHelper=new xl(this.dbManager),this.settingsManager=new _l(this.dbManager),this.sessionEventBroadcaster=new Tl(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Rn({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Rl(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Ol(this.sessionManager,this.dbManager,this.sdkAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new kl(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new Dl(this.settingsManager),this.setupMiddleware(),this.setupRoutes()}setupMiddleware(){Aw(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){this.app.get("/api/health",(e,r)=>{r.status(200).json({status:"ok"})}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app)}async start(){let e=An();this.server=await new Promise((r,t)=>{let s=this.app.listen(e,()=>r(s));s.on("error",t)}),W.info("SYSTEM","Worker started",{port:e,pid:process.pid}),this.initializeBackground().catch(r=>{W.error("SYSTEM","Background initialization failed",{},r)})}async initializeBackground(){await this.dbManager.initialize();let e=new El,r=new wl,t=new Sl(this.dbManager.getSessionSearch(),this.dbManager.getSessionStore(),this.dbManager.getChromaSync(),e,r);this.searchRoutes=new Al(t),this.searchRoutes.setupRoutes(this.app),W.info("WORKER","SearchManager initialized and search routes registered");let s=Vw.default.join(__dirname,"..","..","plugin","scripts","mcp-server.cjs"),i=new kn({command:"node",args:[s],env:process.env});await this.mcpClient.connect(i),W.success("WORKER","Connected to MCP server")}async shutdown(){if(await this.sessionManager.shutdownAll(),this.mcpClient)try{await this.mcpClient.close(),W.info("SYSTEM","MCP client closed")}catch(e){W.error("SYSTEM","Failed to close MCP client",{},e)}this.server&&await new Promise((e,r)=>{this.server.close(t=>t?r(t):e())}),await this.dbManager.close(),W.info("SYSTEM","Worker shutdown complete")}summarizeRequestBody(e,r,t){return Nw(e,r,t)}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),t=this.sessionManager.getActiveSessionCount();W.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:t}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};if(require.main===module||!module.parent){let a=new $l;process.on("SIGTERM",async()=>{W.info("SYSTEM","Received SIGTERM, shutting down gracefully"),await a.shutdown(),process.exit(0)}),process.on("SIGINT",async()=>{W.info("SYSTEM","Received SIGINT, shutting down gracefully"),await a.shutdown(),process.exit(0)}),a.start().catch(e=>{W.failure("SYSTEM","Worker failed to start",{},e),process.exit(1)})}0&&(module.exports={WorkerService}); + `).all().map(o=>o.project);t.json({projects:n})});handleGetProcessingStatus=this.wrapHandler((r,t)=>{let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalActiveWork();t.json({isProcessing:s,queueDepth:i})});handleSetProcessing=this.wrapHandler((r,t)=>{this.workerService.broadcastProcessingStatus();let s=this.sessionManager.isAnySessionProcessing(),i=this.sessionManager.getTotalQueueDepth(),n=this.sessionManager.getActiveSessionCount();t.json({status:"ok",isProcessing:s})});parsePaginationParams(r){let t=parseInt(r.query.offset,10)||0,s=Math.min(parseInt(r.query.limit,10)||20,100),i=r.query.project;return{offset:t,limit:s,project:i}}};var Al=class extends gr{constructor(r){super();this.searchManager=r}setupRoutes(r){r.get("/api/search",this.handleUnifiedSearch.bind(this)),r.get("/api/timeline",this.handleUnifiedTimeline.bind(this)),r.get("/api/decisions",this.handleDecisions.bind(this)),r.get("/api/changes",this.handleChanges.bind(this)),r.get("/api/how-it-works",this.handleHowItWorks.bind(this)),r.get("/api/search/observations",this.handleSearchObservations.bind(this)),r.get("/api/search/sessions",this.handleSearchSessions.bind(this)),r.get("/api/search/prompts",this.handleSearchPrompts.bind(this)),r.get("/api/search/by-concept",this.handleSearchByConcept.bind(this)),r.get("/api/search/by-file",this.handleSearchByFile.bind(this)),r.get("/api/search/by-type",this.handleSearchByType.bind(this)),r.get("/api/context/recent",this.handleGetRecentContext.bind(this)),r.get("/api/context/timeline",this.handleGetContextTimeline.bind(this)),r.get("/api/context/preview",this.handleContextPreview.bind(this)),r.get("/api/context/inject",this.handleContextInject.bind(this)),r.get("/api/timeline/by-query",this.handleGetTimelineByQuery.bind(this)),r.get("/api/search/help",this.handleSearchHelp.bind(this))}handleUnifiedSearch=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.search(r.query);t.json(s.content)});handleUnifiedTimeline=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.timeline(r.query);t.json(s.content)});handleDecisions=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.decisions(r.query);t.json(s.content)});handleChanges=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.changes(r.query);t.json(s.content)});handleHowItWorks=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.howItWorks(r.query);t.json(s.content)});handleSearchObservations=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchObservations(r.query);t.json(s.content)});handleSearchSessions=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchSessions(r.query);t.json(s.content)});handleSearchPrompts=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.searchUserPrompts(r.query);t.json(s.content)});handleSearchByConcept=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByConcept(r.query);t.json(s.content)});handleSearchByFile=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByFile(r.query);t.json(s.content)});handleSearchByType=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.findByType(r.query);t.json(s.content)});handleGetRecentContext=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getRecentContext(r.query);t.json(s.content)});handleGetContextTimeline=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getContextTimeline(r.query);t.json(s.content)});handleContextPreview=this.wrapHandler(async(r,t)=>{let s=r.query.project;if(!s){this.badRequest(t,"Project parameter is required");return}let{generateContext:i}=await Promise.resolve().then(()=>(wf(),Ef)),n=`/preview/${s}`,o=await i({session_id:"preview-"+Date.now(),cwd:n},!0);t.setHeader("Content-Type","text/plain; charset=utf-8"),t.send(o)});handleContextInject=this.wrapHandler(async(r,t)=>{let s=r.query.project,i=r.query.colors==="true";if(!s){this.badRequest(t,"Project parameter is required");return}let{generateContext:n}=await Promise.resolve().then(()=>(wf(),Ef)),o=`/context/${s}`,l=await n({session_id:"context-inject-"+Date.now(),cwd:o},i);t.setHeader("Content-Type","text/plain; charset=utf-8"),t.send(l)});handleGetTimelineByQuery=this.wrapHandler(async(r,t)=>{let s=await this.searchManager.getTimelineByQuery(r.query);t.json(s.content)});handleSearchHelp=this.wrapHandler((r,t)=>{t.json({title:"Claude-Mem Search API",description:"HTTP API for searching persistent memory",endpoints:[{path:"/api/search/observations",method:"GET",description:"Search observations using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/sessions",method:"GET",description:"Search session summaries using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)"}},{path:"/api/search/prompts",method:"GET",description:"Search user prompts using full-text search",parameters:{query:"Search query (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/by-concept",method:"GET",description:"Find observations by concept tag",parameters:{concept:"Concept tag (required): discovery, decision, bugfix, feature, refactor",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-file",method:"GET",description:"Find observations and sessions by file path",parameters:{filePath:"File path or partial path (required)",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results per type (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-type",method:"GET",description:"Find observations by type",parameters:{type:"Observation type (required): discovery, decision, bugfix, feature, refactor",format:'Response format: "index" or "full" (default: "full")',limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/context/recent",method:"GET",description:"Get recent session context including summaries and observations",parameters:{project:"Project name (default: current directory)",limit:"Number of recent sessions (default: 3)"}},{path:"/api/context/timeline",method:"GET",description:"Get unified timeline around a specific point in time",parameters:{anchor:'Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp (required)',depth_before:"Number of records before anchor (default: 10)",depth_after:"Number of records after anchor (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/timeline/by-query",method:"GET",description:"Search for best match, then get timeline around it",parameters:{query:"Search query (required)",mode:'Search mode: "auto", "observations", or "sessions" (default: "auto")',depth_before:"Number of records before match (default: 10)",depth_after:"Number of records after match (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/help",method:"GET",description:"Get this help documentation"}],examples:['curl "http://localhost:37777/api/search/observations?query=authentication&format=index&limit=5"','curl "http://localhost:37777/api/search/by-type?type=bugfix&limit=10"','curl "http://localhost:37777/api/context/recent?project=claude-mem&limit=3"','curl "http://localhost:37777/api/context/timeline?anchor=123&depth_before=5&depth_after=5"']})})};var Wn=gt(require("path"),1),br=require("fs"),Rf=require("os");xs();wt();var Tf=require("child_process"),Fs=require("fs"),Fw=require("os"),Ji=require("path");wt();var Yi=(0,Ji.join)((0,Fw.homedir)(),".claude","plugins","marketplaces","thedotmack");function yr(a){return(0,Tf.execSync)(`git ${a}`,{cwd:Yi,encoding:"utf-8",timeout:3e4}).trim()}function Uw(a,e=6e4){return(0,Tf.execSync)(a,{cwd:Yi,encoding:"utf-8",timeout:e}).trim()}function Nl(){let a=(0,Ji.join)(Yi,".git");if(!(0,Fs.existsSync)(a))return{branch:null,isBeta:!1,isGitRepo:!1,isDirty:!1,canSwitch:!1,error:"Installed plugin is not a git repository"};try{let e=yr("rev-parse --abbrev-ref HEAD"),t=yr("status --porcelain").length>0,s=e.startsWith("beta");return{branch:e,isBeta:s,isGitRepo:!0,isDirty:t,canSwitch:!0}}catch(e){return W.error("BRANCH","Failed to get branch info",{},e),{branch:null,isBeta:!1,isGitRepo:!0,isDirty:!1,canSwitch:!1,error:e.message}}}async function zw(a){let e=Nl();if(!e.isGitRepo)return{success:!1,error:"Installed plugin is not a git repository. Please reinstall."};if(e.branch===a)return{success:!0,branch:a,message:`Already on branch ${a}`};try{W.info("BRANCH","Starting branch switch",{from:e.branch,to:a}),W.debug("BRANCH","Discarding local changes"),yr("checkout -- ."),yr("clean -fd"),W.debug("BRANCH","Fetching from origin"),yr("fetch origin"),W.debug("BRANCH","Checking out branch",{branch:a});try{yr(`checkout ${a}`)}catch{yr(`checkout -b ${a} origin/${a}`)}W.debug("BRANCH","Pulling latest"),yr(`pull origin ${a}`);let r=(0,Ji.join)(Yi,".install-version");return(0,Fs.existsSync)(r)&&(0,Fs.unlinkSync)(r),W.debug("BRANCH","Running npm install"),Uw("npm install",12e4),W.success("BRANCH","Branch switch complete",{branch:a}),{success:!0,branch:a,message:`Switched to ${a}. Worker will restart automatically.`}}catch(r){W.error("BRANCH","Branch switch failed",{targetBranch:a},r);try{e.branch&&yr(`checkout ${e.branch}`)}catch{}return{success:!1,error:`Branch switch failed: ${r.message}`}}}async function Bw(){let a=Nl();if(!a.isGitRepo||!a.branch)return{success:!1,error:"Cannot pull updates: not a git repository"};try{W.info("BRANCH","Pulling updates",{branch:a.branch}),yr("checkout -- ."),yr("fetch origin"),yr(`pull origin ${a.branch}`);let e=(0,Ji.join)(Yi,".install-version");return(0,Fs.existsSync)(e)&&(0,Fs.unlinkSync)(e),Uw("npm install",12e4),W.success("BRANCH","Updates pulled",{branch:a.branch}),{success:!0,branch:a.branch,message:`Updated ${a.branch}. Worker will restart automatically.`}}catch(e){return W.error("BRANCH","Pull failed",{},e),{success:!1,error:`Pull failed: ${e.message}`}}}Zc();zi();var Dl=class extends gr{constructor(r){super();this.settingsManager=r}setupRoutes(r){r.get("/api/settings",this.handleGetSettings.bind(this)),r.post("/api/settings",this.handleUpdateSettings.bind(this)),r.get("/api/mcp/status",this.handleGetMcpStatus.bind(this)),r.post("/api/mcp/toggle",this.handleToggleMcp.bind(this)),r.get("/api/branch/status",this.handleGetBranchStatus.bind(this)),r.post("/api/branch/switch",this.handleSwitchBranch.bind(this)),r.post("/api/branch/update",this.handleUpdateBranch.bind(this))}handleGetSettings=this.wrapHandler((r,t)=>{let s=Wn.default.join((0,Rf.homedir)(),".claude-mem","settings.json"),i=Br.loadFromFile(s);t.json(i)});handleUpdateSettings=this.wrapHandler((r,t)=>{if(r.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let l=parseInt(r.body.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(isNaN(l)||l<1||l>200){t.status(400).json({success:!1,error:"CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200"});return}}if(r.body.CLAUDE_MEM_WORKER_PORT){let l=parseInt(r.body.CLAUDE_MEM_WORKER_PORT,10);if(isNaN(l)||l<1024||l>65535){t.status(400).json({success:!1,error:"CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535"});return}}let s=this.validateContextSettings(r.body);if(!s.valid){t.status(400).json({success:!1,error:s.error});return}let i=Wn.default.join((0,Rf.homedir)(),".claude-mem","settings.json"),n={env:{}};if((0,br.existsSync)(i)){let l=(0,br.readFileSync)(i,"utf-8");n=JSON.parse(l),n.env||(n.env={})}let o=["CLAUDE_MEM_MODEL","CLAUDE_MEM_CONTEXT_OBSERVATIONS","CLAUDE_MEM_WORKER_PORT","CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES","CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS","CLAUDE_MEM_CONTEXT_FULL_COUNT","CLAUDE_MEM_CONTEXT_FULL_FIELD","CLAUDE_MEM_CONTEXT_SESSION_COUNT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let l of o)r.body[l]!==void 0&&(n.env[l]=r.body[l]);(0,br.writeFileSync)(i,JSON.stringify(n,null,2),"utf-8"),W.info("WORKER","Settings updated"),t.json({success:!0,message:"Settings updated successfully"})});handleGetMcpStatus=this.wrapHandler((r,t)=>{let s=this.isMcpEnabled();t.json({enabled:s})});handleToggleMcp=this.wrapHandler((r,t)=>{let{enabled:s}=r.body;if(typeof s!="boolean"){this.badRequest(t,"enabled must be a boolean");return}this.toggleMcp(s),t.json({success:!0,enabled:this.isMcpEnabled()})});handleGetBranchStatus=this.wrapHandler((r,t)=>{let s=Nl();t.json(s)});handleSwitchBranch=this.wrapHandler(async(r,t)=>{let{branch:s}=r.body;if(!s){t.status(400).json({success:!1,error:"Missing branch parameter"});return}let i=["main","beta/7.0"];if(!i.includes(s)){t.status(400).json({success:!1,error:`Invalid branch. Allowed: ${i.join(", ")}`});return}W.info("WORKER","Branch switch requested",{branch:s});let n=await zw(s);n.success&&setTimeout(()=>{W.info("WORKER","Restarting worker after branch switch"),process.exit(0)},1e3),t.json(n)});handleUpdateBranch=this.wrapHandler(async(r,t)=>{W.info("WORKER","Branch update requested");let s=await Bw();s.success&&setTimeout(()=>{W.info("WORKER","Restarting worker after branch update"),process.exit(0)},1e3),t.json(s)});validateContextSettings(r){let t=["CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let s of t)if(r[s]&&!["true","false"].includes(r[s]))return{valid:!1,error:`${s} must be "true" or "false"`};if(r.CLAUDE_MEM_CONTEXT_FULL_COUNT){let s=parseInt(r.CLAUDE_MEM_CONTEXT_FULL_COUNT,10);if(isNaN(s)||s<0||s>20)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20"}}if(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT){let s=parseInt(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT,10);if(isNaN(s)||s<1||s>50)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50"}}if(r.CLAUDE_MEM_CONTEXT_FULL_FIELD&&!["narrative","facts"].includes(r.CLAUDE_MEM_CONTEXT_FULL_FIELD))return{valid:!1,error:'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"'};if(r.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES){let s=r.CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES.split(",").map(i=>i.trim());for(let i of s)if(i&&!Cn.includes(i))return{valid:!1,error:`Invalid observation type: ${i}. Valid types: ${Cn.join(", ")}`}}if(r.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS){let s=r.CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS.split(",").map(i=>i.trim());for(let i of s)if(i&&!In.includes(i))return{valid:!1,error:`Invalid observation concept: ${i}. Valid concepts: ${In.join(", ")}`}}return{valid:!0}}isMcpEnabled(){let r=sa(),t=Wn.default.join(r,"plugin",".mcp.json");return(0,br.existsSync)(t)}toggleMcp(r){try{let t=sa(),s=Wn.default.join(t,"plugin",".mcp.json"),i=Wn.default.join(t,"plugin",".mcp.json.disabled");r&&(0,br.existsSync)(i)?((0,br.renameSync)(i,s),W.info("WORKER","MCP search server enabled")):!r&&(0,br.existsSync)(s)?((0,br.renameSync)(s,i),W.info("WORKER","MCP search server disabled")):W.debug("WORKER","MCP toggle no-op (already in desired state)",{enabled:r})}catch(t){throw W.failure("WORKER","Failed to toggle MCP",{enabled:r},t),t}}};var $l=class{app;server=null;startTime=Date.now();mcpClient;dbManager;sessionManager;sseBroadcaster;sdkAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;constructor(){this.app=(0,Hw.default)(),this.dbManager=new tl,this.sessionManager=new rl(this.dbManager),this.sseBroadcaster=new al,this.sdkAgent=new bl(this.dbManager,this.sessionManager),this.paginationHelper=new xl(this.dbManager),this.settingsManager=new _l(this.dbManager),this.sessionEventBroadcaster=new Tl(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Rn({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Rl(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Ol(this.sessionManager,this.dbManager,this.sdkAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new kl(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new Dl(this.settingsManager),this.setupMiddleware(),this.setupRoutes()}setupMiddleware(){Aw(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){this.app.get("/api/health",(e,r)=>{r.status(200).json({status:"ok"})}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app)}async start(){let e=An();this.server=await new Promise((r,t)=>{let s=this.app.listen(e,()=>r(s));s.on("error",t)}),W.info("SYSTEM","Worker started",{port:e,pid:process.pid}),this.initializeBackground().catch(r=>{W.error("SYSTEM","Background initialization failed",{},r)})}async initializeBackground(){await this.dbManager.initialize();let e=new El,r=new wl,t=new Sl(this.dbManager.getSessionSearch(),this.dbManager.getSessionStore(),this.dbManager.getChromaSync(),e,r);this.searchRoutes=new Al(t),this.searchRoutes.setupRoutes(this.app),W.info("WORKER","SearchManager initialized and search routes registered");let s=Vw.default.join(__dirname,"mcp-server.cjs"),i=new kn({command:"node",args:[s],env:process.env});await this.mcpClient.connect(i),W.success("WORKER","Connected to MCP server")}async shutdown(){if(await this.sessionManager.shutdownAll(),this.mcpClient)try{await this.mcpClient.close(),W.info("SYSTEM","MCP client closed")}catch(e){W.error("SYSTEM","Failed to close MCP client",{},e)}this.server&&await new Promise((e,r)=>{this.server.close(t=>t?r(t):e())}),await this.dbManager.close(),W.info("SYSTEM","Worker shutdown complete")}summarizeRequestBody(e,r,t){return Nw(e,r,t)}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),t=this.sessionManager.getActiveSessionCount();W.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:t}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};if(require.main===module||!module.parent){let a=new $l;process.on("SIGTERM",async()=>{W.info("SYSTEM","Received SIGTERM, shutting down gracefully"),await a.shutdown(),process.exit(0)}),process.on("SIGINT",async()=>{W.info("SYSTEM","Received SIGINT, shutting down gracefully"),await a.shutdown(),process.exit(0)}),a.start().catch(e=>{W.failure("SYSTEM","Worker failed to start",{},e),process.exit(1)})}0&&(module.exports={WorkerService}); /*! Bundled license information: depd/index.js: diff --git a/scripts/smart-install.js b/scripts/smart-install.js index 104f9ea1..ee79f332 100644 --- a/scripts/smart-install.js +++ b/scripts/smart-install.js @@ -12,25 +12,25 @@ */ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { execSync, spawnSync } from 'child_process'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { execSync, spawnSync, spawn } from 'child_process'; +import { join } from 'path'; import { homedir } from 'os'; +import { createRequire } from 'module'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +// CRITICAL: Always use marketplace directory for ALL operations +// This script may run from the cache directory (plugin/scripts/) but must +// operate on the marketplace directory where package.json and node_modules live. +// This ensures cross-platform compatibility and avoids cache directory confusion. +const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); -// Plugin root is parent directory of scripts/ -const PLUGIN_ROOT = join(__dirname, '..'); +// Use MARKETPLACE_ROOT for all paths - this script can be deployed anywhere +// but always operates on the marketplace directory +const PLUGIN_ROOT = MARKETPLACE_ROOT; const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json'); const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version'); const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules'); const BETTER_SQLITE3_PATH = join(NODE_MODULES_PATH, 'better-sqlite3'); -// CRITICAL: Always use marketplace directory for PM2/ecosystem -// This ensures cross-platform compatibility and avoids cache directory confusion -const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); - // Colors for output const colors = { reset: '\x1b[0m', @@ -155,8 +155,10 @@ async function verifyNativeModules() { try { log('šŸ” Verifying native modules...', colors.dim); - // Try to actually load better-sqlite3 - const { default: Database } = await import('better-sqlite3'); + // CRITICAL: Use createRequire() to resolve from MARKETPLACE_ROOT + // This script may run from cache but must load modules from marketplace's node_modules + const require = createRequire(join(MARKETPLACE_ROOT, 'package.json')); + const Database = require('better-sqlite3'); // Try to create a test in-memory database const db = new Database(':memory:'); @@ -369,32 +371,10 @@ async function main() { } } - // Try to start the PM2 worker after fresh install - try { - log('šŸš€ Starting worker service...', colors.cyan); - // CRITICAL: Always use marketplace directory for PM2/ecosystem - // This ensures PM2 starts from the correct location regardless of where this script runs from - const localPm2Base = join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2'); - const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base; - const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2'; - const ecosystemPath = join(MARKETPLACE_ROOT, 'ecosystem.config.cjs'); - - // Using spawnSync with array args to avoid command injection risks - const result = spawnSync(pm2Command, ['start', ecosystemPath], { - cwd: MARKETPLACE_ROOT, - stdio: 'pipe', - encoding: 'utf-8' - }); - if (result.status !== 0) { - throw new Error(result.stderr || 'PM2 start failed'); - } - - log('āœ… Worker service started', colors.green); - } catch (error) { - // Worker might already be running or PM2 not available - that's okay - // The ensureWorkerRunning() function will handle auto-start when needed - log('ā„¹ļø Worker will start automatically when needed', colors.dim); - } + // NOTE: Worker auto-start disabled in smart-install.js + // The context-hook.js calls ensureWorkerRunning() which handles worker startup + // This avoids potential process management conflicts during plugin initialization + log('āœ… Installation complete', colors.green); // Success - dependencies installed (if needed) process.exit(0); diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 8804744f..ec9588b8 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -157,7 +157,7 @@ export class WorkerService { logger.info('WORKER', 'SearchManager initialized and search routes registered'); // Connect to MCP server - const mcpServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'mcp-server.cjs'); + const mcpServerPath = path.join(__dirname, 'mcp-server.cjs'); const transport = new StdioClientTransport({ command: 'node', args: [mcpServerPath], diff --git a/src/shared/worker-utils.ts b/src/shared/worker-utils.ts index 4dc84ef9..90a71701 100644 --- a/src/shared/worker-utils.ts +++ b/src/shared/worker-utils.ts @@ -9,9 +9,10 @@ import { SettingsDefaultsManager } from "../services/worker/settings/SettingsDef const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); // Named constants for health checks -const HEALTH_CHECK_TIMEOUT_MS = 100; -const WORKER_STARTUP_WAIT_MS = 500; -const WORKER_STARTUP_RETRIES = 10; +// Windows needs longer timeouts due to startup overhead +const HEALTH_CHECK_TIMEOUT_MS = 500; +const WORKER_STARTUP_WAIT_MS = 1000; +const WORKER_STARTUP_RETRIES = 15; /** * Get the worker port number @@ -39,35 +40,56 @@ async function isWorkerHealthy(): Promise { } /** - * Start the worker using PM2 + * Start the worker service + * On Windows: Uses PowerShell Start-Process with hidden window to avoid console flash + * On Unix: Uses PM2 for process management */ async function startWorker(): Promise { try { - // CRITICAL: Always use marketplace directory for ecosystem.config.cjs - // This ensures PM2 starts from the correct location regardless of where hooks run from - const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs'); + const workerScript = path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'); - if (!existsSync(ecosystemPath)) { - throw new Error(`Ecosystem config not found at ${ecosystemPath}`); + if (!existsSync(workerScript)) { + throw new Error(`Worker script not found at ${workerScript}`); } - // Try to use local PM2 from node_modules first, fall back to global PM2 - // On Windows, PM2 executable is pm2.cmd, not pm2 - const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2'); - const localPm2Cmd = process.platform === 'win32' ? localPm2Base + '.cmd' : localPm2Base; - const pm2Command = existsSync(localPm2Cmd) ? localPm2Cmd : 'pm2'; + if (process.platform === 'win32') { + // On Windows, use PowerShell Start-Process with -WindowStyle Hidden + // This avoids visible console windows that PM2 creates on Windows + const result = spawnSync('powershell.exe', [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Start-Process -FilePath 'node' -ArgumentList '${workerScript}' -WorkingDirectory '${MARKETPLACE_ROOT}' -WindowStyle Hidden` + ], { + cwd: MARKETPLACE_ROOT, + stdio: 'pipe', + encoding: 'utf-8', + windowsHide: true + }); - // Start using PM2 with the ecosystem config - // CRITICAL: Must set cwd to MARKETPLACE_ROOT so PM2 starts from marketplace directory - // Using spawnSync with array args to avoid command injection risks - const result = spawnSync(pm2Command, ['start', ecosystemPath], { - cwd: MARKETPLACE_ROOT, - stdio: 'pipe', - encoding: 'utf-8', - windowsHide: true - }); - if (result.status !== 0) { - throw new Error(result.stderr || 'PM2 start failed'); + if (result.status !== 0) { + throw new Error(result.stderr || 'PowerShell Start-Process failed'); + } + } else { + // On Unix, use PM2 for process management + const ecosystemPath = path.join(MARKETPLACE_ROOT, 'ecosystem.config.cjs'); + + if (!existsSync(ecosystemPath)) { + throw new Error(`Ecosystem config not found at ${ecosystemPath}`); + } + + const localPm2Base = path.join(MARKETPLACE_ROOT, 'node_modules', '.bin', 'pm2'); + const pm2Command = existsSync(localPm2Base) ? localPm2Base : 'pm2'; + + const result = spawnSync(pm2Command, ['start', ecosystemPath], { + cwd: MARKETPLACE_ROOT, + stdio: 'pipe', + encoding: 'utf-8' + }); + + if (result.status !== 0) { + throw new Error(result.stderr || 'PM2 start failed'); + } } // Wait for worker to become healthy From d7dc29498ccb441c18d5337e40c0d9c6384aa0a5 Mon Sep 17 00:00:00 2001 From: kat-bell <224458406+kat-bell@users.noreply.github.com> Date: Tue, 9 Dec 2025 05:27:26 -0600 Subject: [PATCH 2/2] fix(cache): Add package.json to plugin directory for cache dependency resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled hook scripts use `external: ['better-sqlite3']` during esbuild, meaning the dependency must be resolved at runtime. When hooks run from the cache directory (~/.claude/plugins/cache/thedotmack/claude-mem/X.X.X/), they couldn't find better-sqlite3 because: 1. Cache directory had no package.json 2. smart-install.js was hardcoded to install in marketplace directory only This fix: - Adds plugin/package.json declaring runtime dependencies (better-sqlite3) - Updates build-hooks.js to auto-generate plugin/package.json from main package.json - Updates smart-install.js to detect execution context (cache vs marketplace) and install dependencies in the correct location The script now detects if it's running from cache (via path pattern matching) and installs dependencies there, where the hooks actually execute. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- plugin/package.json | 13 ++++++ plugin/scripts/smart-install.js | 73 ++++++++++++++++++++------------- scripts/build-hooks.js | 20 +++++++++ 3 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 plugin/package.json diff --git a/plugin/package.json b/plugin/package.json new file mode 100644 index 00000000..519346e8 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "claude-mem-plugin", + "version": "7.0.3", + "private": true, + "description": "Runtime dependencies for claude-mem bundled hooks", + "type": "module", + "dependencies": { + "better-sqlite3": "^12.5.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/plugin/scripts/smart-install.js b/plugin/scripts/smart-install.js index ee79f332..87872141 100644 --- a/plugin/scripts/smart-install.js +++ b/plugin/scripts/smart-install.js @@ -4,6 +4,8 @@ * Smart Install Script for claude-mem * * Features: + * - Detects execution context (cache vs marketplace directory) + * - Installs dependencies where the hooks actually run (cache directory) * - Only runs npm install when necessary (version change or missing deps) * - Caches installation state with version marker * - Provides helpful Windows-specific error messages @@ -12,20 +14,30 @@ */ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { execSync, spawnSync, spawn } from 'child_process'; -import { join } from 'path'; +import { execSync } from 'child_process'; +import { join, dirname } from 'path'; import { homedir } from 'os'; import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; -// CRITICAL: Always use marketplace directory for ALL operations -// This script may run from the cache directory (plugin/scripts/) but must -// operate on the marketplace directory where package.json and node_modules live. -// This ensures cross-platform compatibility and avoids cache directory confusion. -const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); +// Determine the directory where THIS script is running from +// This could be either: +// 1. Cache: ~/.claude/plugins/cache/thedotmack/claude-mem/X.X.X/scripts/ +// 2. Marketplace: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/ +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SCRIPT_ROOT = dirname(__dirname); // Parent of scripts/ directory + +// Detect if running from cache directory (has version number in path) +const CACHE_PATTERN = /[/\\]cache[/\\]thedotmack[/\\]claude-mem[/\\]\d+\.\d+\.\d+/; +const IS_RUNNING_FROM_CACHE = CACHE_PATTERN.test(__dirname); + +// Set PLUGIN_ROOT based on where we're running +// If from cache, install dependencies IN the cache directory (where hooks run) +// If from marketplace, use marketplace directory +const PLUGIN_ROOT = IS_RUNNING_FROM_CACHE + ? SCRIPT_ROOT // Cache directory (e.g., ~/.claude/plugins/cache/thedotmack/claude-mem/7.0.3/) + : join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack'); -// Use MARKETPLACE_ROOT for all paths - this script can be deployed anywhere -// but always operates on the marketplace directory -const PLUGIN_ROOT = MARKETPLACE_ROOT; const PACKAGE_JSON_PATH = join(PLUGIN_ROOT, 'package.json'); const VERSION_MARKER_PATH = join(PLUGIN_ROOT, '.install-version'); const NODE_MODULES_PATH = join(PLUGIN_ROOT, 'node_modules'); @@ -102,6 +114,12 @@ function setInstalledVersion(packageVersion, nodeVersion) { } function needsInstall() { + // Check if package.json exists (required for npm install) + if (!existsSync(PACKAGE_JSON_PATH)) { + log(`āš ļø No package.json found at ${PLUGIN_ROOT}`, colors.yellow); + return false; // Can't install without package.json + } + // Check if node_modules exists if (!existsSync(NODE_MODULES_PATH)) { log('šŸ“¦ Dependencies not found - first time setup', colors.cyan); @@ -155,9 +173,8 @@ async function verifyNativeModules() { try { log('šŸ” Verifying native modules...', colors.dim); - // CRITICAL: Use createRequire() to resolve from MARKETPLACE_ROOT - // This script may run from cache but must load modules from marketplace's node_modules - const require = createRequire(join(MARKETPLACE_ROOT, 'package.json')); + // Use createRequire() to resolve from PLUGIN_ROOT's node_modules + const require = createRequire(join(PLUGIN_ROOT, 'package.json')); const Database = require('better-sqlite3'); // Try to create a test in-memory database @@ -247,7 +264,8 @@ async function runNpmInstall() { const isWindows = process.platform === 'win32'; log('', colors.cyan); - log('šŸ”Ø Installing dependencies...', colors.bright); + log(`šŸ”Ø Installing dependencies in ${IS_RUNNING_FROM_CACHE ? 'cache' : 'marketplace'}...`, colors.bright); + log(` ${PLUGIN_ROOT}`, colors.dim); log('', colors.reset); // Try normal install first, then retry with force if it fails @@ -274,7 +292,7 @@ async function runNpmInstall() { throw new Error('better-sqlite3 installation verification failed'); } - // NEW: Verify native modules actually work + // Verify native modules actually work const nativeModulesWork = await verifyNativeModules(); if (!nativeModulesWork) { throw new Error('Native modules failed to load after install'); @@ -331,16 +349,15 @@ async function runNpmInstall() { return false; } -/** - * Check if we should fail when worker startup fails - * Returns true if worker failed AND dependencies are missing - */ -function shouldFailOnWorkerStartup(workerStarted) { - return !workerStarted && !existsSync(NODE_MODULES_PATH); -} - async function main() { try { + // Log execution context for debugging + if (IS_RUNNING_FROM_CACHE) { + log('šŸ“ Running from cache directory', colors.dim); + } else { + log('šŸ“ Running from marketplace directory', colors.dim); + } + // Check if we need to install dependencies const installNeeded = needsInstall(); @@ -355,7 +372,7 @@ async function main() { process.exit(1); } } else { - // NEW: Even if install not needed, verify native modules work + // Even if install not needed, verify native modules work const nativeModulesWork = await verifyNativeModules(); if (!nativeModulesWork) { @@ -371,10 +388,10 @@ async function main() { } } - // NOTE: Worker auto-start disabled in smart-install.js - // The context-hook.js calls ensureWorkerRunning() which handles worker startup - // This avoids potential process management conflicts during plugin initialization - log('āœ… Installation complete', colors.green); + // NOTE: Worker auto-start disabled in smart-install.js + // The context-hook.js calls ensureWorkerRunning() which handles worker startup + // This avoids potential process management conflicts during plugin initialization + log('āœ… Installation complete', colors.green); // Success - dependencies installed (if needed) process.exit(0); diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index 5b49fc26..224f64a0 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -58,6 +58,26 @@ async function buildHooks() { } console.log('āœ“ Output directories ready'); + // Generate plugin/package.json for cache directory dependency installation + // The bundled hooks use `external: ['better-sqlite3']` so dependencies must be + // installed at runtime. This package.json enables npm install in the cache directory. + console.log('\nšŸ“¦ Generating plugin package.json...'); + const pluginPackageJson = { + name: 'claude-mem-plugin', + version: version, + private: true, + description: 'Runtime dependencies for claude-mem bundled hooks', + type: 'module', + dependencies: { + 'better-sqlite3': packageJson.dependencies['better-sqlite3'] + }, + engines: { + node: '>=18.0.0' + } + }; + fs.writeFileSync('plugin/package.json', JSON.stringify(pluginPackageJson, null, 2) + '\n'); + console.log('āœ“ plugin/package.json generated'); + // Build React viewer console.log('\nšŸ“‹ Building React viewer...'); const { spawn } = await import('child_process');