From 610be468e47105c1e4fe0448d60479fb76f7293e Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Fri, 26 Dec 2025 22:09:26 -0500 Subject: [PATCH] fix: Add missing OpenRouter and Gemini settings to settingKeys array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings persistence was broken because 7 setting keys were missing from the settingKeys array in SettingsRoutes.ts handleUpdateSettings(): - CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED - CLAUDE_MEM_OPENROUTER_API_KEY - CLAUDE_MEM_OPENROUTER_MODEL - CLAUDE_MEM_OPENROUTER_SITE_URL - CLAUDE_MEM_OPENROUTER_APP_NAME - CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES - CLAUDE_MEM_OPENROUTER_MAX_TOKENS Phase 1/5 of PR #448 fix plan. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- plugin/scripts/worker-service.cjs | 2 +- src/services/worker/http/routes/SettingsRoutes.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 2b551165..7119cae0 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1064,7 +1064,7 @@ Tips: WHERE project IS NOT NULL GROUP BY project ORDER BY MAX(created_at_epoch) DESC - `).all().map(o=>o.project);n.json({projects:i})});handleGetProcessingStatus=this.wrapHandler((r,n)=>{let a=this.sessionManager.isAnySessionProcessing(),s=this.sessionManager.getTotalActiveWork();n.json({isProcessing:a,queueDepth:s})});handleSetProcessing=this.wrapHandler((r,n)=>{this.workerService.broadcastProcessingStatus();let a=this.sessionManager.isAnySessionProcessing(),s=this.sessionManager.getTotalQueueDepth(),i=this.sessionManager.getActiveSessionCount();n.json({status:"ok",isProcessing:a,queueDepth:s,activeSessions:i})});parsePaginationParams(r){let n=parseInt(r.query.offset,10)||0,a=Math.min(parseInt(r.query.limit,10)||20,100),s=r.query.project;return{offset:n,limit:a,project:s}}handleImport=this.wrapHandler((r,n)=>{let{sessions:a,summaries:s,observations:i,prompts:o}=r.body,c={sessionsImported:0,sessionsSkipped:0,summariesImported:0,summariesSkipped:0,observationsImported:0,observationsSkipped:0,promptsImported:0,promptsSkipped:0},u=this.dbManager.getSessionStore();if(Array.isArray(a))for(let l of a)u.importSdkSession(l).imported?c.sessionsImported++:c.sessionsSkipped++;if(Array.isArray(s))for(let l of s)u.importSessionSummary(l).imported?c.summariesImported++:c.summariesSkipped++;if(Array.isArray(i))for(let l of i)u.importObservation(l).imported?c.observationsImported++:c.observationsSkipped++;if(Array.isArray(o))for(let l of o)u.importUserPrompt(l).imported?c.promptsImported++:c.promptsSkipped++;n.json({success:!0,stats:c})});handleGetPendingQueue=this.wrapHandler((r,n)=>{let{PendingMessageStore:a}=(wo(),Ah(Wu)),s=new a(this.dbManager.getSessionStore().db,3),i=s.getQueueMessages(),o=s.getRecentlyProcessed(20,30),c=s.getStuckCount(300*1e3),u=s.getSessionsWithPendingMessages();n.json({queue:{messages:i,totalPending:i.filter(l=>l.status==="pending").length,totalProcessing:i.filter(l=>l.status==="processing").length,totalFailed:i.filter(l=>l.status==="failed").length,stuckCount:c},recentlyProcessed:o,sessionsWithPendingWork:u})});handleProcessPendingQueue=this.wrapHandler(async(r,n)=>{let a=Math.min(Math.max(parseInt(r.body.sessionLimit,10)||10,1),100),s=await this.workerService.processPendingQueues(a);n.json({success:!0,...s})})};var rd=class extends Cr{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,n)=>{let a=await this.searchManager.search(r.query);n.json(a)});handleUnifiedTimeline=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.timeline(r.query);n.json(a)});handleDecisions=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.decisions(r.query);n.json(a)});handleChanges=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.changes(r.query);n.json(a)});handleHowItWorks=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.howItWorks(r.query);n.json(a)});handleSearchObservations=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchObservations(r.query);n.json(a)});handleSearchSessions=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchSessions(r.query);n.json(a)});handleSearchPrompts=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchUserPrompts(r.query);n.json(a)});handleSearchByConcept=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByConcept(r.query);n.json(a)});handleSearchByFile=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByFile(r.query);n.json(a)});handleSearchByType=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByType(r.query);n.json(a)});handleGetRecentContext=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getRecentContext(r.query);n.json(a)});handleGetContextTimeline=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getContextTimeline(r.query);n.json(a)});handleContextPreview=this.wrapHandler(async(r,n)=>{let a=r.query.project;if(!a){this.badRequest(n,"Project parameter is required");return}let{generateContext:s}=await Promise.resolve().then(()=>(td(),ed)),i=`/preview/${a}`,o=await s({session_id:"preview-"+Date.now(),cwd:i},!0);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(o)});handleContextInject=this.wrapHandler(async(r,n)=>{let a=r.query.project,s=r.query.colors==="true";if(!a){this.badRequest(n,"Project parameter is required");return}let{generateContext:i}=await Promise.resolve().then(()=>(td(),ed)),o=`/context/${a}`,c=await i({session_id:"context-inject-"+Date.now(),cwd:o},s);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(c)});handleGetTimelineByQuery=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getTimelineByQuery(r.query);n.json(a)});handleSearchHelp=this.wrapHandler((r,n)=>{n.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)",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)",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)",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",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)",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",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&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 Gn=bt(require("path"),1),Ft=require("fs"),_h=require("os");Rr();lt();var gh=require("child_process"),Vn=require("fs"),kR=require("os"),Jo=require("path");lt();var Yo=(0,Jo.join)((0,kR.homedir)(),".claude","plugins","marketplaces","thedotmack");function vh(t){return!t||typeof t!="string"?!1:/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(t)&&!t.includes("..")}var fV=3e5,yh=6e5;function Ir(t){let e=(0,gh.spawnSync)("git",t,{cwd:Yo,encoding:"utf-8",timeout:fV,windowsHide:!0,shell:!1});if(e.error)throw e.error;if(e.status!==0)throw new Error(e.stderr||e.stdout||"Git command failed");return e.stdout.trim()}function RR(t,e=yh){let n=process.platform==="win32"?"npm.cmd":"npm",a=(0,gh.spawnSync)(n,t,{cwd:Yo,encoding:"utf-8",timeout:e,windowsHide:!0,shell:!1});if(a.error)throw a.error;if(a.status!==0)throw new Error(a.stderr||a.stdout||"npm command failed");return a.stdout.trim()}function ad(){let t=(0,Jo.join)(Yo,".git");if(!(0,Vn.existsSync)(t))return{branch:null,isBeta:!1,isGitRepo:!1,isDirty:!1,canSwitch:!1,error:"Installed plugin is not a git repository"};try{let e=Ir(["rev-parse","--abbrev-ref","HEAD"]),n=Ir(["status","--porcelain"]).length>0,a=e.startsWith("beta");return{branch:e,isBeta:a,isGitRepo:!0,isDirty:n,canSwitch:!0}}catch(e){return A.error("BRANCH","Failed to get branch info",{},e),{branch:null,isBeta:!1,isGitRepo:!0,isDirty:!1,canSwitch:!1,error:e.message}}}async function PR(t){if(!vh(t))return{success:!1,error:`Invalid branch name: ${t}. Branch names must be alphanumeric with hyphens, underscores, slashes, or dots.`};let e=ad();if(!e.isGitRepo)return{success:!1,error:"Installed plugin is not a git repository. Please reinstall."};if(e.branch===t)return{success:!0,branch:t,message:`Already on branch ${t}`};try{A.info("BRANCH","Starting branch switch",{from:e.branch,to:t}),A.debug("BRANCH","Discarding local changes"),Ir(["checkout","--","."]),Ir(["clean","-fd"]),A.debug("BRANCH","Fetching from origin"),Ir(["fetch","origin"]),A.debug("BRANCH","Checking out branch",{branch:t});try{Ir(["checkout",t])}catch{Ir(["checkout","-b",t,`origin/${t}`])}A.debug("BRANCH","Pulling latest"),Ir(["pull","origin",t]);let r=(0,Jo.join)(Yo,".install-version");return(0,Vn.existsSync)(r)&&(0,Vn.unlinkSync)(r),A.debug("BRANCH","Running npm install"),RR(["install"],yh),A.success("BRANCH","Branch switch complete",{branch:t}),{success:!0,branch:t,message:`Switched to ${t}. Worker will restart automatically.`}}catch(r){A.error("BRANCH","Branch switch failed",{targetBranch:t},r);try{e.branch&&vh(e.branch)&&Ir(["checkout",e.branch])}catch{}return{success:!1,error:`Branch switch failed: ${r.message}`}}}async function $R(){let t=ad();if(!t.isGitRepo||!t.branch)return{success:!1,error:"Cannot pull updates: not a git repository"};try{if(!vh(t.branch))return{success:!1,error:`Invalid current branch name: ${t.branch}`};A.info("BRANCH","Pulling updates",{branch:t.branch}),Ir(["checkout","--","."]),Ir(["fetch","origin"]),Ir(["pull","origin",t.branch]);let e=(0,Jo.join)(Yo,".install-version");return(0,Vn.existsSync)(e)&&(0,Vn.unlinkSync)(e),RR(["install"],yh),A.success("BRANCH","Updates pulled",{branch:t.branch}),{success:!0,branch:t.branch,message:`Updated ${t.branch}. Worker will restart automatically.`}}catch(e){return A.error("BRANCH","Pull failed",{},e),{success:!1,error:`Pull failed: ${e.message}`}}}kr();var nd=class extends Cr{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,n)=>{let a=Gn.default.join((0,_h.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(a);let s=We.loadFromFile(a);n.json(s)});handleUpdateSettings=this.wrapHandler((r,n)=>{let a=this.validateSettings(r.body);if(!a.valid){n.status(400).json({success:!1,error:a.error});return}let s=Gn.default.join((0,_h.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(s);let i={};if((0,Ft.existsSync)(s)){let c=(0,Ft.readFileSync)(s,"utf-8");i=JSON.parse(c)}let o=["CLAUDE_MEM_MODEL","CLAUDE_MEM_CONTEXT_OBSERVATIONS","CLAUDE_MEM_WORKER_PORT","CLAUDE_MEM_WORKER_HOST","CLAUDE_MEM_PROVIDER","CLAUDE_MEM_GEMINI_API_KEY","CLAUDE_MEM_GEMINI_MODEL","CLAUDE_MEM_DATA_DIR","CLAUDE_MEM_LOG_LEVEL","CLAUDE_MEM_PYTHON_VERSION","CLAUDE_CODE_PATH","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 c of o)r.body[c]!==void 0&&(i[c]=r.body[c]);(0,Ft.writeFileSync)(s,JSON.stringify(i,null,2),"utf-8"),p1(),A.info("WORKER","Settings updated"),n.json({success:!0,message:"Settings updated successfully"})});handleGetMcpStatus=this.wrapHandler((r,n)=>{let a=this.isMcpEnabled();n.json({enabled:a})});handleToggleMcp=this.wrapHandler((r,n)=>{let{enabled:a}=r.body;if(typeof a!="boolean"){this.badRequest(n,"enabled must be a boolean");return}this.toggleMcp(a),n.json({success:!0,enabled:this.isMcpEnabled()})});handleGetBranchStatus=this.wrapHandler((r,n)=>{let a=ad();n.json(a)});handleSwitchBranch=this.wrapHandler(async(r,n)=>{let{branch:a}=r.body;if(!a){n.status(400).json({success:!1,error:"Missing branch parameter"});return}let s=["main","beta/7.0","feature/bun-executable"];if(!s.includes(a)){n.status(400).json({success:!1,error:`Invalid branch. Allowed: ${s.join(", ")}`});return}A.info("WORKER","Branch switch requested",{branch:a});let i=await PR(a);i.success&&setTimeout(()=>{A.info("WORKER","Restarting worker after branch switch"),process.exit(0)},1e3),n.json(i)});handleUpdateBranch=this.wrapHandler(async(r,n)=>{A.info("WORKER","Branch update requested");let a=await $R();a.success&&setTimeout(()=>{A.info("WORKER","Restarting worker after branch update"),process.exit(0)},1e3),n.json(a)});validateSettings(r){if(r.CLAUDE_MEM_PROVIDER&&!["claude","gemini","openrouter"].includes(r.CLAUDE_MEM_PROVIDER))return{valid:!1,error:'CLAUDE_MEM_PROVIDER must be "claude", "gemini", or "openrouter"'};if(r.CLAUDE_MEM_GEMINI_MODEL&&!["gemini-2.5-flash-lite","gemini-2.5-flash","gemini-3-flash"].includes(r.CLAUDE_MEM_GEMINI_MODEL))return{valid:!1,error:"CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.5-flash-lite, gemini-2.5-flash, gemini-3-flash"};if(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let a=parseInt(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(isNaN(a)||a<1||a>200)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200"}}if(r.CLAUDE_MEM_WORKER_PORT){let a=parseInt(r.CLAUDE_MEM_WORKER_PORT,10);if(isNaN(a)||a<1024||a>65535)return{valid:!1,error:"CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535"}}if(r.CLAUDE_MEM_WORKER_HOST){let a=r.CLAUDE_MEM_WORKER_HOST;if(!/^(127\.0\.0\.1|0\.0\.0\.0|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(a))return{valid:!1,error:"CLAUDE_MEM_WORKER_HOST must be a valid IP address (e.g., 127.0.0.1, 0.0.0.0)"}}if(r.CLAUDE_MEM_LOG_LEVEL&&!["DEBUG","INFO","WARN","ERROR","SILENT"].includes(r.CLAUDE_MEM_LOG_LEVEL.toUpperCase()))return{valid:!1,error:"CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT"};if(r.CLAUDE_MEM_PYTHON_VERSION&&!/^3\.\d{1,2}$/.test(r.CLAUDE_MEM_PYTHON_VERSION))return{valid:!1,error:'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")'};let n=["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 a of n)if(r[a]&&!["true","false"].includes(r[a]))return{valid:!1,error:`${a} must be "true" or "false"`};if(r.CLAUDE_MEM_CONTEXT_FULL_COUNT){let a=parseInt(r.CLAUDE_MEM_CONTEXT_FULL_COUNT,10);if(isNaN(a)||a<0||a>20)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20"}}if(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT){let a=parseInt(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT,10);if(isNaN(a)||a<1||a>50)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50"}}return r.CLAUDE_MEM_CONTEXT_FULL_FIELD&&!["narrative","facts"].includes(r.CLAUDE_MEM_CONTEXT_FULL_FIELD)?{valid:!1,error:'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"'}:{valid:!0}}isMcpEnabled(){let r=nr(),n=Gn.default.join(r,"plugin",".mcp.json");return(0,Ft.existsSync)(n)}toggleMcp(r){let n=nr(),a=Gn.default.join(n,"plugin",".mcp.json"),s=Gn.default.join(n,"plugin",".mcp.json.disabled");r&&(0,Ft.existsSync)(s)?((0,Ft.renameSync)(s,a),A.info("WORKER","MCP search server enabled")):!r&&(0,Ft.existsSync)(a)?((0,Ft.renameSync)(a,s),A.info("WORKER","MCP search server disabled")):A.debug("WORKER","MCP toggle no-op (already in desired state)",{enabled:r})}ensureSettingsFile(r){if(!(0,Ft.existsSync)(r)){let n=We.getAllDefaults(),a=Gn.default.dirname(r);(0,Ft.existsSync)(a)||(0,Ft.mkdirSync)(a,{recursive:!0}),(0,Ft.writeFileSync)(r,JSON.stringify(n,null,2),"utf-8"),A.info("SETTINGS","Created settings file with defaults",{settingsPath:r})}}};var sd=(0,jR.promisify)(Kn.exec),DR=Ka.default.join((0,MR.homedir)(),".claude-mem"),Wn=Ka.default.join(DR,"worker.pid"),id='{"continue": true, "suppressOutput": true}';function OR(t){(0,Hr.mkdirSync)(DR,{recursive:!0}),(0,Hr.writeFileSync)(Wn,JSON.stringify(t,null,2))}function mV(){try{return(0,Hr.existsSync)(Wn)?JSON.parse((0,Hr.readFileSync)(Wn,"utf-8")):null}catch(t){return A.warn("SYSTEM","Failed to read PID file",{path:Wn,error:t.message}),null}}function Zn(){try{(0,Hr.existsSync)(Wn)&&(0,Hr.unlinkSync)(Wn)}catch(t){A.warn("SYSTEM","Failed to remove PID file",{path:Wn,error:t.message})}}async function xh(t){try{return(await fetch(`http://127.0.0.1:${t}/api/health`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}async function CR(t,e=3e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}async function IR(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST",signal:AbortSignal.timeout(5e3)});return e.ok?!0:(A.warn("SYSTEM","Shutdown request returned error",{port:t,status:e.status}),!1)}catch(e){return e.message?.includes("ECONNREFUSED")||A.warn("SYSTEM","Shutdown request failed",{port:t,error:e.message}),!1}}async function AR(t,e=1e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}var od=class{app;server=null;startTime=Date.now();mcpClient;mcpReady=!1;initializationCompleteFlag=!1;dbManager;sessionManager;sseBroadcaster;sdkAgent;geminiAgent;openRouterAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;initializationComplete;resolveInitialization;constructor(){this.app=(0,NR.default)(),this.initializationComplete=new Promise(e=>{this.resolveInitialization=e}),this.dbManager=new Zu,this.sessionManager=new Ku(this.dbManager),this.sseBroadcaster=new Xu,this.sdkAgent=new Al(this.dbManager,this.sessionManager),this.geminiAgent=new Nl(this.dbManager,this.sessionManager),this.geminiAgent.setFallbackAgent(this.sdkAgent),this.openRouterAgent=new Dl(this.dbManager,this.sessionManager),this.openRouterAgent.setFallbackAgent(this.sdkAgent),this.paginationHelper=new ql(this.dbManager),this.settingsManager=new Ll(this.dbManager),this.sessionEventBroadcaster=new Bl(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Ls({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Gl(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Kl(this.sessionManager,this.dbManager,this.sdkAgent,this.geminiAgent,this.openRouterAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new Xl(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new nd(this.settingsManager),this.setupMiddleware(),this.setupRoutes()}setupMiddleware(){hR(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){let e="TEST-008-wrapper-ipc";this.app.get("/api/health",(r,n)=>{n.status(200).json({status:"ok",build:e,managed:process.env.CLAUDE_MEM_MANAGED==="true",hasIpc:typeof process.send=="function",platform:process.platform,pid:process.pid,initialized:this.initializationCompleteFlag,mcpReady:this.mcpReady})}),this.app.get("/api/readiness",(r,n)=>{this.initializationCompleteFlag?n.status(200).json({status:"ready",mcpReady:this.mcpReady}):n.status(503).json({status:"initializing",message:"Worker is still initializing, please retry"})}),this.app.get("/api/version",(r,n)=>{let{homedir:a}=require("os"),{readFileSync:s}=require("fs"),i=Ka.default.join(a(),".claude","plugins","marketplaces","thedotmack"),o=Ka.default.join(i,"package.json"),c=JSON.parse(s(o,"utf-8"));n.status(200).json({version:c.version})}),this.app.get("/api/instructions",async(r,n)=>{let a=r.query.topic||"all",s=r.query.operation;try{let i;if(s){let o=Ka.default.join(__dirname,"../skills/mem-search/operations",`${s}.md`);i=await bh.promises.readFile(o,"utf-8")}else{let o=Ka.default.join(__dirname,"../skills/mem-search/SKILL.md"),c=await bh.promises.readFile(o,"utf-8");i=this.extractInstructionSection(c,a)}n.json({content:[{type:"text",text:i}]})}catch(i){A.error("WORKER","Failed to load instructions",{topic:a,operation:s},i),n.status(500).json({content:[{type:"text",text:`Error loading instructions: ${i instanceof Error?i.message:"Unknown error"}`}],isError:!0})}}),this.app.post("/api/admin/restart",fh,async(r,n)=>{n.json({status:"restarting"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(A.info("SYSTEM","Sending restart request to wrapper"),process.send({type:"restart"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.app.post("/api/admin/shutdown",fh,async(r,n)=>{n.json({status:"shutting_down"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(A.info("SYSTEM","Sending shutdown request to wrapper"),process.send({type:"shutdown"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app),this.app.get("/api/context/inject",async(r,n,a)=>{try{let i=new Promise((f,m)=>setTimeout(()=>m(new Error("Initialization timeout")),3e5));if(await Promise.race([this.initializationComplete,i]),!this.searchRoutes){n.status(503).json({error:"Search routes not initialized"});return}let o=r.query.project,c=r.query.colors==="true";if(!o){n.status(400).json({error:"Project parameter is required"});return}let{generateContext:u}=await Promise.resolve().then(()=>(td(),ed)),l=`/context/${o}`,d=await u({session_id:"context-inject-"+Date.now(),cwd:l},c);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(d)}catch(s){A.error("WORKER","Context inject handler failed",{},s),n.status(500).json({error:s instanceof Error?s.message:"Internal server error"})}})}async cleanupOrphanedProcesses(){let e=process.platform==="win32",r=[];if(e){let n=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`,{stdout:a}=await sd(n,{timeout:6e4});if(!a.trim()){A.debug("SYSTEM","No orphaned chroma-mcp processes found (Windows)");return}let s=a.trim().split(` + `).all().map(o=>o.project);n.json({projects:i})});handleGetProcessingStatus=this.wrapHandler((r,n)=>{let a=this.sessionManager.isAnySessionProcessing(),s=this.sessionManager.getTotalActiveWork();n.json({isProcessing:a,queueDepth:s})});handleSetProcessing=this.wrapHandler((r,n)=>{this.workerService.broadcastProcessingStatus();let a=this.sessionManager.isAnySessionProcessing(),s=this.sessionManager.getTotalQueueDepth(),i=this.sessionManager.getActiveSessionCount();n.json({status:"ok",isProcessing:a,queueDepth:s,activeSessions:i})});parsePaginationParams(r){let n=parseInt(r.query.offset,10)||0,a=Math.min(parseInt(r.query.limit,10)||20,100),s=r.query.project;return{offset:n,limit:a,project:s}}handleImport=this.wrapHandler((r,n)=>{let{sessions:a,summaries:s,observations:i,prompts:o}=r.body,c={sessionsImported:0,sessionsSkipped:0,summariesImported:0,summariesSkipped:0,observationsImported:0,observationsSkipped:0,promptsImported:0,promptsSkipped:0},u=this.dbManager.getSessionStore();if(Array.isArray(a))for(let l of a)u.importSdkSession(l).imported?c.sessionsImported++:c.sessionsSkipped++;if(Array.isArray(s))for(let l of s)u.importSessionSummary(l).imported?c.summariesImported++:c.summariesSkipped++;if(Array.isArray(i))for(let l of i)u.importObservation(l).imported?c.observationsImported++:c.observationsSkipped++;if(Array.isArray(o))for(let l of o)u.importUserPrompt(l).imported?c.promptsImported++:c.promptsSkipped++;n.json({success:!0,stats:c})});handleGetPendingQueue=this.wrapHandler((r,n)=>{let{PendingMessageStore:a}=(wo(),Ah(Wu)),s=new a(this.dbManager.getSessionStore().db,3),i=s.getQueueMessages(),o=s.getRecentlyProcessed(20,30),c=s.getStuckCount(300*1e3),u=s.getSessionsWithPendingMessages();n.json({queue:{messages:i,totalPending:i.filter(l=>l.status==="pending").length,totalProcessing:i.filter(l=>l.status==="processing").length,totalFailed:i.filter(l=>l.status==="failed").length,stuckCount:c},recentlyProcessed:o,sessionsWithPendingWork:u})});handleProcessPendingQueue=this.wrapHandler(async(r,n)=>{let a=Math.min(Math.max(parseInt(r.body.sessionLimit,10)||10,1),100),s=await this.workerService.processPendingQueues(a);n.json({success:!0,...s})})};var rd=class extends Cr{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,n)=>{let a=await this.searchManager.search(r.query);n.json(a)});handleUnifiedTimeline=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.timeline(r.query);n.json(a)});handleDecisions=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.decisions(r.query);n.json(a)});handleChanges=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.changes(r.query);n.json(a)});handleHowItWorks=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.howItWorks(r.query);n.json(a)});handleSearchObservations=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchObservations(r.query);n.json(a)});handleSearchSessions=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchSessions(r.query);n.json(a)});handleSearchPrompts=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.searchUserPrompts(r.query);n.json(a)});handleSearchByConcept=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByConcept(r.query);n.json(a)});handleSearchByFile=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByFile(r.query);n.json(a)});handleSearchByType=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.findByType(r.query);n.json(a)});handleGetRecentContext=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getRecentContext(r.query);n.json(a)});handleGetContextTimeline=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getContextTimeline(r.query);n.json(a)});handleContextPreview=this.wrapHandler(async(r,n)=>{let a=r.query.project;if(!a){this.badRequest(n,"Project parameter is required");return}let{generateContext:s}=await Promise.resolve().then(()=>(td(),ed)),i=`/preview/${a}`,o=await s({session_id:"preview-"+Date.now(),cwd:i},!0);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(o)});handleContextInject=this.wrapHandler(async(r,n)=>{let a=r.query.project,s=r.query.colors==="true";if(!a){this.badRequest(n,"Project parameter is required");return}let{generateContext:i}=await Promise.resolve().then(()=>(td(),ed)),o=`/context/${a}`,c=await i({session_id:"context-inject-"+Date.now(),cwd:o},s);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(c)});handleGetTimelineByQuery=this.wrapHandler(async(r,n)=>{let a=await this.searchManager.getTimelineByQuery(r.query);n.json(a)});handleSearchHelp=this.wrapHandler((r,n)=>{n.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)",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)",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)",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",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)",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",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&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 Gn=bt(require("path"),1),Ft=require("fs"),_h=require("os");Rr();lt();var gh=require("child_process"),Vn=require("fs"),kR=require("os"),Jo=require("path");lt();var Yo=(0,Jo.join)((0,kR.homedir)(),".claude","plugins","marketplaces","thedotmack");function vh(t){return!t||typeof t!="string"?!1:/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(t)&&!t.includes("..")}var fV=3e5,yh=6e5;function Ir(t){let e=(0,gh.spawnSync)("git",t,{cwd:Yo,encoding:"utf-8",timeout:fV,windowsHide:!0,shell:!1});if(e.error)throw e.error;if(e.status!==0)throw new Error(e.stderr||e.stdout||"Git command failed");return e.stdout.trim()}function RR(t,e=yh){let n=process.platform==="win32"?"npm.cmd":"npm",a=(0,gh.spawnSync)(n,t,{cwd:Yo,encoding:"utf-8",timeout:e,windowsHide:!0,shell:!1});if(a.error)throw a.error;if(a.status!==0)throw new Error(a.stderr||a.stdout||"npm command failed");return a.stdout.trim()}function ad(){let t=(0,Jo.join)(Yo,".git");if(!(0,Vn.existsSync)(t))return{branch:null,isBeta:!1,isGitRepo:!1,isDirty:!1,canSwitch:!1,error:"Installed plugin is not a git repository"};try{let e=Ir(["rev-parse","--abbrev-ref","HEAD"]),n=Ir(["status","--porcelain"]).length>0,a=e.startsWith("beta");return{branch:e,isBeta:a,isGitRepo:!0,isDirty:n,canSwitch:!0}}catch(e){return A.error("BRANCH","Failed to get branch info",{},e),{branch:null,isBeta:!1,isGitRepo:!0,isDirty:!1,canSwitch:!1,error:e.message}}}async function PR(t){if(!vh(t))return{success:!1,error:`Invalid branch name: ${t}. Branch names must be alphanumeric with hyphens, underscores, slashes, or dots.`};let e=ad();if(!e.isGitRepo)return{success:!1,error:"Installed plugin is not a git repository. Please reinstall."};if(e.branch===t)return{success:!0,branch:t,message:`Already on branch ${t}`};try{A.info("BRANCH","Starting branch switch",{from:e.branch,to:t}),A.debug("BRANCH","Discarding local changes"),Ir(["checkout","--","."]),Ir(["clean","-fd"]),A.debug("BRANCH","Fetching from origin"),Ir(["fetch","origin"]),A.debug("BRANCH","Checking out branch",{branch:t});try{Ir(["checkout",t])}catch{Ir(["checkout","-b",t,`origin/${t}`])}A.debug("BRANCH","Pulling latest"),Ir(["pull","origin",t]);let r=(0,Jo.join)(Yo,".install-version");return(0,Vn.existsSync)(r)&&(0,Vn.unlinkSync)(r),A.debug("BRANCH","Running npm install"),RR(["install"],yh),A.success("BRANCH","Branch switch complete",{branch:t}),{success:!0,branch:t,message:`Switched to ${t}. Worker will restart automatically.`}}catch(r){A.error("BRANCH","Branch switch failed",{targetBranch:t},r);try{e.branch&&vh(e.branch)&&Ir(["checkout",e.branch])}catch{}return{success:!1,error:`Branch switch failed: ${r.message}`}}}async function $R(){let t=ad();if(!t.isGitRepo||!t.branch)return{success:!1,error:"Cannot pull updates: not a git repository"};try{if(!vh(t.branch))return{success:!1,error:`Invalid current branch name: ${t.branch}`};A.info("BRANCH","Pulling updates",{branch:t.branch}),Ir(["checkout","--","."]),Ir(["fetch","origin"]),Ir(["pull","origin",t.branch]);let e=(0,Jo.join)(Yo,".install-version");return(0,Vn.existsSync)(e)&&(0,Vn.unlinkSync)(e),RR(["install"],yh),A.success("BRANCH","Updates pulled",{branch:t.branch}),{success:!0,branch:t.branch,message:`Updated ${t.branch}. Worker will restart automatically.`}}catch(e){return A.error("BRANCH","Pull failed",{},e),{success:!1,error:`Pull failed: ${e.message}`}}}kr();var nd=class extends Cr{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,n)=>{let a=Gn.default.join((0,_h.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(a);let s=We.loadFromFile(a);n.json(s)});handleUpdateSettings=this.wrapHandler((r,n)=>{let a=this.validateSettings(r.body);if(!a.valid){n.status(400).json({success:!1,error:a.error});return}let s=Gn.default.join((0,_h.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(s);let i={};if((0,Ft.existsSync)(s)){let c=(0,Ft.readFileSync)(s,"utf-8");i=JSON.parse(c)}let o=["CLAUDE_MEM_MODEL","CLAUDE_MEM_CONTEXT_OBSERVATIONS","CLAUDE_MEM_WORKER_PORT","CLAUDE_MEM_WORKER_HOST","CLAUDE_MEM_PROVIDER","CLAUDE_MEM_GEMINI_API_KEY","CLAUDE_MEM_GEMINI_MODEL","CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED","CLAUDE_MEM_OPENROUTER_API_KEY","CLAUDE_MEM_OPENROUTER_MODEL","CLAUDE_MEM_OPENROUTER_SITE_URL","CLAUDE_MEM_OPENROUTER_APP_NAME","CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES","CLAUDE_MEM_OPENROUTER_MAX_TOKENS","CLAUDE_MEM_DATA_DIR","CLAUDE_MEM_LOG_LEVEL","CLAUDE_MEM_PYTHON_VERSION","CLAUDE_CODE_PATH","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 c of o)r.body[c]!==void 0&&(i[c]=r.body[c]);(0,Ft.writeFileSync)(s,JSON.stringify(i,null,2),"utf-8"),p1(),A.info("WORKER","Settings updated"),n.json({success:!0,message:"Settings updated successfully"})});handleGetMcpStatus=this.wrapHandler((r,n)=>{let a=this.isMcpEnabled();n.json({enabled:a})});handleToggleMcp=this.wrapHandler((r,n)=>{let{enabled:a}=r.body;if(typeof a!="boolean"){this.badRequest(n,"enabled must be a boolean");return}this.toggleMcp(a),n.json({success:!0,enabled:this.isMcpEnabled()})});handleGetBranchStatus=this.wrapHandler((r,n)=>{let a=ad();n.json(a)});handleSwitchBranch=this.wrapHandler(async(r,n)=>{let{branch:a}=r.body;if(!a){n.status(400).json({success:!1,error:"Missing branch parameter"});return}let s=["main","beta/7.0","feature/bun-executable"];if(!s.includes(a)){n.status(400).json({success:!1,error:`Invalid branch. Allowed: ${s.join(", ")}`});return}A.info("WORKER","Branch switch requested",{branch:a});let i=await PR(a);i.success&&setTimeout(()=>{A.info("WORKER","Restarting worker after branch switch"),process.exit(0)},1e3),n.json(i)});handleUpdateBranch=this.wrapHandler(async(r,n)=>{A.info("WORKER","Branch update requested");let a=await $R();a.success&&setTimeout(()=>{A.info("WORKER","Restarting worker after branch update"),process.exit(0)},1e3),n.json(a)});validateSettings(r){if(r.CLAUDE_MEM_PROVIDER&&!["claude","gemini","openrouter"].includes(r.CLAUDE_MEM_PROVIDER))return{valid:!1,error:'CLAUDE_MEM_PROVIDER must be "claude", "gemini", or "openrouter"'};if(r.CLAUDE_MEM_GEMINI_MODEL&&!["gemini-2.5-flash-lite","gemini-2.5-flash","gemini-3-flash"].includes(r.CLAUDE_MEM_GEMINI_MODEL))return{valid:!1,error:"CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.5-flash-lite, gemini-2.5-flash, gemini-3-flash"};if(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let a=parseInt(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(isNaN(a)||a<1||a>200)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200"}}if(r.CLAUDE_MEM_WORKER_PORT){let a=parseInt(r.CLAUDE_MEM_WORKER_PORT,10);if(isNaN(a)||a<1024||a>65535)return{valid:!1,error:"CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535"}}if(r.CLAUDE_MEM_WORKER_HOST){let a=r.CLAUDE_MEM_WORKER_HOST;if(!/^(127\.0\.0\.1|0\.0\.0\.0|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(a))return{valid:!1,error:"CLAUDE_MEM_WORKER_HOST must be a valid IP address (e.g., 127.0.0.1, 0.0.0.0)"}}if(r.CLAUDE_MEM_LOG_LEVEL&&!["DEBUG","INFO","WARN","ERROR","SILENT"].includes(r.CLAUDE_MEM_LOG_LEVEL.toUpperCase()))return{valid:!1,error:"CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT"};if(r.CLAUDE_MEM_PYTHON_VERSION&&!/^3\.\d{1,2}$/.test(r.CLAUDE_MEM_PYTHON_VERSION))return{valid:!1,error:'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")'};let n=["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 a of n)if(r[a]&&!["true","false"].includes(r[a]))return{valid:!1,error:`${a} must be "true" or "false"`};if(r.CLAUDE_MEM_CONTEXT_FULL_COUNT){let a=parseInt(r.CLAUDE_MEM_CONTEXT_FULL_COUNT,10);if(isNaN(a)||a<0||a>20)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20"}}if(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT){let a=parseInt(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT,10);if(isNaN(a)||a<1||a>50)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50"}}return r.CLAUDE_MEM_CONTEXT_FULL_FIELD&&!["narrative","facts"].includes(r.CLAUDE_MEM_CONTEXT_FULL_FIELD)?{valid:!1,error:'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"'}:{valid:!0}}isMcpEnabled(){let r=nr(),n=Gn.default.join(r,"plugin",".mcp.json");return(0,Ft.existsSync)(n)}toggleMcp(r){let n=nr(),a=Gn.default.join(n,"plugin",".mcp.json"),s=Gn.default.join(n,"plugin",".mcp.json.disabled");r&&(0,Ft.existsSync)(s)?((0,Ft.renameSync)(s,a),A.info("WORKER","MCP search server enabled")):!r&&(0,Ft.existsSync)(a)?((0,Ft.renameSync)(a,s),A.info("WORKER","MCP search server disabled")):A.debug("WORKER","MCP toggle no-op (already in desired state)",{enabled:r})}ensureSettingsFile(r){if(!(0,Ft.existsSync)(r)){let n=We.getAllDefaults(),a=Gn.default.dirname(r);(0,Ft.existsSync)(a)||(0,Ft.mkdirSync)(a,{recursive:!0}),(0,Ft.writeFileSync)(r,JSON.stringify(n,null,2),"utf-8"),A.info("SETTINGS","Created settings file with defaults",{settingsPath:r})}}};var sd=(0,jR.promisify)(Kn.exec),DR=Ka.default.join((0,MR.homedir)(),".claude-mem"),Wn=Ka.default.join(DR,"worker.pid"),id='{"continue": true, "suppressOutput": true}';function OR(t){(0,Hr.mkdirSync)(DR,{recursive:!0}),(0,Hr.writeFileSync)(Wn,JSON.stringify(t,null,2))}function mV(){try{return(0,Hr.existsSync)(Wn)?JSON.parse((0,Hr.readFileSync)(Wn,"utf-8")):null}catch(t){return A.warn("SYSTEM","Failed to read PID file",{path:Wn,error:t.message}),null}}function Zn(){try{(0,Hr.existsSync)(Wn)&&(0,Hr.unlinkSync)(Wn)}catch(t){A.warn("SYSTEM","Failed to remove PID file",{path:Wn,error:t.message})}}async function xh(t){try{return(await fetch(`http://127.0.0.1:${t}/api/health`,{signal:AbortSignal.timeout(2e3)})).ok}catch{return!1}}async function CR(t,e=3e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}async function IR(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST",signal:AbortSignal.timeout(5e3)});return e.ok?!0:(A.warn("SYSTEM","Shutdown request returned error",{port:t,status:e.status}),!1)}catch(e){return e.message?.includes("ECONNREFUSED")||A.warn("SYSTEM","Shutdown request failed",{port:t,error:e.message}),!1}}async function AR(t,e=1e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}var od=class{app;server=null;startTime=Date.now();mcpClient;mcpReady=!1;initializationCompleteFlag=!1;dbManager;sessionManager;sseBroadcaster;sdkAgent;geminiAgent;openRouterAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;initializationComplete;resolveInitialization;constructor(){this.app=(0,NR.default)(),this.initializationComplete=new Promise(e=>{this.resolveInitialization=e}),this.dbManager=new Zu,this.sessionManager=new Ku(this.dbManager),this.sseBroadcaster=new Xu,this.sdkAgent=new Al(this.dbManager,this.sessionManager),this.geminiAgent=new Nl(this.dbManager,this.sessionManager),this.geminiAgent.setFallbackAgent(this.sdkAgent),this.openRouterAgent=new Dl(this.dbManager,this.sessionManager),this.openRouterAgent.setFallbackAgent(this.sdkAgent),this.paginationHelper=new ql(this.dbManager),this.settingsManager=new Ll(this.dbManager),this.sessionEventBroadcaster=new Bl(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Ls({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Gl(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Kl(this.sessionManager,this.dbManager,this.sdkAgent,this.geminiAgent,this.openRouterAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new Xl(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new nd(this.settingsManager),this.setupMiddleware(),this.setupRoutes()}setupMiddleware(){hR(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){let e="TEST-008-wrapper-ipc";this.app.get("/api/health",(r,n)=>{n.status(200).json({status:"ok",build:e,managed:process.env.CLAUDE_MEM_MANAGED==="true",hasIpc:typeof process.send=="function",platform:process.platform,pid:process.pid,initialized:this.initializationCompleteFlag,mcpReady:this.mcpReady})}),this.app.get("/api/readiness",(r,n)=>{this.initializationCompleteFlag?n.status(200).json({status:"ready",mcpReady:this.mcpReady}):n.status(503).json({status:"initializing",message:"Worker is still initializing, please retry"})}),this.app.get("/api/version",(r,n)=>{let{homedir:a}=require("os"),{readFileSync:s}=require("fs"),i=Ka.default.join(a(),".claude","plugins","marketplaces","thedotmack"),o=Ka.default.join(i,"package.json"),c=JSON.parse(s(o,"utf-8"));n.status(200).json({version:c.version})}),this.app.get("/api/instructions",async(r,n)=>{let a=r.query.topic||"all",s=r.query.operation;try{let i;if(s){let o=Ka.default.join(__dirname,"../skills/mem-search/operations",`${s}.md`);i=await bh.promises.readFile(o,"utf-8")}else{let o=Ka.default.join(__dirname,"../skills/mem-search/SKILL.md"),c=await bh.promises.readFile(o,"utf-8");i=this.extractInstructionSection(c,a)}n.json({content:[{type:"text",text:i}]})}catch(i){A.error("WORKER","Failed to load instructions",{topic:a,operation:s},i),n.status(500).json({content:[{type:"text",text:`Error loading instructions: ${i instanceof Error?i.message:"Unknown error"}`}],isError:!0})}}),this.app.post("/api/admin/restart",fh,async(r,n)=>{n.json({status:"restarting"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(A.info("SYSTEM","Sending restart request to wrapper"),process.send({type:"restart"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.app.post("/api/admin/shutdown",fh,async(r,n)=>{n.json({status:"shutting_down"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(A.info("SYSTEM","Sending shutdown request to wrapper"),process.send({type:"shutdown"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app),this.app.get("/api/context/inject",async(r,n,a)=>{try{let i=new Promise((f,m)=>setTimeout(()=>m(new Error("Initialization timeout")),3e5));if(await Promise.race([this.initializationComplete,i]),!this.searchRoutes){n.status(503).json({error:"Search routes not initialized"});return}let o=r.query.project,c=r.query.colors==="true";if(!o){n.status(400).json({error:"Project parameter is required"});return}let{generateContext:u}=await Promise.resolve().then(()=>(td(),ed)),l=`/context/${o}`,d=await u({session_id:"context-inject-"+Date.now(),cwd:l},c);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(d)}catch(s){A.error("WORKER","Context inject handler failed",{},s),n.status(500).json({error:s instanceof Error?s.message:"Internal server error"})}})}async cleanupOrphanedProcesses(){let e=process.platform==="win32",r=[];if(e){let n=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`,{stdout:a}=await sd(n,{timeout:6e4});if(!a.trim()){A.debug("SYSTEM","No orphaned chroma-mcp processes found (Windows)");return}let s=a.trim().split(` `);for(let i of s){let o=parseInt(i.trim(),10);!isNaN(o)&&Number.isInteger(o)&&o>0&&r.push(o)}}else{let{stdout:n}=await sd('ps aux | grep "chroma-mcp" | grep -v grep || true');if(!n.trim()){A.debug("SYSTEM","No orphaned chroma-mcp processes found (Unix)");return}let a=n.trim().split(` `);for(let s of a){let i=s.trim().split(/\s+/);if(i.length>1){let o=parseInt(i[1],10);!isNaN(o)&&Number.isInteger(o)&&o>0&&r.push(o)}}}if(r.length!==0){if(A.info("SYSTEM","Cleaning up orphaned chroma-mcp processes",{platform:e?"Windows":"Unix",count:r.length,pids:r}),e)for(let n of r){if(!Number.isInteger(n)||n<=0){A.warn("SYSTEM","Skipping invalid PID",{pid:n});continue}try{(0,Kn.execSync)(`taskkill /PID ${n} /T /F`,{timeout:6e4,stdio:"ignore"})}catch{}}else for(let n of r)try{process.kill(n,"SIGKILL")}catch{}A.info("SYSTEM","Orphaned processes cleaned up",{count:r.length})}}async start(){let e=Mn(),r=d1();this.server=await new Promise((n,a)=>{let s=this.app.listen(e,r,()=>n(s));s.on("error",a)}),A.info("SYSTEM","Worker started",{host:r,port:e,pid:process.pid}),this.initializeBackground().catch(n=>{A.error("SYSTEM","Background initialization failed",{},n)})}async initializeBackground(){try{await this.cleanupOrphanedProcesses();let{ModeManager:e}=await Promise.resolve().then(()=>(na(),R1)),{SettingsDefaultsManager:r}=await Promise.resolve().then(()=>(kr(),c1)),{USER_SETTINGS_PATH:n}=await Promise.resolve().then(()=>(Rr(),x1)),s=r.loadFromFile(n).CLAUDE_MEM_MODE;e.getInstance().loadMode(s),A.info("SYSTEM",`Mode loaded: ${s}`),await this.dbManager.initialize();let{PendingMessageStore:i}=await Promise.resolve().then(()=>(wo(),Wu)),o=new i(this.dbManager.getSessionStore().db,3),c=300*1e3,u=o.resetStuckMessages(c);u>0&&A.info("SYSTEM",`Recovered ${u} stuck messages from previous session`,{thresholdMinutes:5});let l=new zl,d=new Hl,f=new Ul(this.dbManager.getSessionSearch(),this.dbManager.getSessionStore(),this.dbManager.getChromaSync(),l,d);this.searchRoutes=new rd(f),this.searchRoutes.setupRoutes(this.app),A.info("WORKER","SearchManager initialized and search routes registered");let m=Ka.default.join(__dirname,"mcp-server.cjs"),g=new zs({command:"node",args:[m],env:process.env}),_=3e5,p=this.mcpClient.connect(g),h=new Promise((y,v)=>setTimeout(()=>v(new Error("MCP connection timeout after 5 minutes")),_));await Promise.race([p,h]),this.mcpReady=!0,A.success("WORKER","Connected to MCP server"),this.initializationCompleteFlag=!0,this.resolveInitialization(),A.info("SYSTEM","Background initialization complete")}catch(e){throw A.error("SYSTEM","Background initialization failed",{},e),e}}async processPendingQueues(e=10){let{PendingMessageStore:r}=await Promise.resolve().then(()=>(wo(),Wu)),n=new r(this.dbManager.getSessionStore().db,3),a=n.getSessionsWithPendingMessages(),s={totalPendingSessions:a.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(a.length===0)return s;A.info("SYSTEM",`Processing up to ${e} of ${a.length} pending session queues`);for(let i of a){if(s.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(i)?.generatorPromise){s.sessionsSkipped++;continue}let c=this.sessionManager.initializeSession(i);A.info("SYSTEM",`Starting processor for session ${i}`,{project:c.project,pendingCount:n.getPendingCount(i)}),c.generatorPromise=this.sdkAgent.startSession(c,this).finally(()=>{c.generatorPromise=null,this.broadcastProcessingStatus()}),s.sessionsStarted++,s.startedSessionIds.push(i),await new Promise(u=>setTimeout(u,100))}catch(o){A.warn("SYSTEM",`Failed to process session ${i}`,{},o),s.sessionsSkipped++}}return s}extractInstructionSection(e,r){let n={workflow:this.extractBetween(e,"## The Workflow","## Search Parameters"),search_params:this.extractBetween(e,"## Search Parameters","## Examples"),examples:this.extractBetween(e,"## Examples","## Why This Workflow"),all:e};return n[r]||n.all}extractBetween(e,r,n){let a=e.indexOf(r),s=e.indexOf(n);return a===-1?e:s===-1?e.substring(a):e.substring(a,s).trim()}async shutdown(){A.info("SYSTEM","Shutdown initiated");let e=await this.getChildProcesses(process.pid);if(A.info("SYSTEM","Found child processes",{count:e.length,pids:e}),this.server&&(this.server.closeAllConnections(),await new Promise((r,n)=>{this.server.close(a=>a?n(a):r())}),this.server=null,A.info("SYSTEM","HTTP server closed")),await this.sessionManager.shutdownAll(),this.mcpClient&&(await this.mcpClient.close(),A.info("SYSTEM","MCP client closed")),await this.dbManager.close(),e.length>0){A.info("SYSTEM","Force killing remaining children");for(let r of e)await this.forceKillProcess(r);await this.waitForProcessesExit(e,5e3)}A.info("SYSTEM","Worker shutdown complete")}async getChildProcesses(e){if(process.platform!=="win32")return[];if(!Number.isInteger(e)||e<=0)return A.warn("SYSTEM","Invalid parent PID for child process enumeration",{parentPid:e}),[];try{let r=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${e} } | Select-Object -ExpandProperty ProcessId"`,{stdout:n}=await sd(r,{timeout:6e4});return n.trim().split(` `).map(a=>parseInt(a.trim(),10)).filter(a=>!isNaN(a)&&Number.isInteger(a)&&a>0)}catch(r){return A.warn("SYSTEM","Failed to enumerate child processes",{parentPid:e,error:r.message}),[]}}async forceKillProcess(e){if(!Number.isInteger(e)||e<=0){A.warn("SYSTEM","Invalid PID for force kill",{pid:e});return}try{process.platform==="win32"?await sd(`taskkill /PID ${e} /T /F`,{timeout:6e4}):process.kill(e,"SIGKILL"),A.info("SYSTEM","Killed process",{pid:e})}catch{A.debug("SYSTEM","Process already exited during force kill",{pid:e})}}async waitForProcessesExit(e,r){let n=Date.now();for(;Date.now()-n{try{return process.kill(s,0),!0}catch{return!1}});if(a.length===0){A.info("SYSTEM","All child processes exited");return}A.debug("SYSTEM","Waiting for processes to exit",{stillAlive:a}),await new Promise(s=>setTimeout(s,100))}A.warn("SYSTEM","Timeout waiting for child processes to exit")}summarizeRequestBody(e,r,n){return vR(e,r,n)}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();A.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function hV(){let t=process.argv[2],e=Mn();switch(t){case"start":{await xh(e)&&(console.log(id),process.exit(0));let r=(0,Kn.spawn)(process.execPath,[__filename,"--daemon"],{detached:!0,stdio:"ignore",windowsHide:!0,env:{...process.env,CLAUDE_MEM_WORKER_PORT:String(e)}});r.pid===void 0&&(console.error("Failed to spawn worker daemon"),process.exit(1)),r.unref(),OR({pid:r.pid,port:e,startedAt:new Date().toISOString()}),await CR(e,3e4)||(Zn(),console.error("Worker failed to start"),process.exit(1)),console.log(id),process.exit(0)}case"stop":await IR(e),await AR(e,1e4),Zn(),console.log(id),process.exit(0);case"restart":{await IR(e),await AR(e,1e4),Zn();let r=(0,Kn.spawn)(process.execPath,[__filename,"--daemon"],{detached:!0,stdio:"ignore",windowsHide:!0,env:{...process.env,CLAUDE_MEM_WORKER_PORT:String(e)}});r.pid===void 0&&(console.error("Failed to spawn worker daemon during restart"),process.exit(1)),r.unref(),OR({pid:r.pid,port:e,startedAt:new Date().toISOString()}),await CR(e,3e4)||(Zn(),console.error("Worker failed to restart"),process.exit(1)),console.log(id),process.exit(0)}case"status":{let r=await xh(e),n=mV();console.log(r&&n?`Worker running (PID: ${n.pid}, Port: ${n.port})`:"Worker not running"),process.exit(0)}case"--daemon":default:{let r=new od;process.on("SIGTERM",async()=>{A.info("SYSTEM","Received SIGTERM"),await r.shutdown(),Zn(),process.exit(0)}),process.on("SIGINT",async()=>{A.info("SYSTEM","Received SIGINT"),await r.shutdown(),Zn(),process.exit(0)}),r.start().catch(n=>{A.failure("SYSTEM","Worker failed to start",{},n),Zn(),process.exit(1)})}}}(require.main===module||!module.parent)&&hV();0&&(module.exports={WorkerService}); diff --git a/src/services/worker/http/routes/SettingsRoutes.ts b/src/services/worker/http/routes/SettingsRoutes.ts index 35846908..f78520e1 100644 --- a/src/services/worker/http/routes/SettingsRoutes.ts +++ b/src/services/worker/http/routes/SettingsRoutes.ts @@ -84,6 +84,14 @@ export class SettingsRoutes extends BaseRouteHandler { 'CLAUDE_MEM_PROVIDER', 'CLAUDE_MEM_GEMINI_API_KEY', 'CLAUDE_MEM_GEMINI_MODEL', + 'CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED', + // OpenRouter Configuration + 'CLAUDE_MEM_OPENROUTER_API_KEY', + 'CLAUDE_MEM_OPENROUTER_MODEL', + 'CLAUDE_MEM_OPENROUTER_SITE_URL', + 'CLAUDE_MEM_OPENROUTER_APP_NAME', + 'CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES', + 'CLAUDE_MEM_OPENROUTER_MAX_TOKENS', // System Configuration 'CLAUDE_MEM_DATA_DIR', 'CLAUDE_MEM_LOG_LEVEL',