feat: add OpenRouter provider support and enhance context generation
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.
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"bun": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run build-and-sync",
|
||||
"build": "node scripts/build-hooks.js",
|
||||
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart",
|
||||
"sync-marketplace": "node scripts/sync-marketplace.cjs",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
import{stdin as C}from"process";import M from"path";import{homedir as x}from"os";import{readFileSync as b}from"fs";import{readFileSync as $,writeFileSync as v,existsSync as P}from"fs";import{join as w}from"path";import{homedir as W}from"os";var m="bugfix,feature,refactor,discovery,decision,change",D="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_DATA_DIR:w(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:m,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:D,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!P(t))return this.getAllDefaults();let r=$(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),a.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){a.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(s[i]=n[i]);return s}catch(r){return a.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var p=(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))(p||{}),O=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=c.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=p[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),S=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${s}:${i}:${E}.${S}`}log(t,r,e,n,s){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=p[t].padEnd(5),S=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let l="";s!=null&&(this.getLevel()===0&&typeof s=="object"?l=`
|
||||
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let T="";if(n){let{sessionId:B,sdkSessionId:Y,correlationId:J,...A}=n;Object.keys(A).length>0&&(T=` {${Object.entries(A).map(([k,y])=>`${k}=${y}`).join(", ")}}`)}let L=`[${i}] [${E}] [${S}] ${_}${e}${T}${l}`;t===3?console.error(L):console.log(L)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,s=""){let _=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",T={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,T,n),s}},a=new O;var g={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(o){return process.platform==="win32"?Math.round(o*g.WINDOWS_MULTIPLIER):o}function I(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,s=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${s}${i}
|
||||
import{stdin as C}from"process";import M from"path";import{homedir as x}from"os";import{readFileSync as b}from"fs";import{readFileSync as $,writeFileSync as P,existsSync as v}from"fs";import{join as w}from"path";import{homedir as W}from"os";var m="bugfix,feature,refactor,discovery,decision,change",D="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:w(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:m,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:D,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!v(t))return this.getAllDefaults();let r=$(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{P(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let s={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(s[i]=n[i]);return s}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var p=(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))(p||{}),f=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=c.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=p[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${s}:${i}:${E}.${T}`}log(t,r,e,n,s){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=p[t].padEnd(5),T=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";s!=null&&(this.getLevel()===0&&typeof s=="object"?l=`
|
||||
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let S="";if(n){let{sessionId:Y,sdkSessionId:B,correlationId:J,...L}=n;Object.keys(L).length>0&&(S=` {${Object.entries(L).map(([k,y])=>`${k}=${y}`).join(", ")}}`)}let A=`[${i}] [${E}] [${T}] ${a}${e}${S}${l}`;t===3?console.error(A):console.log(A)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,s=""){let a=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",S={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,S,n),s}},_=new f;var g={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function R(o){return process.platform==="win32"?Math.round(o*g.WINDOWS_MULTIPLIER):o}function U(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,s=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${s}${i}
|
||||
|
||||
`;return E+=`To restart the worker:
|
||||
`,E+=`1. Exit Claude Code completely
|
||||
@@ -11,4 +11,4 @@ ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Obje
|
||||
|
||||
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
|
||||
|
||||
${E}`),E}var H=M.join(x(),".claude","plugins","marketplaces","thedotmack"),R=d(g.HEALTH_CHECK),f=null;function u(){if(f!==null)return f;let o=M.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(o);return f=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),f}async function F(){let o=u();return(await fetch(`http://127.0.0.1:${o}/api/readiness`,{signal:AbortSignal.timeout(R)})).ok}function G(){let o=M.join(H,"package.json");return JSON.parse(b(o,"utf-8")).version}async function K(){let o=u(),t=await fetch(`http://127.0.0.1:${o}/api/version`,{signal:AbortSignal.timeout(R)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function j(){let o=G(),t=await K();o!==t&&a.warn("SYSTEM","Worker version mismatch",{pluginVersion:o,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function U(){for(let r=0;r<25;r++){try{if(await F()){await j();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:u(),customPrefix:"Worker did not become ready within 5 seconds."}))}import V from"path";function h(o){if(!o||o.trim()==="")return a.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=V.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let s=`drive-${e[1].toUpperCase()}`;return a.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:s}),s}}return a.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}async function N(o){await U();let t=o?.cwd??process.cwd(),r=h(t),n=`http://127.0.0.1:${u()}/api/context/inject?project=${encodeURIComponent(r)}`,s=await fetch(n,{signal:AbortSignal.timeout(g.DEFAULT)});if(!s.ok)throw new Error(`Context generation failed: ${s.status}`);return(await s.text()).trim()}var X=process.argv.includes("--colors");if(C.isTTY||X)N(void 0).then(o=>{console.log(o),process.exit(0)});else{let o="";C.on("data",t=>o+=t),C.on("end",async()=>{let t;try{t=o.trim()?JSON.parse(o):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await N(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
|
||||
${E}`),E}var H=M.join(x(),".claude","plugins","marketplaces","thedotmack"),d=R(g.HEALTH_CHECK),O=null;function u(){if(O!==null)return O;let o=M.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(o);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function F(){let o=u();return(await fetch(`http://127.0.0.1:${o}/api/readiness`,{signal:AbortSignal.timeout(d)})).ok}function G(){let o=M.join(H,"package.json");return JSON.parse(b(o,"utf-8")).version}async function K(){let o=u(),t=await fetch(`http://127.0.0.1:${o}/api/version`,{signal:AbortSignal.timeout(d)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function j(){let o=G(),t=await K();o!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:o,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function I(){for(let r=0;r<25;r++){try{if(await F()){await j();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(U({port:u(),customPrefix:"Worker did not become ready within 5 seconds."}))}import V from"path";function N(o){if(!o||o.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=V.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let s=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:s}),s}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}async function h(o){await I();let t=o?.cwd??process.cwd(),r=N(t),n=`http://127.0.0.1:${u()}/api/context/inject?project=${encodeURIComponent(r)}`,s=await fetch(n,{signal:AbortSignal.timeout(g.DEFAULT)});if(!s.ok)throw new Error(`Context generation failed: ${s.status}`);return(await s.text()).trim()}var X=process.argv.includes("--colors");if(C.isTTY||X)h(void 0).then(o=>{console.log(o),process.exit(0)});else{let o="";C.on("data",t=>o+=t),C.on("end",async()=>{let t;try{t=o.trim()?JSON.parse(o):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await h(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
import{stdin as k}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import C from"path";import{homedir as H}from"os";import{readFileSync as x}from"fs";import{readFileSync as P,writeFileSync as v,existsSync as w}from"fs";import{join as b}from"path";import{homedir as W}from"os";var D="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_DATA_DIR:b(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!w(t))return this.getAllDefaults();let r=P(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let s={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(s[E]=n[E]);return s}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var T=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(T||{}),O=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=g.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=T[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${s}:${E}:${i}.${c}`}log(t,r,e,n,s){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=T[t].padEnd(5),c=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";s!=null&&(this.getLevel()===0&&typeof s=="object"?l=`
|
||||
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let u="";if(n){let{sessionId:Y,sdkSessionId:J,correlationId:q,...L}=n;Object.keys(L).length>0&&(u=` {${Object.entries(L).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let m=`[${E}] [${i}] [${c}] ${a}${e}${u}${l}`;t===3?console.error(m):console.log(m)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,s=""){let a=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),s}},_=new O;var M={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function R(o){return process.platform==="win32"?Math.round(o*M.WINDOWS_MULTIPLIER):o}function I(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,s=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${s}${E}
|
||||
import{stdin as k}from"process";var T=JSON.stringify({continue:!0,suppressOutput:!0});import A from"path";import{homedir as H}from"os";import{readFileSync as x}from"fs";import{readFileSync as P,writeFileSync as v,existsSync as w}from"fs";import{join as b}from"path";import{homedir as W}from"os";var D="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:b(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!w(t))return this.getAllDefaults();let r=P(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let s={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(s[E]=n[E]);return s}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var O=(s=>(s[s.DEBUG=0]="DEBUG",s[s.INFO=1]="INFO",s[s.WARN=2]="WARN",s[s.ERROR=3]="ERROR",s[s.SILENT=4]="SILENT",s))(O||{}),f=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=g.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),c=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${s}:${E}:${i}.${c}`}log(t,r,e,n,s){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=O[t].padEnd(5),c=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";s!=null&&(this.getLevel()===0&&typeof s=="object"?l=`
|
||||
`+JSON.stringify(s,null,2):l=" "+this.formatData(s));let u="";if(n){let{sessionId:Y,sdkSessionId:J,correlationId:q,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let L=`[${E}] [${i}] [${c}] ${a}${e}${u}${l}`;t===3?console.error(L):console.log(L)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,s=""){let a=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),s}},_=new f;var M={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function R(o){return process.platform==="win32"?Math.round(o*M.WINDOWS_MULTIPLIER):o}function U(o={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=o,s=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${s}${E}
|
||||
|
||||
`;return i+=`To restart the worker:
|
||||
`,i+=`1. Exit Claude Code completely
|
||||
@@ -11,4 +11,4 @@ ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Obje
|
||||
|
||||
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
|
||||
|
||||
${i}`),i}var F=C.join(H(),".claude","plugins","marketplaces","thedotmack"),h=R(M.HEALTH_CHECK),S=null;function p(){if(S!==null)return S;let o=C.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(o);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function G(){let o=p();return(await fetch(`http://127.0.0.1:${o}/api/readiness`,{signal:AbortSignal.timeout(h)})).ok}function K(){let o=C.join(F,"package.json");return JSON.parse(x(o,"utf-8")).version}async function j(){let o=p(),t=await fetch(`http://127.0.0.1:${o}/api/version`,{signal:AbortSignal.timeout(h)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let o=K(),t=await j();o!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:o,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function U(){for(let r=0;r<25;r++){try{if(await G()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:p(),customPrefix:"Worker did not become ready within 5 seconds."}))}import X from"path";function N(o){if(!o||o.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=X.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let s=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:s}),s}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}async function B(o){if(await U(),!o)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=o,n=N(r),s=p(),E=await fetch(`http://127.0.0.1:${s}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,project:n,prompt:e}),signal:AbortSignal.timeout(5e3)});if(!E.ok)throw new Error(`Session initialization failed: ${E.status}`);let i=await E.json(),c=i.sessionDbId,a=i.promptNumber;if(i.skipped&&i.reason==="private"){console.error(`[new-hook] Session ${c}, prompt #${a} (fully private - skipped)`),console.log(f);return}console.error(`[new-hook] Session ${c}, prompt #${a}`);let l=e.startsWith("/")?e.substring(1):e,u=await fetch(`http://127.0.0.1:${s}/sessions/${c}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:l,promptNumber:a}),signal:AbortSignal.timeout(5e3)});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);console.log(f)}var A="";k.on("data",o=>A+=o);k.on("end",async()=>{let o;try{o=A?JSON.parse(A):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await B(o)});
|
||||
${i}`),i}var F=A.join(H(),".claude","plugins","marketplaces","thedotmack"),I=R(M.HEALTH_CHECK),S=null;function p(){if(S!==null)return S;let o=A.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(o);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function K(){let o=p();return(await fetch(`http://127.0.0.1:${o}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function G(){let o=A.join(F,"package.json");return JSON.parse(x(o,"utf-8")).version}async function j(){let o=p(),t=await fetch(`http://127.0.0.1:${o}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let o=G(),t=await j();o!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:o,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function N(){for(let r=0;r<25;r++){try{if(await K()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(U({port:p(),customPrefix:"Worker did not become ready within 5 seconds."}))}import X from"path";function h(o){if(!o||o.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:o}),"unknown-project";let t=X.basename(o);if(t===""){if(process.platform==="win32"){let e=o.match(/^([A-Z]):\\/i);if(e){let s=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:o,projectName:s}),s}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:o}),"unknown-project"}return t}async function B(o){if(await N(),!o)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=o,n=h(r),s=p(),E=await fetch(`http://127.0.0.1:${s}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,project:n,prompt:e}),signal:AbortSignal.timeout(5e3)});if(!E.ok)throw new Error(`Session initialization failed: ${E.status}`);let i=await E.json(),c=i.sessionDbId,a=i.promptNumber;if(i.skipped&&i.reason==="private"){console.error(`[new-hook] Session ${c}, prompt #${a} (fully private - skipped)`),console.log(T);return}console.error(`[new-hook] Session ${c}, prompt #${a}`);let l=e.startsWith("/")?e.substring(1):e,u=await fetch(`http://127.0.0.1:${s}/sessions/${c}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:l,promptNumber:a}),signal:AbortSignal.timeout(5e3)});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);console.log(T)}var C="";k.on("data",o=>C+=o);k.on("end",async()=>{let o;try{o=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await B(o)});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
import{stdin as N}from"process";var D=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as k,writeFileSync as P,existsSync as v}from"fs";import{join as w}from"path";import{homedir as H}from"os";var m="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_DATA_DIR:w(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:m,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!v(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{P(t,JSON.stringify(n,null,2),"utf-8"),a.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){a.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return a.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),p=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=c.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=f[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=f[t].padEnd(5),_=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let u="";o!=null&&(this.getLevel()===0&&typeof o=="object"?u=`
|
||||
`+JSON.stringify(o,null,2):u=" "+this.formatData(o));let S="";if(n){let{sessionId:j,sdkSessionId:B,correlationId:Y,...L}=n;Object.keys(L).length>0&&(S=` {${Object.entries(L).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let A=`[${i}] [${E}] [${_}] ${l}${e}${S}${u}`;t===3?console.error(A):console.log(A)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),u=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",S={...e,location:u};return this.warn(t,`[HAPPY-PATH] ${r}`,S,n),o}},a=new p;import M from"path";import{homedir as W}from"os";import{readFileSync as b}from"fs";var g={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function U(s){return process.platform==="win32"?Math.round(s*g.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i}
|
||||
import{stdin as h}from"process";var D=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as k,writeFileSync as P,existsSync as v}from"fs";import{join as w}from"path";import{homedir as H}from"os";var m="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:w(H(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:m,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!v(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{P(t,JSON.stringify(n,null,2),"utf-8"),_.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(i){_.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},i)}}let o={...this.DEFAULTS};for(let i of Object.keys(this.DEFAULTS))n[i]!==void 0&&(o[i]=n[i]);return o}catch(r){return _.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var M=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(M||{}),f=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=c.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=M[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),a=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${a}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=M[t].padEnd(5),a=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let g="";o!=null&&(this.getLevel()===0&&typeof o=="object"?g=`
|
||||
`+JSON.stringify(o,null,2):g=" "+this.formatData(o));let O="";if(n){let{sessionId:j,sdkSessionId:B,correlationId:Y,...C}=n;Object.keys(C).length>0&&(O=` {${Object.entries(C).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let L=`[${i}] [${E}] [${a}] ${l}${e}${O}${g}`;t===3?console.error(L):console.log(L)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),g=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",O={...e,location:g};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new f;import p from"path";import{homedir as W}from"os";import{readFileSync as b}from"fs";var u={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function R(s){return process.platform==="win32"?Math.round(s*u.WINDOWS_MULTIPLIER):s}function d(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",i=t?` (port ${t})`:"",E=`${o}${i}
|
||||
|
||||
`;return E+=`To restart the worker:
|
||||
`,E+=`1. Exit Claude Code completely
|
||||
@@ -11,4 +11,4 @@ ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Obje
|
||||
|
||||
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
|
||||
|
||||
${E}`),E}var F=M.join(W(),".claude","plugins","marketplaces","thedotmack"),R=U(g.HEALTH_CHECK),O=null;function T(){if(O!==null)return O;let s=M.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function x(){let s=T();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(R)})).ok}function K(){let s=M.join(F,"package.json");return JSON.parse(b(s,"utf-8")).version}async function G(){let s=T(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(R)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let s=K(),t=await G();s!==t&&a.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function h(){for(let r=0;r<25;r++){try{if(await x()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:T(),customPrefix:"Worker did not become ready within 5 seconds."}))}async function X(s){if(await h(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=T(),E=a.formatTool(e,n);if(a.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let _=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r}),signal:AbortSignal.timeout(g.DEFAULT)});if(!_.ok)throw new Error(`Observation storage failed: ${_.status}`);a.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(D)}var C="";N.on("data",s=>C+=s);N.on("end",async()=>{let s;try{s=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await X(s)});
|
||||
${E}`),E}var F=p.join(W(),".claude","plugins","marketplaces","thedotmack"),I=R(u.HEALTH_CHECK),S=null;function T(){if(S!==null)return S;let s=p.join(c.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=c.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function x(){let s=T();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function K(){let s=p.join(F,"package.json");return JSON.parse(b(s,"utf-8")).version}async function G(){let s=T(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let s=K(),t=await G();s!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function N(){for(let r=0;r<25;r++){try{if(await x()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(d({port:T(),customPrefix:"Worker did not become ready within 5 seconds."}))}async function X(s){if(await N(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=T(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let a=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r}),signal:AbortSignal.timeout(u.DEFAULT)});if(!a.ok)throw new Error(`Observation storage failed: ${a.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(D)}var A="";h.on("data",s=>A+=s);h.on("end",async()=>{let s;try{s=A?JSON.parse(A):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await X(s)});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
import{stdin as N}from"process";var T=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as w,writeFileSync as v,existsSync as H}from"fs";import{join as P}from"path";import{homedir as W}from"os";var d="bugfix,feature,refactor,discovery,decision,change",I="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_DATA_DIR:P(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:d,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:I,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!H(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),M=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=g.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let a=this.formatTimestamp(new Date),i=O[t].padEnd(5),_=r.padEnd(6),E="";n?.correlationId?E=`[${n.correlationId}] `:n?.sessionId&&(E=`[session-${n.sessionId}] `);let l="";o!=null&&(this.getLevel()===0&&typeof o=="object"?l=`
|
||||
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let S="";if(n){let{sessionId:J,sdkSessionId:q,correlationId:z,...D}=n;Object.keys(D).length>0&&(S=` {${Object.entries(D).map(([$,k])=>`${$}=${k}`).join(", ")}}`)}let C=`[${a}] [${i}] [${_}] ${E}${e}${S}${l}`;t===3?console.error(C):console.log(C)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let E=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=E?`${E[1].split("/").pop()}:${E[2]}`:"unknown",S={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,S,n),o}},c=new M;import m from"path";import{homedir as x}from"os";import{readFileSync as b}from"fs";var u={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(s){return process.platform==="win32"?Math.round(s*u.WINDOWS_MULTIPLIER):s}function R(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",i=`${o}${a}
|
||||
import{stdin as y}from"process";var p=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as P,writeFileSync as w,existsSync as v}from"fs";import{join as H}from"path";import{homedir as W}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:H(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!v(t))return this.getAllDefaults();let r=P(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{w(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),M=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=g.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=O[t].padEnd(5),_=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(this.getLevel()===0&&typeof o=="object"?l=`
|
||||
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let f="";if(n){let{sessionId:J,sdkSessionId:q,correlationId:z,...D}=n;Object.keys(D).length>0&&(f=` {${Object.entries(D).map(([$,k])=>`${$}=${k}`).join(", ")}}`)}let C=`[${E}] [${i}] [${_}] ${a}${e}${f}${l}`;t===3?console.error(C):console.log(C)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",f={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,f,n),o}},c=new M;import A from"path";import{homedir as x}from"os";import{readFileSync as b}from"fs";var u={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(s){return process.platform==="win32"?Math.round(s*u.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
|
||||
|
||||
`;return i+=`To restart the worker:
|
||||
`,i+=`1. Exit Claude Code completely
|
||||
@@ -11,8 +11,8 @@ ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Obje
|
||||
|
||||
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
|
||||
|
||||
${i}`),i}var F=m.join(x(),".claude","plugins","marketplaces","thedotmack"),U=h(u.HEALTH_CHECK),p=null;function f(){if(p!==null)return p;let s=m.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return p=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),p}async function K(){let s=f();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(U)})).ok}function G(){let s=m.join(F,"package.json");return JSON.parse(b(s,"utf-8")).version}async function V(){let s=f(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(U)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function X(){let s=G(),t=await V();s!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function y(){for(let r=0;r<25;r++){try{if(await K()){await X();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(R({port:f(),customPrefix:"Worker did not become ready within 5 seconds."}))}import{readFileSync as j,existsSync as B}from"fs";function A(s,t,r=!1){if(!s||!B(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=j(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(`
|
||||
`),o=!1;for(let a=n.length-1;a>=0;a--){let i=JSON.parse(n[a]);if(i.type===t&&(o=!0,i.message?.content)){let _="",E=i.message.content;if(typeof E=="string")_=E;else if(Array.isArray(E))_=E.filter(l=>l.type==="text").map(l=>l.text).join(`
|
||||
`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof E}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,`
|
||||
${i}`),i}var F=A.join(x(),".claude","plugins","marketplaces","thedotmack"),h=d(u.HEALTH_CHECK),T=null;function S(){if(T!==null)return T;let s=A.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(s);return T=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),T}async function K(){let s=S();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(h)})).ok}function G(){let s=A.join(F,"package.json");return JSON.parse(b(s,"utf-8")).version}async function V(){let s=S(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(h)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function X(){let s=G(),t=await V();s!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function N(){for(let r=0;r<25;r++){try{if(await K()){await X();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:S(),customPrefix:"Worker did not become ready within 5 seconds."}))}import{readFileSync as j,existsSync as B}from"fs";function L(s,t,r=!1){if(!s||!B(s))throw new Error(`Transcript path missing or file does not exist: ${s}`);let e=j(s,"utf-8").trim();if(!e)throw new Error(`Transcript file exists but is empty: ${s}`);let n=e.split(`
|
||||
`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(`
|
||||
`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,`
|
||||
|
||||
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Y(s){if(await y(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=f();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=A(s.transcript_path,"user"),n=A(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastUserMessage:!!e,hasLastAssistantMessage:!!n});let o=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:e,last_assistant_message:n}),signal:AbortSignal.timeout(u.DEFAULT)});if(!o.ok)throw console.log(T),new Error(`Summary generation failed: ${o.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(T)}var L="";N.on("data",s=>L+=s);N.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Y(s)});
|
||||
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Y(s){if(await N(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=S();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=L(s.transcript_path,"user"),n=L(s.transcript_path,"assistant",!0);c.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastUserMessage:!!e,hasLastAssistantMessage:!!n});let o=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:e,last_assistant_message:n}),signal:AbortSignal.timeout(u.DEFAULT)});if(!o.ok)throw console.log(p),new Error(`Summary generation failed: ${o.status}`);c.debug("HOOK","Summary request sent successfully"),console.log(p)}var m="";y.on("data",s=>m+=s);y.on("end",async()=>{let s;try{s=m?JSON.parse(m):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Y(s)});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
import{basename as X}from"path";import M from"path";import{homedir as b}from"os";import{readFileSync as H}from"fs";import{readFileSync as k,writeFileSync as v,existsSync as W}from"fs";import{join as P}from"path";import{homedir as w}from"os";var D="bugfix,feature,refactor,discovery,decision,change",m="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var _=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_DATA_DIR:P(w(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:m,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!W(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{v(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var p=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),f=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=_.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=p[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),u=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${u}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=p[t].padEnd(5),u=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(this.getLevel()===0&&typeof o=="object"?l=`
|
||||
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let T="";if(n){let{sessionId:Y,sdkSessionId:J,correlationId:q,...A}=n;Object.keys(A).length>0&&(T=` {${Object.entries(A).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let L=`[${E}] [${i}] [${u}] ${a}${e}${T}${l}`;t===3?console.error(L):console.log(L)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",T={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,T,n),o}},c=new f;var O={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},I={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function U(s){return process.platform==="win32"?Math.round(s*O.WINDOWS_MULTIPLIER):s}function R(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
|
||||
import{basename as X}from"path";import f from"path";import{homedir as b}from"os";import{readFileSync as H}from"fs";import{readFileSync as k,writeFileSync as P,existsSync as v}from"fs";import{join as W}from"path";import{homedir as w}from"os";var D="bugfix,feature,refactor,discovery,decision,change",m="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var a=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"anthropic/claude-3.5-sonnet",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_DATA_DIR:W(w(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:D,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:m,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!v(t))return this.getAllDefaults();let r=k(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{P(t,JSON.stringify(n,null,2),"utf-8"),c.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){c.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return c.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),S=class{level=null;useColor;constructor(){this.useColor=process.stdout.isTTY??!1}getLevel(){if(this.level===null){let t=a.get("CLAUDE_MEM_LOG_LEVEL").toUpperCase();this.level=O[t]??1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
|
||||
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),u=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${u}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=O[t].padEnd(5),u=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let l="";o!=null&&(this.getLevel()===0&&typeof o=="object"?l=`
|
||||
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let T="";if(n){let{sessionId:B,sdkSessionId:J,correlationId:q,...A}=n;Object.keys(A).length>0&&(T=` {${Object.entries(A).map(([y,$])=>`${y}=${$}`).join(", ")}}`)}let L=`[${E}] [${i}] [${u}] ${_}${e}${T}${l}`;t===3?console.error(L):console.log(L)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",T={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,T,n),o}},c=new S;var p={DEFAULT:12e4,HEALTH_CHECK:1e3,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:15,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},U={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function R(s){return process.platform==="win32"?Math.round(s*p.WINDOWS_MULTIPLIER):s}function I(s={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=s,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",i=`${o}${E}
|
||||
|
||||
`;return i+=`To restart the worker:
|
||||
`,i+=`1. Exit Claude Code completely
|
||||
@@ -11,15 +11,15 @@ ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Obje
|
||||
|
||||
If that doesn't work, try: /troubleshoot`),n&&(i=`Worker Error: ${n}
|
||||
|
||||
${i}`),i}var x=M.join(b(),".claude","plugins","marketplaces","thedotmack"),d=U(O.HEALTH_CHECK),S=null;function g(){if(S!==null)return S;let s=M.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function F(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(d)})).ok}function G(){let s=M.join(x,"package.json");return JSON.parse(H(s,"utf-8")).version}async function K(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(d)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let s=G(),t=await K();s!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function h(){for(let r=0;r<25;r++){try{if(await F()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(R({port:g(),customPrefix:"Worker did not become ready within 5 seconds."}))}await h();var N=g(),j=X(process.cwd()),C=await fetch(`http://127.0.0.1:${N}/api/context/inject?project=${encodeURIComponent(j)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!C.ok)throw new Error(`Failed to fetch context: ${C.status}`);var B=await C.text();console.error(`
|
||||
${i}`),i}var x=f.join(b(),".claude","plugins","marketplaces","thedotmack"),d=R(p.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let s=f.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function F(){let s=g();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(d)})).ok}function G(){let s=f.join(x,"package.json");return JSON.parse(H(s,"utf-8")).version}async function K(){let s=g(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(d)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function V(){let s=G(),t=await K();s!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function h(){for(let r=0;r<25;r++){try{if(await F()){await V();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(I({port:g(),customPrefix:"Worker did not become ready within 5 seconds."}))}await h();var N=g(),j=X(process.cwd()),C=await fetch(`http://127.0.0.1:${N}/api/context/inject?project=${encodeURIComponent(j)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!C.ok)throw new Error(`Failed to fetch context: ${C.status}`);var Y=await C.text();console.error(`
|
||||
|
||||
\u{1F4DD} Claude-Mem Context Loaded
|
||||
\u2139\uFE0F Note: This appears as stderr but is informational only
|
||||
|
||||
`+B+`
|
||||
`+Y+`
|
||||
|
||||
\u{1F4A1} New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.
|
||||
|
||||
\u{1F4AC} Community https://discord.gg/J4wttp9vDu
|
||||
\u{1F4FA} Watch live in browser http://localhost:${N}/
|
||||
`);process.exit(I.USER_MESSAGE_ONLY);
|
||||
`);process.exit(U.USER_MESSAGE_ONLY);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+187
-161
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -25,6 +25,7 @@ import { SessionManager } from './worker/SessionManager.js';
|
||||
import { SSEBroadcaster } from './worker/SSEBroadcaster.js';
|
||||
import { SDKAgent } from './worker/SDKAgent.js';
|
||||
import { GeminiAgent } from './worker/GeminiAgent.js';
|
||||
import { OpenRouterAgent } from './worker/OpenRouterAgent.js';
|
||||
import { PaginationHelper } from './worker/PaginationHelper.js';
|
||||
import { SettingsManager } from './worker/SettingsManager.js';
|
||||
import { SearchManager } from './worker/SearchManager.js';
|
||||
@@ -56,6 +57,7 @@ export class WorkerService {
|
||||
private sseBroadcaster: SSEBroadcaster;
|
||||
private sdkAgent: SDKAgent;
|
||||
private geminiAgent: GeminiAgent;
|
||||
private openRouterAgent: OpenRouterAgent;
|
||||
private paginationHelper: PaginationHelper;
|
||||
private settingsManager: SettingsManager;
|
||||
private sessionEventBroadcaster: SessionEventBroadcaster;
|
||||
@@ -86,6 +88,8 @@ export class WorkerService {
|
||||
this.sdkAgent = new SDKAgent(this.dbManager, this.sessionManager);
|
||||
this.geminiAgent = new GeminiAgent(this.dbManager, this.sessionManager);
|
||||
this.geminiAgent.setFallbackAgent(this.sdkAgent); // Enable fallback to Claude on Gemini API failure
|
||||
this.openRouterAgent = new OpenRouterAgent(this.dbManager, this.sessionManager);
|
||||
this.openRouterAgent.setFallbackAgent(this.sdkAgent); // Enable fallback to Claude on OpenRouter API failure
|
||||
this.paginationHelper = new PaginationHelper(this.dbManager);
|
||||
this.settingsManager = new SettingsManager(this.dbManager);
|
||||
this.sessionEventBroadcaster = new SessionEventBroadcaster(this.sseBroadcaster, this);
|
||||
@@ -103,7 +107,7 @@ export class WorkerService {
|
||||
|
||||
// Initialize route handlers (SearchRoutes will use MCP client initially, then switch to SearchManager after DB init)
|
||||
this.viewerRoutes = new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager);
|
||||
this.sessionRoutes = new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.geminiAgent, this.sessionEventBroadcaster, this);
|
||||
this.sessionRoutes = new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.geminiAgent, this.openRouterAgent, this.sessionEventBroadcaster, this);
|
||||
this.dataRoutes = new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime);
|
||||
// SearchRoutes needs SearchManager which requires initialized DB - will be created in initializeBackground()
|
||||
this.searchRoutes = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface ActiveSession {
|
||||
pendingProcessingIds: Set<number>; // Track ALL message IDs yielded but not yet processed
|
||||
earliestPendingTimestamp: number | null; // Original timestamp of earliest pending message (for accurate observation timestamps)
|
||||
conversationHistory: ConversationMessage[]; // Shared conversation history for provider switching
|
||||
currentProvider: 'claude' | 'gemini' | null; // Track which provider is currently running
|
||||
currentProvider: 'claude' | 'gemini' | 'openrouter' | null; // Track which provider is currently running
|
||||
}
|
||||
|
||||
export interface PendingMessage {
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* OpenRouterAgent: OpenRouter-based observation extraction
|
||||
*
|
||||
* Alternative to SDKAgent that uses OpenRouter's unified API
|
||||
* for accessing 100+ models from different providers.
|
||||
*
|
||||
* Responsibility:
|
||||
* - Call OpenRouter REST API for observation extraction
|
||||
* - Parse XML responses (same format as Claude/Gemini)
|
||||
* - Sync to database and Chroma
|
||||
* - Support dynamic model selection across providers
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { DatabaseManager } from './DatabaseManager.js';
|
||||
import { SessionManager } from './SessionManager.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { parseObservations, parseSummary } from '../../sdk/parser.js';
|
||||
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
|
||||
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
|
||||
import { ModeManager } from '../domain/ModeManager.js';
|
||||
|
||||
// OpenRouter API endpoint
|
||||
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
|
||||
// OpenAI-compatible message format
|
||||
interface OpenAIMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface OpenRouterResponse {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
role?: string;
|
||||
content?: string;
|
||||
};
|
||||
finish_reason?: string;
|
||||
}>;
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
total_tokens?: number;
|
||||
};
|
||||
error?: {
|
||||
message?: string;
|
||||
code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Forward declaration for fallback agent type
|
||||
type FallbackAgent = {
|
||||
startSession(session: ActiveSession, worker?: any): Promise<void>;
|
||||
};
|
||||
|
||||
export class OpenRouterAgent {
|
||||
private dbManager: DatabaseManager;
|
||||
private sessionManager: SessionManager;
|
||||
private fallbackAgent: FallbackAgent | null = null;
|
||||
|
||||
constructor(dbManager: DatabaseManager, sessionManager: SessionManager) {
|
||||
this.dbManager = dbManager;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fallback agent (Claude SDK) for when OpenRouter API fails
|
||||
* Must be set after construction to avoid circular dependency
|
||||
*/
|
||||
setFallbackAgent(agent: FallbackAgent): void {
|
||||
this.fallbackAgent = agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error should trigger fallback to Claude
|
||||
*/
|
||||
private shouldFallbackToClaude(error: any): boolean {
|
||||
const message = error?.message || '';
|
||||
// Fall back on rate limit (429), server errors (5xx), or network issues
|
||||
return (
|
||||
message.includes('429') ||
|
||||
message.includes('500') ||
|
||||
message.includes('502') ||
|
||||
message.includes('503') ||
|
||||
message.includes('ECONNREFUSED') ||
|
||||
message.includes('ETIMEDOUT') ||
|
||||
message.includes('fetch failed')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OpenRouter agent for a session
|
||||
* Uses multi-turn conversation to maintain context across messages
|
||||
*/
|
||||
async startSession(session: ActiveSession, worker?: any): Promise<void> {
|
||||
try {
|
||||
// Get OpenRouter configuration
|
||||
const { apiKey, model, siteUrl, appName } = this.getOpenRouterConfig();
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.');
|
||||
}
|
||||
|
||||
// Load active mode
|
||||
const mode = ModeManager.getInstance().getActiveMode();
|
||||
|
||||
// Build initial prompt
|
||||
const initPrompt = session.lastPromptNumber === 1
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
|
||||
// Add to conversation history and query OpenRouter with full context
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
const initResponse = await this.queryOpenRouterMultiTurn(session.conversationHistory, apiKey, model, siteUrl, appName);
|
||||
|
||||
if (initResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: initResponse.content });
|
||||
|
||||
// Track token usage
|
||||
const tokensUsed = initResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7); // Rough estimate
|
||||
session.cumulativeOutputTokens += Math.floor(tokensUsed * 0.3);
|
||||
|
||||
// Process response (no original timestamp for init - not from queue)
|
||||
await this.processOpenRouterResponse(session, initResponse.content, worker, tokensUsed, null);
|
||||
}
|
||||
|
||||
// Process pending messages
|
||||
for await (const message of this.sessionManager.getMessageIterator(session.sessionDbId)) {
|
||||
// Capture earliest timestamp BEFORE processing (will be cleared after)
|
||||
const originalTimestamp = session.earliestPendingTimestamp;
|
||||
|
||||
if (message.type === 'observation') {
|
||||
// Update last prompt number
|
||||
if (message.prompt_number !== undefined) {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
}
|
||||
|
||||
// Build observation prompt
|
||||
const obsPrompt = buildObservationPrompt({
|
||||
id: 0,
|
||||
tool_name: message.tool_name!,
|
||||
tool_input: JSON.stringify(message.tool_input),
|
||||
tool_output: JSON.stringify(message.tool_response),
|
||||
created_at_epoch: originalTimestamp ?? Date.now(),
|
||||
cwd: message.cwd
|
||||
});
|
||||
|
||||
// Add to conversation history and query OpenRouter with full context
|
||||
session.conversationHistory.push({ role: 'user', content: obsPrompt });
|
||||
const obsResponse = await this.queryOpenRouterMultiTurn(session.conversationHistory, apiKey, model, siteUrl, appName);
|
||||
|
||||
if (obsResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: obsResponse.content });
|
||||
|
||||
const tokensUsed = obsResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7);
|
||||
session.cumulativeOutputTokens += Math.floor(tokensUsed * 0.3);
|
||||
await this.processOpenRouterResponse(session, obsResponse.content, worker, tokensUsed, originalTimestamp);
|
||||
} else {
|
||||
// Empty response - still mark messages as processed to avoid stuck state
|
||||
logger.warn('SDK', 'Empty OpenRouter response for observation, marking as processed', {
|
||||
sessionId: session.sessionDbId,
|
||||
toolName: message.tool_name
|
||||
});
|
||||
await this.markMessagesProcessed(session, worker);
|
||||
}
|
||||
|
||||
} else if (message.type === 'summarize') {
|
||||
// Build summary prompt
|
||||
const summaryPrompt = buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
// Add to conversation history and query OpenRouter with full context
|
||||
session.conversationHistory.push({ role: 'user', content: summaryPrompt });
|
||||
const summaryResponse = await this.queryOpenRouterMultiTurn(session.conversationHistory, apiKey, model, siteUrl, appName);
|
||||
|
||||
if (summaryResponse.content) {
|
||||
// Add response to conversation history
|
||||
session.conversationHistory.push({ role: 'assistant', content: summaryResponse.content });
|
||||
|
||||
const tokensUsed = summaryResponse.tokensUsed || 0;
|
||||
session.cumulativeInputTokens += Math.floor(tokensUsed * 0.7);
|
||||
session.cumulativeOutputTokens += Math.floor(tokensUsed * 0.3);
|
||||
await this.processOpenRouterResponse(session, summaryResponse.content, worker, tokensUsed, originalTimestamp);
|
||||
} else {
|
||||
// Empty response - still mark messages as processed to avoid stuck state
|
||||
logger.warn('SDK', 'Empty OpenRouter response for summary, marking as processed', {
|
||||
sessionId: session.sessionDbId
|
||||
});
|
||||
await this.markMessagesProcessed(session, worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark session complete
|
||||
const sessionDuration = Date.now() - session.startTime;
|
||||
logger.success('SDK', 'OpenRouter agent completed', {
|
||||
sessionId: session.sessionDbId,
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`,
|
||||
historyLength: session.conversationHistory.length,
|
||||
model
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
logger.warn('SDK', 'OpenRouter agent aborted', { sessionId: session.sessionDbId });
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check if we should fall back to Claude
|
||||
if (this.shouldFallbackToClaude(error) && this.fallbackAgent) {
|
||||
logger.warn('SDK', 'OpenRouter API failed, falling back to Claude SDK', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
error: error.message,
|
||||
historyLength: session.conversationHistory.length
|
||||
});
|
||||
|
||||
// Reset any 'processing' messages back to 'pending' so Claude can retry them
|
||||
const pendingStore = this.sessionManager.getPendingMessageStore();
|
||||
const resetCount = pendingStore.resetStuckMessages(0); // 0 = reset ALL processing messages
|
||||
if (resetCount > 0) {
|
||||
logger.info('SDK', 'Reset processing messages for fallback', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
resetCount
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to Claude - it will use the same session with shared conversationHistory
|
||||
return this.fallbackAgent.startSession(session, worker);
|
||||
}
|
||||
|
||||
logger.failure('SDK', 'OpenRouter agent error', { sessionDbId: session.sessionDbId }, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert shared ConversationMessage array to OpenAI-compatible message format
|
||||
*/
|
||||
private conversationToOpenAIMessages(history: ConversationMessage[]): OpenAIMessage[] {
|
||||
return history.map(msg => ({
|
||||
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
||||
content: msg.content
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query OpenRouter via REST API with full conversation history (multi-turn)
|
||||
* Sends the entire conversation context for coherent responses
|
||||
*/
|
||||
private async queryOpenRouterMultiTurn(
|
||||
history: ConversationMessage[],
|
||||
apiKey: string,
|
||||
model: string,
|
||||
siteUrl?: string,
|
||||
appName?: string
|
||||
): Promise<{ content: string; tokensUsed?: number }> {
|
||||
const messages = this.conversationToOpenAIMessages(history);
|
||||
const totalChars = history.reduce((sum, m) => sum + m.content.length, 0);
|
||||
|
||||
logger.debug('SDK', `Querying OpenRouter multi-turn (${model})`, {
|
||||
turns: history.length,
|
||||
totalChars
|
||||
});
|
||||
|
||||
const response = await fetch(OPENROUTER_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'HTTP-Referer': siteUrl || 'https://github.com/thedotmack/claude-mem',
|
||||
'X-Title': appName || 'claude-mem',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages,
|
||||
temperature: 0.3, // Lower temperature for structured extraction
|
||||
max_tokens: 4096,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as OpenRouterResponse;
|
||||
|
||||
// Check for API error in response body
|
||||
if (data.error) {
|
||||
throw new Error(`OpenRouter API error: ${data.error.code} - ${data.error.message}`);
|
||||
}
|
||||
|
||||
if (!data.choices?.[0]?.message?.content) {
|
||||
logger.warn('SDK', 'Empty response from OpenRouter');
|
||||
return { content: '' };
|
||||
}
|
||||
|
||||
const content = data.choices[0].message.content;
|
||||
const tokensUsed = data.usage?.total_tokens;
|
||||
|
||||
return { content, tokensUsed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process OpenRouter response (same format as Claude/Gemini)
|
||||
* @param originalTimestamp - Original epoch when message was queued (for backlog processing accuracy)
|
||||
*/
|
||||
private async processOpenRouterResponse(
|
||||
session: ActiveSession,
|
||||
text: string,
|
||||
worker: any | undefined,
|
||||
discoveryTokens: number,
|
||||
originalTimestamp: number | null
|
||||
): Promise<void> {
|
||||
// Parse observations (same XML format)
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
|
||||
// Store observations with original timestamp (if processing backlog) or current time
|
||||
for (const obs of observations) {
|
||||
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
discoveryTokens,
|
||||
originalTimestamp ?? undefined
|
||||
);
|
||||
|
||||
logger.info('SDK', 'OpenRouter observation saved', {
|
||||
sessionId: session.sessionDbId,
|
||||
obsId,
|
||||
type: obs.type,
|
||||
title: obs.title || '(untitled)'
|
||||
});
|
||||
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch,
|
||||
discoveryTokens
|
||||
).catch(err => {
|
||||
logger.warn('SDK', 'OpenRouter chroma sync failed', { obsId }, err);
|
||||
});
|
||||
|
||||
// Broadcast to SSE clients
|
||||
if (worker && worker.sseBroadcaster) {
|
||||
worker.sseBroadcaster.broadcast({
|
||||
type: 'new_observation',
|
||||
observation: {
|
||||
id: obsId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
session_id: session.claudeSessionId,
|
||||
type: obs.type,
|
||||
title: obs.title,
|
||||
subtitle: obs.subtitle,
|
||||
text: null,
|
||||
narrative: obs.narrative || null,
|
||||
facts: JSON.stringify(obs.facts || []),
|
||||
concepts: JSON.stringify(obs.concepts || []),
|
||||
files_read: JSON.stringify(obs.files_read || []),
|
||||
files_modified: JSON.stringify(obs.files_modified || []),
|
||||
project: session.project,
|
||||
prompt_number: session.lastPromptNumber,
|
||||
created_at_epoch: createdAtEpoch
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parse summary
|
||||
const summary = parseSummary(text, session.sessionDbId);
|
||||
|
||||
if (summary) {
|
||||
// Convert nullable fields to empty strings for storeSummary
|
||||
const summaryForStore = {
|
||||
request: summary.request || '',
|
||||
investigated: summary.investigated || '',
|
||||
learned: summary.learned || '',
|
||||
completed: summary.completed || '',
|
||||
next_steps: summary.next_steps || '',
|
||||
notes: summary.notes
|
||||
};
|
||||
|
||||
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
discoveryTokens,
|
||||
originalTimestamp ?? undefined
|
||||
);
|
||||
|
||||
logger.info('SDK', 'OpenRouter summary saved', {
|
||||
sessionId: session.sessionDbId,
|
||||
summaryId,
|
||||
request: summary.request || '(no request)'
|
||||
});
|
||||
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch,
|
||||
discoveryTokens
|
||||
).catch(err => {
|
||||
logger.warn('SDK', 'OpenRouter chroma sync failed', { summaryId }, err);
|
||||
});
|
||||
|
||||
// Broadcast to SSE clients
|
||||
if (worker && worker.sseBroadcaster) {
|
||||
worker.sseBroadcaster.broadcast({
|
||||
type: 'new_summary',
|
||||
summary: {
|
||||
id: summaryId,
|
||||
session_id: session.claudeSessionId,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
completed: summary.completed,
|
||||
next_steps: summary.next_steps,
|
||||
notes: summary.notes,
|
||||
project: session.project,
|
||||
prompt_number: session.lastPromptNumber,
|
||||
created_at_epoch: createdAtEpoch
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mark messages as processed
|
||||
await this.markMessagesProcessed(session, worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark pending messages as processed
|
||||
*/
|
||||
private async markMessagesProcessed(session: ActiveSession, worker: any | undefined): Promise<void> {
|
||||
const pendingMessageStore = this.sessionManager.getPendingMessageStore();
|
||||
if (session.pendingProcessingIds.size > 0) {
|
||||
for (const messageId of session.pendingProcessingIds) {
|
||||
pendingMessageStore.markProcessed(messageId);
|
||||
}
|
||||
logger.debug('SDK', 'OpenRouter messages marked as processed', {
|
||||
sessionId: session.sessionDbId,
|
||||
count: session.pendingProcessingIds.size
|
||||
});
|
||||
session.pendingProcessingIds.clear();
|
||||
|
||||
const deletedCount = pendingMessageStore.cleanupProcessed(100);
|
||||
if (deletedCount > 0) {
|
||||
logger.debug('SDK', 'OpenRouter cleaned up old processed messages', { deletedCount });
|
||||
}
|
||||
}
|
||||
|
||||
if (worker && typeof worker.broadcastProcessingStatus === 'function') {
|
||||
worker.broadcastProcessingStatus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenRouter configuration from settings or environment
|
||||
*/
|
||||
private getOpenRouterConfig(): { apiKey: string; model: string; siteUrl?: string; appName?: string } {
|
||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||
|
||||
// API key: check settings first, then environment variable
|
||||
const apiKey = settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY || '';
|
||||
|
||||
// Model: from settings or default
|
||||
const model = settings.CLAUDE_MEM_OPENROUTER_MODEL || 'anthropic/claude-3.5-sonnet';
|
||||
|
||||
// Optional analytics headers
|
||||
const siteUrl = settings.CLAUDE_MEM_OPENROUTER_SITE_URL || '';
|
||||
const appName = settings.CLAUDE_MEM_OPENROUTER_APP_NAME || 'claude-mem';
|
||||
|
||||
return { apiKey, model, siteUrl, appName };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenRouter is available (has API key configured)
|
||||
*/
|
||||
export function isOpenRouterAvailable(): boolean {
|
||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenRouter is the selected provider
|
||||
*/
|
||||
export function isOpenRouterSelected(): boolean {
|
||||
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
|
||||
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
|
||||
return settings.CLAUDE_MEM_PROVIDER === 'openrouter';
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { SessionManager } from '../../SessionManager.js';
|
||||
import { DatabaseManager } from '../../DatabaseManager.js';
|
||||
import { SDKAgent } from '../../SDKAgent.js';
|
||||
import { GeminiAgent, isGeminiSelected, isGeminiAvailable } from '../../GeminiAgent.js';
|
||||
import { OpenRouterAgent, isOpenRouterSelected, isOpenRouterAvailable } from '../../OpenRouterAgent.js';
|
||||
import type { WorkerService } from '../../../worker-service.js';
|
||||
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||
import { SessionEventBroadcaster } from '../../events/SessionEventBroadcaster.js';
|
||||
@@ -29,6 +30,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
private dbManager: DatabaseManager,
|
||||
private sdkAgent: SDKAgent,
|
||||
private geminiAgent: GeminiAgent,
|
||||
private openRouterAgent: OpenRouterAgent,
|
||||
private eventBroadcaster: SessionEventBroadcaster,
|
||||
private workerService: WorkerService
|
||||
) {
|
||||
@@ -41,12 +43,20 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
|
||||
/**
|
||||
* Get the appropriate agent based on settings
|
||||
* Throws error if Gemini is selected but not configured (no silent fallback)
|
||||
* Throws error if provider is selected but not configured (no silent fallback)
|
||||
*
|
||||
* Note: Session linking via claudeSessionId allows provider switching mid-session.
|
||||
* The conversationHistory on ActiveSession maintains context across providers.
|
||||
*/
|
||||
private getActiveAgent(): SDKAgent | GeminiAgent {
|
||||
private getActiveAgent(): SDKAgent | GeminiAgent | OpenRouterAgent {
|
||||
if (isOpenRouterSelected()) {
|
||||
if (isOpenRouterAvailable()) {
|
||||
logger.debug('SESSION', 'Using OpenRouter agent');
|
||||
return this.openRouterAgent;
|
||||
} else {
|
||||
throw new Error('OpenRouter provider selected but no API key configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.');
|
||||
}
|
||||
}
|
||||
if (isGeminiSelected()) {
|
||||
if (isGeminiAvailable()) {
|
||||
logger.debug('SESSION', 'Using Gemini agent');
|
||||
@@ -61,7 +71,10 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
/**
|
||||
* Get the currently selected provider name
|
||||
*/
|
||||
private getSelectedProvider(): 'claude' | 'gemini' {
|
||||
private getSelectedProvider(): 'claude' | 'gemini' | 'openrouter' {
|
||||
if (isOpenRouterSelected() && isOpenRouterAvailable()) {
|
||||
return 'openrouter';
|
||||
}
|
||||
return (isGeminiSelected() && isGeminiAvailable()) ? 'gemini' : 'claude';
|
||||
}
|
||||
|
||||
@@ -104,13 +117,13 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
*/
|
||||
private startGeneratorWithProvider(
|
||||
session: ReturnType<typeof this.sessionManager.getSession>,
|
||||
provider: 'claude' | 'gemini',
|
||||
provider: 'claude' | 'gemini' | 'openrouter',
|
||||
source: string
|
||||
): void {
|
||||
if (!session) return;
|
||||
|
||||
const agent = provider === 'gemini' ? this.geminiAgent : this.sdkAgent;
|
||||
const agentName = provider === 'gemini' ? 'Gemini' : 'Claude SDK';
|
||||
const agent = provider === 'openrouter' ? this.openRouterAgent : (provider === 'gemini' ? this.geminiAgent : this.sdkAgent);
|
||||
const agentName = provider === 'openrouter' ? 'OpenRouter' : (provider === 'gemini' ? 'Gemini' : 'Claude SDK');
|
||||
|
||||
logger.info('SESSION', `Generator auto-starting (${source}) using ${agentName}`, {
|
||||
sessionId: session.sessionDbId,
|
||||
|
||||
@@ -216,9 +216,9 @@ export class SettingsRoutes extends BaseRouteHandler {
|
||||
private validateSettings(settings: any): { valid: boolean; error?: string } {
|
||||
// Validate CLAUDE_MEM_PROVIDER
|
||||
if (settings.CLAUDE_MEM_PROVIDER) {
|
||||
const validProviders = ['claude', 'gemini'];
|
||||
if (!validProviders.includes(settings.CLAUDE_MEM_PROVIDER)) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_PROVIDER must be "claude" or "gemini"' };
|
||||
const validProviders = ['claude', 'gemini', 'openrouter'];
|
||||
if (!validProviders.includes(settings.CLAUDE_MEM_PROVIDER)) {
|
||||
return { valid: false, error: 'CLAUDE_MEM_PROVIDER must be "claude", "gemini", or "openrouter"' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ export interface SettingsDefaults {
|
||||
CLAUDE_MEM_WORKER_HOST: string;
|
||||
CLAUDE_MEM_SKIP_TOOLS: string;
|
||||
// AI Provider Configuration
|
||||
CLAUDE_MEM_PROVIDER: string; // 'claude' | 'gemini'
|
||||
CLAUDE_MEM_PROVIDER: string; // 'claude' | 'gemini' | 'openrouter'
|
||||
CLAUDE_MEM_GEMINI_API_KEY: string;
|
||||
CLAUDE_MEM_GEMINI_MODEL: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash'
|
||||
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: string; // 'true' | 'false' - enable rate limiting for free tier
|
||||
CLAUDE_MEM_OPENROUTER_API_KEY: string;
|
||||
CLAUDE_MEM_OPENROUTER_MODEL: string;
|
||||
CLAUDE_MEM_OPENROUTER_SITE_URL: string;
|
||||
CLAUDE_MEM_OPENROUTER_APP_NAME: string;
|
||||
// System Configuration
|
||||
CLAUDE_MEM_DATA_DIR: string;
|
||||
CLAUDE_MEM_LOG_LEVEL: string;
|
||||
@@ -60,6 +64,10 @@ export class SettingsDefaultsManager {
|
||||
CLAUDE_MEM_GEMINI_API_KEY: '', // Empty by default, can be set via UI or env
|
||||
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite', // Default Gemini model (highest free tier RPM)
|
||||
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true', // Rate limiting ON by default for free tier users
|
||||
CLAUDE_MEM_OPENROUTER_API_KEY: '', // Empty by default, can be set via UI or env
|
||||
CLAUDE_MEM_OPENROUTER_MODEL: 'anthropic/claude-3.5-sonnet', // Default OpenRouter model
|
||||
CLAUDE_MEM_OPENROUTER_SITE_URL: '', // Optional: for OpenRouter analytics
|
||||
CLAUDE_MEM_OPENROUTER_APP_NAME: 'claude-mem', // App name for OpenRouter analytics
|
||||
// System Configuration
|
||||
CLAUDE_MEM_DATA_DIR: join(homedir(), '.claude-mem'),
|
||||
CLAUDE_MEM_LOG_LEVEL: 'INFO',
|
||||
|
||||
@@ -436,10 +436,11 @@ export function ContextSettingsModal({
|
||||
>
|
||||
<option value="claude">Claude (uses your Claude account)</option>
|
||||
<option value="gemini">Gemini (uses API key)</option>
|
||||
<option value="openrouter">OpenRouter (multi-model)</option>
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
{formState.CLAUDE_MEM_PROVIDER === 'claude' ? (
|
||||
{formState.CLAUDE_MEM_PROVIDER === 'claude' && (
|
||||
<FormField
|
||||
label="Claude Model"
|
||||
tooltip="Claude model used for generating observations"
|
||||
@@ -453,7 +454,9 @@ export function ContextSettingsModal({
|
||||
<option value="opus">opus (highest quality)</option>
|
||||
</select>
|
||||
</FormField>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{formState.CLAUDE_MEM_PROVIDER === 'gemini' && (
|
||||
<>
|
||||
<FormField
|
||||
label="Gemini API Key"
|
||||
@@ -491,6 +494,55 @@ export function ContextSettingsModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{formState.CLAUDE_MEM_PROVIDER === 'openrouter' && (
|
||||
<>
|
||||
<FormField
|
||||
label="OpenRouter API Key"
|
||||
tooltip="Your OpenRouter API key from openrouter.ai (or set OPENROUTER_API_KEY env var)"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={formState.CLAUDE_MEM_OPENROUTER_API_KEY || ''}
|
||||
onChange={(e) => updateSetting('CLAUDE_MEM_OPENROUTER_API_KEY', e.target.value)}
|
||||
placeholder="Enter OpenRouter API key..."
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="OpenRouter Model"
|
||||
tooltip="Model identifier from OpenRouter (e.g., anthropic/claude-3.5-sonnet, google/gemini-2.0-flash-thinking-exp)"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={formState.CLAUDE_MEM_OPENROUTER_MODEL || 'anthropic/claude-3.5-sonnet'}
|
||||
onChange={(e) => updateSetting('CLAUDE_MEM_OPENROUTER_MODEL', e.target.value)}
|
||||
placeholder="e.g., anthropic/claude-3.5-sonnet"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Site URL (Optional)"
|
||||
tooltip="Your site URL for OpenRouter analytics (optional)"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={formState.CLAUDE_MEM_OPENROUTER_SITE_URL || ''}
|
||||
onChange={(e) => updateSetting('CLAUDE_MEM_OPENROUTER_SITE_URL', e.target.value)}
|
||||
placeholder="https://yoursite.com"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="App Name (Optional)"
|
||||
tooltip="Your app name for OpenRouter analytics (optional)"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={formState.CLAUDE_MEM_OPENROUTER_APP_NAME || 'claude-mem'}
|
||||
onChange={(e) => updateSetting('CLAUDE_MEM_OPENROUTER_APP_NAME', e.target.value)}
|
||||
placeholder="claude-mem"
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
label="Worker Port"
|
||||
tooltip="Port for the background worker service"
|
||||
|
||||
@@ -12,6 +12,10 @@ export const DEFAULT_SETTINGS = {
|
||||
CLAUDE_MEM_PROVIDER: 'claude',
|
||||
CLAUDE_MEM_GEMINI_API_KEY: '',
|
||||
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite',
|
||||
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_GEMINI_RATE_LIMITING_ENABLED: 'true',
|
||||
|
||||
// Token Economics (all true for backwards compatibility)
|
||||
|
||||
@@ -61,10 +61,14 @@ export interface Settings {
|
||||
CLAUDE_MEM_WORKER_HOST: string;
|
||||
|
||||
// AI Provider Configuration
|
||||
CLAUDE_MEM_PROVIDER?: string; // 'claude' | 'gemini'
|
||||
CLAUDE_MEM_PROVIDER?: string; // 'claude' | 'gemini' | 'openrouter'
|
||||
CLAUDE_MEM_GEMINI_API_KEY?: string;
|
||||
CLAUDE_MEM_GEMINI_MODEL?: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash'
|
||||
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED?: string; // 'true' | 'false'
|
||||
CLAUDE_MEM_OPENROUTER_API_KEY?: string;
|
||||
CLAUDE_MEM_OPENROUTER_MODEL?: string;
|
||||
CLAUDE_MEM_OPENROUTER_SITE_URL?: string;
|
||||
CLAUDE_MEM_OPENROUTER_APP_NAME?: string;
|
||||
|
||||
// Token Economics Display
|
||||
CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS?: string;
|
||||
|
||||
Reference in New Issue
Block a user