86d0d1a21a
Added support for OpenRouter as an alternative LLM provider with new settings for API key, model selection, and app metadata configuration. Enhanced context generation with improved settings management and updated worker service APIs. Includes UI updates for context settings and new observation type configurations.
20 lines
14 KiB
JavaScript
Executable File
20 lines
14 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
import{existsSync as P,readFileSync as rt,writeFileSync as nt,unlinkSync as st,mkdirSync as N}from"fs";import{createWriteStream as ot}from"fs";import{join as S}from"path";import{spawn as it,spawnSync as at}from"child_process";import{homedir as ct}from"os";import{join as p,dirname as q,basename as Lt}from"path";import{homedir as z}from"os";import{fileURLToPath as Q}from"url";import{readFileSync as V,writeFileSync as j,existsSync as X}from"fs";import{join as Y}from"path";import{homedir as J}from"os";var b="bugfix,feature,refactor,discovery,decision,change",k="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var A=(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))(A||{}),C=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=l.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=A[t]??1}return this.level}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.getLevel()===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;let r=typeof e=="string"?JSON.parse(e):e;if(t==="Bash"&&r.command)return`${t}(${r.command})`;if(r.file_path)return`${t}(${r.file_path})`;if(r.notebook_path)return`${t}(${r.notebook_path})`;if(t==="Glob"&&r.pattern)return`${t}(${r.pattern})`;if(t==="Grep"&&r.pattern)return`${t}(${r.pattern})`;if(r.url)return`${t}(${r.url})`;if(r.query)return`${t}(${r.query})`;if(t==="Task"){if(r.subagent_type)return`${t}(${r.subagent_type})`;if(r.description)return`${t}(${r.description})`}return t==="Skill"&&r.skill?`${t}(${r.skill})`:t==="LSP"&&r.operation?`${t}(${r.operation})`:t}formatTimestamp(t){let e=t.getFullYear(),r=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),o=String(t.getMinutes()).padStart(2,"0"),a=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${e}-${r}-${n} ${s}:${o}:${a}.${c}`}log(t,e,r,n,s){if(t<this.getLevel())return;let o=this.formatTimestamp(new Date),a=A[t].padEnd(5),c=e.padEnd(6),E="";n?.correlationId?E=`[${n.correlationId}] `:n?.sessionId&&(E=`[session-${n.sessionId}] `);let _="";s!=null&&(this.getLevel()===0&&typeof s=="object"?_=`
|
|
`+JSON.stringify(s,null,2):_=" "+this.formatData(s));let f="";if(n){let{sessionId:T,sdkSessionId:gt,correlationId:_t,...U}=n;Object.keys(U).length>0&&(f=` {${Object.entries(U).map(([K,B])=>`${K}=${B}`).join(", ")}}`)}let m=`[${o}] [${a}] [${c}] ${E}${r}${f}${_}`;t===3?console.error(m):console.log(m)}debug(t,e,r,n){this.log(0,t,e,r,n)}info(t,e,r,n){this.log(1,t,e,r,n)}warn(t,e,r,n){this.log(2,t,e,r,n)}error(t,e,r,n){this.log(3,t,e,r,n)}dataIn(t,e,r,n){this.info(t,`\u2192 ${e}`,r,n)}dataOut(t,e,r,n){this.info(t,`\u2190 ${e}`,r,n)}success(t,e,r,n){this.info(t,`\u2713 ${e}`,r,n)}failure(t,e,r,n){this.error(t,`\u2717 ${e}`,r,n)}timing(t,e,r,n){this.info(t,`\u23F1 ${e}`,n,{duration:`${r}ms`})}happyPathError(t,e,r,n,s=""){let E=((new Error().stack||"").split(`
|
|
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),_=E?`${E[1].split("/").pop()}:${E[2]}`:"unknown",f={...r,location:_};return this.warn(t,`[HAPPY-PATH] ${e}`,f,n),s}},h=new C;var l=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:Y(J(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:b,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:k,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let e=this.get(t);return parseInt(e,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!X(t))return this.getAllDefaults();let e=V(t,"utf-8"),r=JSON.parse(e),n=r;if(r.env&&typeof r.env=="object"){n=r.env;try{j(t,JSON.stringify(n,null,2),"utf-8"),h.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(o){h.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},o)}}let s={...this.DEFAULTS};for(let o of Object.keys(this.DEFAULTS))n[o]!==void 0&&(s[o]=n[o]);return s}catch(e){return h.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},e),this.getAllDefaults()}}};function Z(){return typeof __dirname<"u"?__dirname:q(Q(import.meta.url))}var Ut=Z(),u=l.get("CLAUDE_MEM_DATA_DIR"),L=process.env.CLAUDE_CONFIG_DIR||p(z(),".claude"),bt=p(u,"archives"),kt=p(u,"logs"),yt=p(u,"trash"),vt=p(u,"backups"),$t=p(u,"modes"),Nt=p(u,"settings.json"),xt=p(u,"claude-mem.db"),Wt=p(u,"vector-db"),Ft=p(L,"settings.json"),Ht=p(L,"commands"),Gt=p(L,"CLAUDE.md");import{spawnSync as tt}from"child_process";import{existsSync as et}from"fs";import{join as y}from"path";import{homedir as v}from"os";function w(){let i=process.platform==="win32";try{if(tt("bun",["--version"],{encoding:"utf-8",stdio:["pipe","pipe","pipe"],shell:!1}).status===0)return"bun"}catch{}let t=i?[y(v(),".bun","bin","bun.exe")]:[y(v(),".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun","/home/linuxbrew/.linuxbrew/bin/bun"];for(let e of t)if(et(e))return e;return null}function $(){return w()!==null}var d=S(u,"worker.pid"),x=S(u,"logs"),R=S(ct(),".claude","plugins","marketplaces","thedotmack"),g=class{static async start(t){if(isNaN(t)||t<1024||t>65535)return{success:!1,error:`Invalid port ${t}. Must be between 1024 and 65535`};if(await this.isRunning())return{success:!0,pid:this.getPidInfo()?.pid};N(x,{recursive:!0});let e=process.platform==="win32"?"worker-wrapper.cjs":"worker-service.cjs",r=S(R,"plugin","scripts",e);if(!P(r))return{success:!1,error:`Worker script not found at ${r}`};let n=this.getLogFilePath();return this.startWithBun(r,n,t)}static isBunAvailable(){return $()}static escapePowerShellString(t){return t.replace(/'/g,"''")}static async startWithBun(t,e,r){let n=w();if(!n)return{success:!1,error:"Bun is required but not found in PATH or common installation paths. Install from https://bun.sh"};try{if(process.platform==="win32"){let o=this.escapePowerShellString(n),a=this.escapePowerShellString(t),c=this.escapePowerShellString(R),E=this.escapePowerShellString(e),f=`${`$env:CLAUDE_MEM_WORKER_PORT='${r}'`}; Start-Process -FilePath '${o}' -ArgumentList '${a}' -WorkingDirectory '${c}' -WindowStyle Hidden -RedirectStandardOutput '${E}' -RedirectStandardError '${E}.err' -PassThru | Select-Object -ExpandProperty Id`,m=at("powershell",["-Command",f],{stdio:"pipe",timeout:1e4,windowsHide:!0});if(m.status!==0)return{success:!1,error:`PowerShell spawn failed: ${m.stderr?.toString()||"unknown error"}`};let T=parseInt(m.stdout.toString().trim(),10);return isNaN(T)?{success:!1,error:"Failed to get PID from PowerShell"}:(this.writePidFile({pid:T,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(T,r))}else{let o=it(n,[t],{detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,CLAUDE_MEM_WORKER_PORT:String(r)},cwd:R}),a=ot(e,{flags:"a"});return o.stdout?.pipe(a),o.stderr?.pipe(a),o.unref(),o.pid?(this.writePidFile({pid:o.pid,port:r,startedAt:new Date().toISOString(),version:process.env.npm_package_version||"unknown"}),this.waitForHealth(o.pid,r)):{success:!1,error:"Failed to get PID from spawned process"}}}catch(s){return{success:!1,error:s instanceof Error?s.message:String(s)}}}static async stop(t=5e3){let e=this.getPidInfo();if(process.platform==="win32"){let r=e?.port??this.getPortFromSettings();if(await this.tryHttpShutdown(r))return this.removePidFile(),!0;if(!e)return!0;let{execSync:s}=await import("child_process");try{s(`taskkill /PID ${e.pid} /T /F`,{timeout:1e4,stdio:"ignore"})}catch{}try{await this.waitForExit(e.pid,t)}catch{}return this.isProcessAlive(e.pid)||this.removePidFile(),!0}else{if(!e)return!0;try{process.kill(e.pid,"SIGTERM"),await this.waitForExit(e.pid,t)}catch{try{process.kill(e.pid,"SIGKILL")}catch{}}return this.removePidFile(),!0}}static async restart(t){return await this.stop(),this.start(t)}static async status(){let t=this.getPidInfo();if(!t)return{running:!1};let e=this.isProcessAlive(t.pid);return{running:e,pid:e?t.pid:void 0,port:e?t.port:void 0,uptime:e?this.formatUptime(t.startedAt):void 0}}static async isRunning(){let t=this.getPidInfo();if(!t)return!1;let e=this.isProcessAlive(t.pid);return e||this.removePidFile(),e}static getPortFromSettings(){try{let t=S(u,"settings.json"),e=l.loadFromFile(t);return parseInt(e.CLAUDE_MEM_WORKER_PORT,10)}catch{return parseInt(l.get("CLAUDE_MEM_WORKER_PORT"),10)}}static async tryHttpShutdown(t){try{return(await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST",signal:AbortSignal.timeout(2e3)})).ok?await this.waitForWorkerDown(t,5e3):!1}catch{return!1}}static async waitForWorkerDown(t,e){let r=Date.now();for(;Date.now()-r<e;)try{await fetch(`http://127.0.0.1:${t}/api/health`,{signal:AbortSignal.timeout(500)}),await new Promise(n=>setTimeout(n,100))}catch{return!0}return!1}static getPidInfo(){try{if(!P(d))return null;let t=rt(d,"utf-8"),e=JSON.parse(t);return typeof e.pid!="number"||typeof e.port!="number"?(logger.warn("PROCESS","Malformed PID file: missing or invalid pid/port fields",{},{parsed:e}),null):e}catch(t){return logger.warn("PROCESS","Failed to read PID file",{},{error:t instanceof Error?t.message:String(t),path:d}),null}}static writePidFile(t){N(u,{recursive:!0}),nt(d,JSON.stringify(t,null,2))}static removePidFile(){try{P(d)&&st(d)}catch{}}static isProcessAlive(t){try{return process.kill(t,0),!0}catch{return!1}}static async waitForHealth(t,e,r=1e4){let n=Date.now(),s=process.platform==="win32",o=s?r*2:r;for(;Date.now()-n<o;){if(!this.isProcessAlive(t))return{success:!1,error:s?`Process died during startup
|
|
|
|
Troubleshooting:
|
|
1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes
|
|
2. Verify port ${e} is not in use: netstat -ano | findstr ${e}
|
|
3. Check worker logs in ~/.claude-mem/logs/
|
|
4. See GitHub issues: #363, #367, #371, #373
|
|
5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`:"Process died during startup"};try{if((await fetch(`http://127.0.0.1:${e}/api/readiness`,{signal:AbortSignal.timeout(1e3)})).ok)return{success:!0,pid:t}}catch{}await new Promise(c=>setTimeout(c,200))}return{success:!1,error:s?`Worker failed to start on Windows (readiness check timed out after ${o}ms)
|
|
|
|
Troubleshooting:
|
|
1. Check Task Manager for zombie 'bun.exe' or 'node.exe' processes
|
|
2. Verify port ${e} is not in use: netstat -ano | findstr ${e}
|
|
3. Check worker logs in ~/.claude-mem/logs/
|
|
4. See GitHub issues: #363, #367, #371, #373
|
|
5. Docs: https://docs.claude-mem.ai/troubleshooting/windows-issues`:`Readiness check timed out after ${o}ms`}}static async waitForExit(t,e){let r=Date.now();for(;Date.now()-r<e;){if(!this.isProcessAlive(t))return;await new Promise(n=>setTimeout(n,100))}throw new Error("Process did not exit within timeout")}static getLogFilePath(){let t=new Date().toISOString().slice(0,10);return S(x,`worker-${t}.log`)}static formatUptime(t){let e=new Date(t).getTime(),n=Date.now()-e,s=Math.floor(n/1e3),o=Math.floor(s/60),a=Math.floor(o/60),c=Math.floor(a/24);return c>0?`${c}d ${a%24}h`:a>0?`${a}h ${o%60}m`:o>0?`${o}m ${s%60}s`:`${s}s`}};import F from"path";import{homedir as ut}from"os";var I={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function W(i){return process.platform==="win32"?Math.round(i*I.WINDOWS_MULTIPLIER):i}var ge=F.join(ut(),".claude","plugins","marketplaces","thedotmack"),_e=W(I.HEALTH_CHECK),M=null;function H(){if(M!==null)return M;let i=F.join(l.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=l.loadFromFile(i);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}import{stdin as lt}from"process";var pt=process.argv[2],G=H(),O='{"continue": true, "suppressOutput": true}',D=lt.isTTY;async function Et(){switch(pt){case"start":{let i=await g.start(G);if(i.success){if(D){console.log(`Worker started (PID: ${i.pid})`);let t=new Date().toISOString().slice(0,10);console.log(`Logs: ~/.claude-mem/logs/worker-${t}.log`)}else console.log(O);process.exit(0)}else console.error(`Failed to start: ${i.error}`),process.exit(1)}case"stop":await g.stop(),console.log(D?"Worker stopped":O),process.exit(0);case"restart":{let i=await g.restart(G);i.success?(console.log(D?`Worker restarted (PID: ${i.pid})`:O),process.exit(0)):(console.error(`Failed to restart: ${i.error}`),process.exit(1))}case"status":{let i=await g.status();D?i.running?(console.log("Worker is running"),console.log(` PID: ${i.pid}`),console.log(` Port: ${i.port}`),console.log(` Uptime: ${i.uptime}`)):console.log("Worker is not running"):console.log(O),process.exit(0)}default:console.log("Usage: worker-cli.js <start|stop|restart|status>"),process.exit(1)}}Et().catch(i=>{console.error(i),process.exit(1)});
|