diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..5dc3af17 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,3 @@ +{ + "env": {} +} diff --git a/.gitignore b/.gitignore index 81fc5f6d..baaa2b68 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,6 @@ node_modules/ .env.local *.tmp *.temp -.claude/ +.claude/settings.local.json plugin/data/ plugin/data.backup/ \ No newline at end of file diff --git a/package.json b/package.json index 43c6731f..84fe516c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "nodejs" ], "author": "Alex Newman", - "license": "SEE LICENSE IN LICENSE", + "license": "AGPL-3.0", "repository": { "type": "git", "url": "https://github.com/thedotmack/claude-mem.git" diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 951fc4fe..100f89b6 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -6,7 +6,7 @@ "name": "Alex Newman" }, "repository": "https://github.com/thedotmack/claude-mem", - "license": "SEE LICENSE IN LICENSE", + "license": "AGPL-3.0", "keywords": [ "memory", "context", diff --git a/plugin/scripts/claude-mem-settings.sh b/plugin/scripts/claude-mem-settings.sh new file mode 100755 index 00000000..78e5f546 --- /dev/null +++ b/plugin/scripts/claude-mem-settings.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# claude-mem-settings.sh - User settings manager for claude-mem plugin + +USER_SETTINGS_FILE="$HOME/.claude/settings.json" + +# Function to check if jq is available +check_jq() { + if ! command -v jq &> /dev/null; then + echo "Error: jq is required for JSON manipulation" + echo "Install with: brew install jq" + exit 1 + fi +} + +# Function to create settings file if it doesn't exist +ensure_settings_file() { + if [ ! -f "$USER_SETTINGS_FILE" ]; then + mkdir -p "$(dirname "$USER_SETTINGS_FILE")" + echo '{}' > "$USER_SETTINGS_FILE" + fi +} + +# Function to get current model setting +get_model() { + if [ -f "$USER_SETTINGS_FILE" ]; then + jq -r '.env.CLAUDE_MEM_MODEL // "claude-sonnet-4-5"' "$USER_SETTINGS_FILE" + else + echo "claude-sonnet-4-5" + fi +} + +# Function to set model setting +set_model() { + local model=$1 + + ensure_settings_file + + # Update or create the env.CLAUDE_MEM_MODEL setting + jq --arg model "$model" '.env.CLAUDE_MEM_MODEL = $model' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE" + echo "Set CLAUDE_MEM_MODEL to: $model" +} + +# Function to remove model setting +remove_model() { + if [ -f "$USER_SETTINGS_FILE" ]; then + jq 'del(.env.CLAUDE_MEM_MODEL)' "$USER_SETTINGS_FILE" > tmp.json && mv tmp.json "$USER_SETTINGS_FILE" + echo "Removed CLAUDE_MEM_MODEL (will use default: claude-sonnet-4-5)" + fi +} + +# Function to list available models +list_models() { + echo "Available models:" + echo " claude-haiku-4-5 - Fast and efficient" + echo " claude-sonnet-4-5 - Balanced (default)" + echo " claude-opus-4 - Most capable" + echo " claude-3-7-sonnet - Alternative version" +} + +# Interactive menu +show_menu() { + echo "Claude Mem Plugin - Model Configuration" + echo "======================================" + echo "Current model: $(get_model)" + echo "Settings file: $USER_SETTINGS_FILE" + echo "" + echo "1) Set model" + echo "2) Remove model setting (use default)" + echo "3) List available models" + echo "4) Exit" + echo "" +} + +# Main interactive loop +main() { + check_jq + + while true; do + show_menu + read -p "Choose an option (1-4): " choice + + case $choice in + 1) + list_models + echo "" + read -p "Enter model name: " model + set_model "$model" + ;; + 2) + remove_model + ;; + 3) + list_models + ;; + 4) + echo "Goodbye!" + exit 0 + ;; + *) + echo "Invalid option. Please choose 1-4." + ;; + esac + echo "" + read -p "Press Enter to continue..." + done +} + +# Run main if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 008c5537..760185d0 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -402,7 +402,7 @@ Output this XML: **Required fields**: request, investigated, learned, completed, next_steps -**Optional fields**: notes`}function xg(t,e){let r=[],a=/([\s\S]*?)<\/observation>/g,i;for(;(i=a.exec(t))!==null;){let s=i[1],n=Va(s,"type"),o=Va(s,"title"),p=Va(s,"subtitle"),c=Va(s,"narrative"),l=ps(s,"facts","fact"),u=ps(s,"concepts","concept"),d=ps(s,"files_read","file"),h=ps(s,"files_modified","file");if(!n||!o||!p||!c){pe.warn("PARSER","Observation missing required fields, skipping",{correlationId:e,hasType:!!n,hasTitle:!!o,hasSubtitle:!!p,hasNarrative:!!c});continue}if(!["change","discovery","decision"].includes(n.trim())){pe.warn("PARSER",`Invalid observation type: ${n}, skipping`,{correlationId:e});continue}r.push({type:n.trim(),title:o,subtitle:p,facts:l,narrative:c,concepts:u,files_read:d,files_modified:h})}return r}function yg(t,e){let a=/([\s\S]*?)<\/summary>/.exec(t);if(!a)return null;let i=a[1],s=Va(i,"request"),n=Va(i,"investigated"),o=Va(i,"learned"),p=Va(i,"completed"),c=Va(i,"next_steps"),l=Va(i,"notes");return!s||!n||!o||!p||!c?(pe.warn("PARSER","Summary missing required fields",{sessionId:e,hasRequest:!!s,hasInvestigated:!!n,hasLearned:!!o,hasCompleted:!!p,hasNextSteps:!!c}),null):{request:s,investigated:n,learned:o,completed:p,next_steps:c,notes:l}}function Va(t,e){let a=new RegExp(`<${e}>([^<]*)`).exec(t);return a?a[1].trim():null}function ps(t,e,r){let a=[],s=new RegExp(`<${e}>(.*?)`,"s").exec(t);if(!s)return a;let n=s[1],o=new RegExp(`<${r}>([^<]+)`,"g"),p;for(;(p=o.exec(n))!==null;)a.push(p[1].trim());return a}var Q2="claude-sonnet-4-5",J2=["Glob","Grep","ListMcpResourcesTool","WebSearch"],ls=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),us=class{app;port=null;sessions=new Map;constructor(){this.app=(0,Vc.default)(),this.app.use(Vc.default.json({limit:"50mb"})),this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/sessions/:sessionDbId/init",this.handleInit.bind(this)),this.app.post("/sessions/:sessionDbId/observations",this.handleObservation.bind(this)),this.app.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),this.app.get("/sessions/:sessionDbId/status",this.handleStatus.bind(this)),this.app.delete("/sessions/:sessionDbId",this.handleDelete.bind(this))}async start(){this.port=ls;let e=new Aa,r=e.cleanupOrphanedSessions();return e.close(),r>0&&pe.info("SYSTEM",`Cleaned up ${r} orphaned sessions`),new Promise((a,i)=>{this.app.listen(ls,"127.0.0.1",()=>{pe.info("SYSTEM","Worker started",{port:ls,pid:process.pid,activeSessions:this.sessions.size}),a()}).on("error",s=>{s.code==="EADDRINUSE"&&pe.error("SYSTEM",`Port ${ls} already in use - worker may already be running`),i(s)})})}handleHealth(e,r){r.json({status:"ok",port:this.port,pid:process.pid,activeSessions:this.sessions.size,uptime:process.uptime(),memory:process.memoryUsage()})}async handleInit(e,r){let a=parseInt(e.params.sessionDbId,10),{project:i,userPrompt:s}=e.body,n=pe.sessionId(a);if(pe.info("WORKER","Session init",{correlationId:n,project:i}),this.sessions.has(a)){r.status(409).json({error:"Session already exists"});return}let o={sessionDbId:a,sdkSessionId:null,project:i,userPrompt:s,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()};this.sessions.set(a,o);let p=new Aa;p.setWorkerPort(a,this.port),p.close(),o.generatorPromise=this.runSDKAgent(o).catch(c=>{pe.failure("WORKER","SDK agent error",{sessionId:a},c);let l=new Aa;l.markSessionFailed(a),l.close(),this.sessions.delete(a)}),pe.success("WORKER","Session initialized",{sessionId:a,port:this.port}),r.json({status:"initialized",sessionDbId:a,port:this.port})}handleObservation(e,r){let a=parseInt(e.params.sessionDbId,10),{tool_name:i,tool_input:s,tool_output:n,prompt_number:o}=e.body,p=this.sessions.get(a);if(!p){r.status(404).json({error:"Session not found"});return}p.observationCounter++;let c=pe.correlationId(a,p.observationCounter),l=pe.formatTool(i,s);pe.dataIn("WORKER",`Observation queued: ${l}`,{correlationId:c,queue:p.pendingMessages.length+1}),p.pendingMessages.push({type:"observation",tool_name:i,tool_input:s,tool_output:n,prompt_number:o}),r.json({status:"queued",queueLength:p.pendingMessages.length})}handleSummarize(e,r){let a=parseInt(e.params.sessionDbId,10),{prompt_number:i}=e.body,s=this.sessions.get(a);if(!s){r.status(404).json({error:"Session not found"});return}pe.dataIn("WORKER","Summary requested",{sessionId:a,promptNumber:i,queue:s.pendingMessages.length+1}),s.pendingMessages.push({type:"summarize",prompt_number:i}),r.json({status:"queued",queueLength:s.pendingMessages.length})}handleStatus(e,r){let a=parseInt(e.params.sessionDbId,10),i=this.sessions.get(a);if(!i){r.status(404).json({error:"Session not found"});return}r.json({sessionDbId:a,sdkSessionId:i.sdkSessionId,project:i.project,pendingMessages:i.pendingMessages.length})}async handleDelete(e,r){let a=parseInt(e.params.sessionDbId,10),i=this.sessions.get(a);if(!i){r.status(404).json({error:"Session not found"});return}pe.warn("WORKER","Session delete requested",{sessionId:a}),i.abortController.abort(),i.generatorPromise&&await Promise.race([i.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let s=new Aa;s.markSessionFailed(a),s.close(),this.sessions.delete(a),pe.info("WORKER","Session deleted",{sessionId:a}),r.json({status:"deleted"})}async runSDKAgent(e){pe.info("SDK","Agent starting",{sessionId:e.sessionDbId});let r=process.env.CLAUDE_CODE_PATH||"/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude";try{let a=lg({prompt:this.createMessageGenerator(e),options:{model:Q2,disallowedTools:J2,abortController:e.abortController,pathToClaudeCodeExecutable:r}});for await(let n of a)if(n.type==="system"&&n.subtype==="init"){let o=n;if(o.session_id){let p=new Aa,c=p.updateSDKSessionId(e.sessionDbId,o.session_id);p.close(),c&&(pe.success("SDK","Session initialized",{sessionId:e.sessionDbId,sdkSessionId:o.session_id}),e.sdkSessionId=o.session_id)}}else if(n.type==="assistant"){let o=n.message.content,p=Array.isArray(o)?o.filter(l=>l.type==="text").map(l=>l.text).join(` +**Optional fields**: notes`}function xg(t,e){let r=[],a=/([\s\S]*?)<\/observation>/g,i;for(;(i=a.exec(t))!==null;){let s=i[1],n=Va(s,"type"),o=Va(s,"title"),p=Va(s,"subtitle"),c=Va(s,"narrative"),l=ps(s,"facts","fact"),u=ps(s,"concepts","concept"),d=ps(s,"files_read","file"),h=ps(s,"files_modified","file");if(!n||!o||!p||!c){pe.warn("PARSER","Observation missing required fields, skipping",{correlationId:e,hasType:!!n,hasTitle:!!o,hasSubtitle:!!p,hasNarrative:!!c});continue}if(!["change","discovery","decision"].includes(n.trim())){pe.warn("PARSER",`Invalid observation type: ${n}, skipping`,{correlationId:e});continue}r.push({type:n.trim(),title:o,subtitle:p,facts:l,narrative:c,concepts:u,files_read:d,files_modified:h})}return r}function yg(t,e){let a=/([\s\S]*?)<\/summary>/.exec(t);if(!a)return null;let i=a[1],s=Va(i,"request"),n=Va(i,"investigated"),o=Va(i,"learned"),p=Va(i,"completed"),c=Va(i,"next_steps"),l=Va(i,"notes");return!s||!n||!o||!p||!c?(pe.warn("PARSER","Summary missing required fields",{sessionId:e,hasRequest:!!s,hasInvestigated:!!n,hasLearned:!!o,hasCompleted:!!p,hasNextSteps:!!c}),null):{request:s,investigated:n,learned:o,completed:p,next_steps:c,notes:l}}function Va(t,e){let a=new RegExp(`<${e}>([^<]*)`).exec(t);return a?a[1].trim():null}function ps(t,e,r){let a=[],s=new RegExp(`<${e}>(.*?)`,"s").exec(t);if(!s)return a;let n=s[1],o=new RegExp(`<${r}>([^<]+)`,"g"),p;for(;(p=o.exec(n))!==null;)a.push(p[1].trim());return a}var Q2=process.env.CLAUDE_MEM_MODEL||"claude-sonnet-4-5",J2=["Glob","Grep","ListMcpResourcesTool","WebSearch"],ls=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),us=class{app;port=null;sessions=new Map;constructor(){this.app=(0,Vc.default)(),this.app.use(Vc.default.json({limit:"50mb"})),this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/sessions/:sessionDbId/init",this.handleInit.bind(this)),this.app.post("/sessions/:sessionDbId/observations",this.handleObservation.bind(this)),this.app.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),this.app.get("/sessions/:sessionDbId/status",this.handleStatus.bind(this)),this.app.delete("/sessions/:sessionDbId",this.handleDelete.bind(this))}async start(){this.port=ls;let e=new Aa,r=e.cleanupOrphanedSessions();return e.close(),r>0&&pe.info("SYSTEM",`Cleaned up ${r} orphaned sessions`),new Promise((a,i)=>{this.app.listen(ls,"127.0.0.1",()=>{pe.info("SYSTEM","Worker started",{port:ls,pid:process.pid,activeSessions:this.sessions.size}),a()}).on("error",s=>{s.code==="EADDRINUSE"&&pe.error("SYSTEM",`Port ${ls} already in use - worker may already be running`),i(s)})})}handleHealth(e,r){r.json({status:"ok",port:this.port,pid:process.pid,activeSessions:this.sessions.size,uptime:process.uptime(),memory:process.memoryUsage()})}async handleInit(e,r){let a=parseInt(e.params.sessionDbId,10),{project:i,userPrompt:s}=e.body,n=pe.sessionId(a);if(pe.info("WORKER","Session init",{correlationId:n,project:i}),this.sessions.has(a)){r.status(409).json({error:"Session already exists"});return}let o={sessionDbId:a,sdkSessionId:null,project:i,userPrompt:s,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()};this.sessions.set(a,o);let p=new Aa;p.setWorkerPort(a,this.port),p.close(),o.generatorPromise=this.runSDKAgent(o).catch(c=>{pe.failure("WORKER","SDK agent error",{sessionId:a},c);let l=new Aa;l.markSessionFailed(a),l.close(),this.sessions.delete(a)}),pe.success("WORKER","Session initialized",{sessionId:a,port:this.port}),r.json({status:"initialized",sessionDbId:a,port:this.port})}handleObservation(e,r){let a=parseInt(e.params.sessionDbId,10),{tool_name:i,tool_input:s,tool_output:n,prompt_number:o}=e.body,p=this.sessions.get(a);if(!p){r.status(404).json({error:"Session not found"});return}p.observationCounter++;let c=pe.correlationId(a,p.observationCounter),l=pe.formatTool(i,s);pe.dataIn("WORKER",`Observation queued: ${l}`,{correlationId:c,queue:p.pendingMessages.length+1}),p.pendingMessages.push({type:"observation",tool_name:i,tool_input:s,tool_output:n,prompt_number:o}),r.json({status:"queued",queueLength:p.pendingMessages.length})}handleSummarize(e,r){let a=parseInt(e.params.sessionDbId,10),{prompt_number:i}=e.body,s=this.sessions.get(a);if(!s){r.status(404).json({error:"Session not found"});return}pe.dataIn("WORKER","Summary requested",{sessionId:a,promptNumber:i,queue:s.pendingMessages.length+1}),s.pendingMessages.push({type:"summarize",prompt_number:i}),r.json({status:"queued",queueLength:s.pendingMessages.length})}handleStatus(e,r){let a=parseInt(e.params.sessionDbId,10),i=this.sessions.get(a);if(!i){r.status(404).json({error:"Session not found"});return}r.json({sessionDbId:a,sdkSessionId:i.sdkSessionId,project:i.project,pendingMessages:i.pendingMessages.length})}async handleDelete(e,r){let a=parseInt(e.params.sessionDbId,10),i=this.sessions.get(a);if(!i){r.status(404).json({error:"Session not found"});return}pe.warn("WORKER","Session delete requested",{sessionId:a}),i.abortController.abort(),i.generatorPromise&&await Promise.race([i.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let s=new Aa;s.markSessionFailed(a),s.close(),this.sessions.delete(a),pe.info("WORKER","Session deleted",{sessionId:a}),r.json({status:"deleted"})}async runSDKAgent(e){pe.info("SDK","Agent starting",{sessionId:e.sessionDbId});let r=process.env.CLAUDE_CODE_PATH||"/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude";try{let a=lg({prompt:this.createMessageGenerator(e),options:{model:Q2,disallowedTools:J2,abortController:e.abortController,pathToClaudeCodeExecutable:r}});for await(let n of a)if(n.type==="system"&&n.subtype==="init"){let o=n;if(o.session_id){let p=new Aa,c=p.updateSDKSessionId(e.sessionDbId,o.session_id);p.close(),c&&(pe.success("SDK","Session initialized",{sessionId:e.sessionDbId,sdkSessionId:o.session_id}),e.sdkSessionId=o.session_id)}}else if(n.type==="assistant"){let o=n.message.content,p=Array.isArray(o)?o.filter(l=>l.type==="text").map(l=>l.text).join(` `):typeof o=="string"?o:"",c=p.length;pe.dataOut("SDK",`Response received (${c} chars)`,{sessionId:e.sessionDbId,promptNumber:e.lastPromptNumber}),pe.debug("SDK","Full response",{sessionId:e.sessionDbId},p),this.handleAgentMessage(e,p,e.lastPromptNumber)}let i=Date.now()-e.startTime;pe.success("SDK","Agent completed",{sessionId:e.sessionDbId,duration:`${(i/1e3).toFixed(1)}s`});let s=new Aa;s.markSessionCompleted(e.sessionDbId),s.close(),this.sessions.delete(e.sessionDbId)}catch(a){throw a.name==="AbortError"?pe.warn("SDK","Agent aborted",{sessionId:e.sessionDbId}):pe.failure("SDK","Agent error",{sessionId:e.sessionDbId},a),a}}async*createMessageGenerator(e){let r=`session-${e.sessionDbId}`,a=hg(e.project,r,e.userPrompt);for(pe.dataIn("SDK",`Init prompt sent (${a.length} chars)`,{sessionId:e.sessionDbId,project:e.project}),pe.debug("SDK","Full init prompt",{sessionId:e.sessionDbId},a),yield{type:"user",session_id:e.sdkSessionId||r,parent_tool_use_id:null,message:{role:"user",content:a}};!e.abortController.signal.aborted;){if(e.pendingMessages.length===0){await new Promise(i=>setTimeout(i,100));continue}for(;e.pendingMessages.length>0;){let i=e.pendingMessages.shift();if(i.type==="summarize"){e.lastPromptNumber=i.prompt_number;let s=new Aa,n=s.getSessionById(e.sessionDbId);if(s.close(),n){let o=gg(n);pe.dataIn("SDK",`Summary prompt sent (${o.length} chars)`,{sessionId:e.sessionDbId,promptNumber:i.prompt_number}),pe.debug("SDK","Full summary prompt",{sessionId:e.sessionDbId},o),yield{type:"user",session_id:e.sdkSessionId||r,parent_tool_use_id:null,message:{role:"user",content:o}}}}else if(i.type==="observation"){e.lastPromptNumber=i.prompt_number;let s=vg({id:0,tool_name:i.tool_name,tool_input:i.tool_input,tool_output:i.tool_output,created_at_epoch:Date.now()}),n=pe.formatTool(i.tool_name,i.tool_input),o=pe.correlationId(e.sessionDbId,e.observationCounter);pe.dataIn("SDK",`Observation prompt: ${n}`,{correlationId:o,promptNumber:i.prompt_number,size:`${s.length} chars`}),pe.debug("SDK","Full observation prompt",{correlationId:o},s),yield{type:"user",session_id:e.sdkSessionId||r,parent_tool_use_id:null,message:{role:"user",content:s}}}}}}handleAgentMessage(e,r,a){let i=pe.correlationId(e.sessionDbId,e.observationCounter),s=xg(r,i);s.length>0&&pe.info("PARSER",`Parsed ${s.length} observation(s)`,{correlationId:i,promptNumber:a,types:s.map(p=>p.type).join(", ")});let n=new Aa;for(let p of s)e.sdkSessionId&&(n.storeObservation(e.sdkSessionId,e.project,p,a),pe.success("DB","Observation stored",{correlationId:i,type:p.type,title:p.title}));let o=yg(r,e.sessionDbId);o&&e.sdkSessionId&&(pe.info("PARSER","Summary parsed",{sessionId:e.sessionDbId,promptNumber:a}),n.storeSummary(e.sdkSessionId,e.project,o,a),pe.success("DB","Summary stored",{sessionId:e.sessionDbId})),n.close()}};async function Y2(){await new us().start(),process.on("SIGINT",()=>{pe.warn("SYSTEM","Shutting down (SIGINT)"),process.exit(0)}),process.on("SIGTERM",()=>{pe.warn("SYSTEM","Shutting down (SIGTERM)"),process.exit(0)})}Y2().catch(t=>{pe.failure("SYSTEM","Fatal startup error",{},t),process.exit(1)});0&&(module.exports={WorkerService}); /*! Bundled license information: diff --git a/src/services/worker-service.ts b/src/services/worker-service.ts index 697cfa17..9348a6e3 100644 --- a/src/services/worker-service.ts +++ b/src/services/worker-service.ts @@ -13,7 +13,7 @@ import type { SDKSession } from '../sdk/prompts.js'; import { logger } from '../utils/logger.js'; import { ensureAllDataDirs } from '../shared/paths.js'; -const MODEL = 'claude-sonnet-4-5'; +const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5'; const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch']; const FIXED_PORT = parseInt(process.env.CLAUDE_MEM_WORKER_PORT || '37777', 10);