claude-mem 12.1.2 → 12.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -44
- package/dist/npx-cli/index.js +125 -116
- package/openclaw/dist/index.js +14 -14
- package/openclaw/src/index.test.ts +0 -204
- package/openclaw/src/index.ts +3 -94
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +9 -9
- package/plugin/package.json +1 -1
- package/plugin/scripts/bun-runner.js +6 -14
- package/plugin/scripts/context-generator.cjs +48 -48
- package/plugin/scripts/mcp-server.cjs +13 -13
- package/plugin/scripts/smart-install.js +1 -54
- package/plugin/scripts/worker-service.cjs +212 -216
- package/plugin/skills/mem-search/SKILL.md +48 -0
- package/plugin/skills/smart-explore/SKILL.md +0 -45
package/openclaw/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
var
|
|
2
|
-
**${
|
|
3
|
-
${e.subtitle}`),c}var
|
|
1
|
+
var G="127.0.0.1",W=["\u{1F527}","\u{1F4D0}","\u{1F50D}","\u{1F4BB}","\u{1F9EA}","\u{1F41B}","\u{1F6E1}\uFE0F","\u2601\uFE0F","\u{1F4E6}","\u{1F3AF}","\u{1F52E}","\u26A1","\u{1F30A}","\u{1F3A8}","\u{1F4CA}","\u{1F680}","\u{1F52C}","\u{1F3D7}\uFE0F","\u{1F4DD}","\u{1F3AD}"];function z(e){let s=0;for(let i=0;i<e.length;i++)s=(s<<5)-s+e.charCodeAt(i)|0;return W[Math.abs(s)%W.length]}var X="\u{1F99E}",Z="\u2328\uFE0F",Y="Claude Code Session",Q="\u{1F980}";function V(e){let s=e?.primary??X,i=e?.claudeCode??Z,g=e?.claudeCodeLabel??Y,c=e?.default??Q,d=e?.agents??{};return function(f){if(!f)return c;if(f.startsWith("openclaw-")){let b=f.slice(9);return b?`${d[b]||z(b)} ${b}`:`${s} openclaw`}if(f==="openclaw")return`${s} openclaw`;let v=g.trim();return v?`${i} ${v} (${f})`:`${i} ${f}`}}var U=G;function M(e){return`http://${U}:${e}`}async function q(e,s,i,g){try{let c=await fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return c.ok?await c.json():(g.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`),null)}catch(c){let d=c instanceof Error?c.message:String(c);return g.warn(`[claude-mem] Worker POST ${s} failed: ${d}`),null}}function J(e,s,i,g){fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}).catch(c=>{let d=c instanceof Error?c.message:String(c);g.warn(`[claude-mem] Worker POST ${s} failed: ${d}`)})}async function K(e,s,i){try{let g=await fetch(`${M(e)}${s}`);return g.ok?await g.text():(i.warn(`[claude-mem] Worker GET ${s} returned ${g.status}`),null)}catch(g){let c=g instanceof Error?g.message:String(g);return i.warn(`[claude-mem] Worker GET ${s} failed: ${c}`),null}}async function B(e,s,i){let g=await K(e,s,i);if(!g)return null;try{return JSON.parse(g)}catch{return i.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function ee(e,s){let i=e.title||"Untitled",c=`${s(e.project)}
|
|
2
|
+
**${i}**`;return e.subtitle&&(c+=`
|
|
3
|
+
${e.subtitle}`),c}var ne={telegram:{namespace:"telegram",functionName:"sendMessageTelegram"},whatsapp:{namespace:"whatsapp",functionName:"sendMessageWhatsApp"},discord:{namespace:"discord",functionName:"sendMessageDiscord"},slack:{namespace:"slack",functionName:"sendMessageSlack"},signal:{namespace:"signal",functionName:"sendMessageSignal"},imessage:{namespace:"imessage",functionName:"sendMessageIMessage"},line:{namespace:"line",functionName:"sendMessageLine"}};async function te(e,s,i,g){try{let c=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:i,parse_mode:"Markdown"})});if(!c.ok){let d=await c.text();g.warn(`[claude-mem] Direct Telegram send failed (${c.status}): ${d}`)}}catch(c){let d=c instanceof Error?c.message:String(c);g.warn(`[claude-mem] Direct Telegram send error: ${d}`)}}function re(e,s,i,g,c){if(c&&s==="telegram")return te(c,i,g,e.logger);let d=ne[s];if(!d)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let w=e.runtime.channel[d.namespace];if(!w)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let f=w[d.functionName];return f?f(...s==="whatsapp"?[i,g,{verbose:!1}]:[i,g]).catch(b=>{let u=b instanceof Error?b.message:String(b);e.logger.error(`[claude-mem] Failed to send to ${s}: ${u}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${d.functionName} function`),Promise.resolve())}async function se(e,s,i,g,c,d,w,f){let v=1e3,b=3e4;for(;!c.signal.aborted;){try{d("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${M(s)}/stream`);let u=await fetch(`${M(s)}/stream`,{signal:c.signal,headers:{Accept:"text/event-stream"}});if(!u.ok)throw new Error(`SSE stream returned HTTP ${u.status}`);if(!u.body)throw new Error("SSE stream response has no body");d("connected"),v=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let _=u.body.getReader(),j=new TextDecoder,k="";for(;;){let{done:L,value:I}=await _.read();if(L)break;k+=j.decode(I,{stream:!0}),k.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),k="");let P=k.split(`
|
|
4
4
|
|
|
5
|
-
`);k=
|
|
6
|
-
`).filter(
|
|
7
|
-
`);if(
|
|
8
|
-
`));let p=1e3;
|
|
9
|
-
`));break}}await
|
|
10
|
-
`)}function
|
|
11
|
-
`)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),
|
|
12
|
-
`)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],
|
|
13
|
-
`)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),
|
|
14
|
-
`)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await
|
|
15
|
-
`)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${
|
|
5
|
+
`);k=P.pop()||"";for(let C of P){let x=C.split(`
|
|
6
|
+
`).filter(y=>y.startsWith("data:")).map(y=>y.slice(5).trim());if(x.length===0)continue;let T=x.join(`
|
|
7
|
+
`);if(T)try{let y=JSON.parse(T);if(y.type==="new_observation"&&y.observation){let A=ee(y.observation,w);await re(e,i,g,A,f)}}catch(y){let R=y instanceof Error?y.message:String(y);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${R}`)}}}}catch(u){if(c.signal.aborted)break;d("reconnecting");let _=u instanceof Error?u.message:String(u);e.logger.warn(`[claude-mem] SSE stream error: ${_}. Reconnecting in ${v/1e3}s`)}if(c.signal.aborted)break;await new Promise(u=>setTimeout(u,v)),v=Math.min(v*2,b)}d("disconnected")}function oe(e){let s=e.pluginConfig||{},i=s.workerPort||37777;U=s.workerHost||G;let g=s.project||"openclaw",c=V(s.observationFeed?.emojis);function d(r){return r.agentId?`openclaw-${r.agentId}`:g}let w=new Map,f=new Map,v=new Map,b=new Map,u=new Map,_=(()=>{let r=Number(s.completionDelayMs);return Number.isFinite(r)?Math.max(0,r):5e3})(),j=s.syncMemoryFile!==!1,k=new Set(s.syncMemoryFileExclude||[]);function L(r){let n=r||"default";return w.has(n)||w.set(n,`openclaw-${n}-${Date.now()}`),w.get(n)}function I(r){if(!j)return!1;let n=r?.agentId;return!(n&&k.has(n))}function P(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let o=typeof t=="string"?t.trim():"";o&&n.add(o)}return n.size===0&&n.add("default"),Array.from(n)}function C(r){let n=P(r),t=n.find(l=>f.has(l));t=t?f.get(t):n[0];let o=v.get(t);o||(o=new Set([t]),v.set(t,o));for(let l of n)o.add(l),f.set(l,t);let a=L(t);for(let l of o)w.set(l,a);return{canonicalKey:t,contentSessionId:a}}function x(r,n,t){let o=Date.now();for(let[m,p]of u)o-p>2e3&&u.delete(m);let a=`${r}::${n}::${t}`,l=u.get(a);return u.set(a,o),typeof l=="number"&&o-l<=2e3}function T(r){let n=P(r),t=n.map(a=>f.get(a)).find(Boolean)||n[0],o=v.get(t)||new Set([t,...n]);for(let a of o)f.delete(a),w.delete(a);v.delete(t),w.delete(t)}function y(r){let n=b.get(r);n&&clearTimeout(n);let t=setTimeout(()=>{b.delete(r),J(i,"/api/sessions/complete",{contentSessionId:r},e.logger)},_);b.set(r,t)}let R=6e4,A=new Map;async function H(r){let n=[g],t=r?d(r):null;t&&t!==g&&n.push(t);let o=n.join(","),a=A.get(o);if(a&&Date.now()-a.fetchedAt<R)return a.text;let l=await K(i,`/api/context/inject?projects=${encodeURIComponent(o)}`,e.logger);if(l&&l.trim().length>0){let m=l.trim();return A.set(o,{text:m,fetchedAt:Date.now()}),m}return null}e.on("session_start",async(r,n)=>{let{contentSessionId:t}=C(n);e.logger.info(`[claude-mem] Session tracking initialized: ${t}`)}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:o}=C(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${o} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{let{contentSessionId:t}=C(n);e.logger.info(`[claude-mem] Session preserved after compaction: ${t}`)}),e.on("before_agent_start",async(r,n)=>{let{contentSessionId:t}=C(n),o=d(n),a=r.prompt||"agent run";if(x(t,o,a)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${t} project=${o}`);return}await q(i,"/api/sessions/init",{contentSessionId:t,project:o,prompt:a},e.logger),e.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${t} project=${o}`)}),e.on("before_prompt_build",async(r,n)=>{if(!I(n))return;let t=await H(n);if(t)return e.logger.info(`[claude-mem] Context injected via system prompt for agent=${n.agentId??"unknown"}`),{appendSystemContext:t}}),e.on("tool_result_persist",(r,n)=>{e.logger.info(`[claude-mem] tool_result_persist fired: tool=${r.toolName??"unknown"} agent=${n.agentId??"none"} session=${n.sessionKey??"none"}`);let t=r.toolName;if(!t||t.startsWith("memory_"))return;let{canonicalKey:o,contentSessionId:a}=C(n),l="",m=r.message?.content;Array.isArray(m)&&(l=m.filter(h=>(h.type==="tool_result"||h.type==="text")&&"text"in h).map(h=>String(h.text)).join(`
|
|
8
|
+
`));let p=1e3;l.length>p&&(l=l.slice(0,p));let S=n.workspaceDir;if(!S){e.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${o} tool=${t}`);return}J(i,"/api/sessions/observations",{contentSessionId:a,tool_name:t,tool_input:r.params||{},tool_response:l,cwd:S},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=C(n),o="";if(Array.isArray(r.messages))for(let a=r.messages.length-1;a>=0;a--){let l=r.messages[a];if(l?.role==="assistant"){typeof l.content=="string"?o=l.content:Array.isArray(l.content)&&(o=l.content.filter(m=>m.type==="text").map(m=>m.text||"").join(`
|
|
9
|
+
`));break}}await q(i,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:o},e.logger),e.logger.info(`[claude-mem] Scheduling session complete in ${_}ms: ${t}`),y(t)}),e.on("session_end",async(r,n)=>{T(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{w.clear(),A.clear(),u.clear(),f.clear(),v.clear();for(let r of b.values())clearTimeout(r);b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let E=null,O="disconnected",$=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{E&&(E.abort(),$&&(await $,$=null));let n=s.observationFeed;if(!n?.enabled){e.logger.info("[claude-mem] Observation feed disabled");return}if(!n.channel||!n.to){e.logger.warn("[claude-mem] Observation feed misconfigured \u2014 channel or target missing");return}e.logger.info(`[claude-mem] Observation feed starting \u2014 channel: ${n.channel}, target: ${n.to}`),E=new AbortController,$=se(e,i,n.channel,n.to,E,t=>{O=t},c,n.botToken)},stop:async r=>{E&&(E.abort(),E=null),$&&(await $,$=null),O="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function N(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,o)=>{let a=t,l=String(a.title||a.subtitle||a.text||"Untitled"),m=a.project?` [${String(a.project)}]`:"";return`${o+1}. ${l}${m}`}).join(`
|
|
10
|
+
`)}function F(r,n=10){let t=Number(r);return Number.isFinite(t)?Math.max(1,Math.min(50,Math.trunc(t))):n}e.registerCommand({name:"claude_mem_feed",description:"Show or toggle Claude-Mem observation feed status",acceptsArgs:!0,handler:async r=>{let n=s.observationFeed;if(!n)return{text:"Observation feed not configured. Add observationFeed to your plugin config."};let t=r.args?.trim();return t==="on"?(e.logger.info("[claude-mem] Feed enable requested via command"),{text:"Feed enable requested. Update observationFeed.enabled in your plugin config to persist."}):t==="off"?(e.logger.info("[claude-mem] Feed disable requested via command"),{text:"Feed disable requested. Update observationFeed.enabled in your plugin config to persist."}):{text:["Claude-Mem Observation Feed",`Enabled: ${n.enabled?"yes":"no"}`,`Channel: ${n.channel||"not set"}`,`Target: ${n.to||"not set"}`,`Connection: ${O}`].join(`
|
|
11
|
+
`)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),o=t[t.length-1],a=/^\d+$/.test(o),l=a?F(o,10):10,m=a?t.slice(0,-1).join(" "):n,p=await B(i,`/api/search/observations?query=${encodeURIComponent(m)}&limit=${l}`,e.logger);if(!p)return"Claude-Mem search failed (worker unavailable or invalid response).";let S=Array.isArray(p.items)?p.items:[];return[`Claude-Mem Search: "${m}"`,N(S,l)].join(`
|
|
12
|
+
`)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],o=t.length>0?t[t.length-1]:"",a=/^\d+$/.test(o),l=a?F(o,3):3,m=a?t.slice(0,-1).join(" "):n,p=new URLSearchParams;p.set("limit",String(l)),m&&p.set("project",m);let S=await B(i,`/api/context/recent?${p.toString()}`,e.logger);if(!S)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let h=Array.isArray(S.session_summaries)?S.session_summaries:[],D=Array.isArray(S.recent_observations)?S.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${m||"(auto)"}`,`Session summaries: ${h.length}`,`Recent observations: ${D.length}`,N(D,Math.min(5,D.length||5))].join(`
|
|
13
|
+
`)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),o=5,a=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(o=F(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(a=F(t.pop(),5));let l=t.join(" "),m=new URLSearchParams({query:l,mode:"auto",depth_before:String(a),depth_after:String(o)}),p=await B(i,`/api/timeline/by-query?${m.toString()}`,e.logger);if(!p)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let S=Array.isArray(p.timeline)?p.timeline:[],h=p.anchor?String(p.anchor):"(none)";return[`Claude-Mem Timeline: "${l}"`,`Anchor: ${h}`,N(S,8)].join(`
|
|
14
|
+
`)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await K(i,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${i}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${i}`,`Active sessions: ${w.size}`,`Observation feed: ${O}`].join(`
|
|
15
|
+
`)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${U}:${i})`)}export{oe as default};
|
|
@@ -979,207 +979,3 @@ describe("SSE stream integration", () => {
|
|
|
979
979
|
await getService().stop({});
|
|
980
980
|
});
|
|
981
981
|
});
|
|
982
|
-
|
|
983
|
-
describe("circuit breaker", () => {
|
|
984
|
-
// Reset circuit breaker state before each test by firing gateway_start.
|
|
985
|
-
// The circuit is module-level state, so tests would otherwise bleed into each other.
|
|
986
|
-
beforeEach(async () => {
|
|
987
|
-
const { api, fireEvent } = createMockApi({ workerPort: 59999 });
|
|
988
|
-
claudeMemPlugin(api);
|
|
989
|
-
await fireEvent("gateway_start", {}, {});
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
it("opens after threshold failures and stops further requests", async () => {
|
|
993
|
-
const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
|
|
994
|
-
claudeMemPlugin(api);
|
|
995
|
-
// Reset circuit inside the test body to guard against timers from preceding
|
|
996
|
-
// tests (e.g. completionDelayMs timers) that may fire between beforeEach and here.
|
|
997
|
-
await fireEvent("gateway_start", {}, {});
|
|
998
|
-
|
|
999
|
-
// Fire threshold+1 calls so the circuit is open by the end of the loop
|
|
1000
|
-
// regardless of whether a concurrent timer fires at the exact boundary.
|
|
1001
|
-
for (let i = 0; i < 4; i++) {
|
|
1002
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-open-${i}` });
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// Circuit is now OPEN. Subsequent calls must be silently dropped.
|
|
1006
|
-
const logCountBeforeDrop = logs.length;
|
|
1007
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-drop" });
|
|
1008
|
-
const noisyDropLogs = logs.slice(logCountBeforeDrop).filter(
|
|
1009
|
-
(l) => l.includes("failed") || l.includes("disabling")
|
|
1010
|
-
);
|
|
1011
|
-
assert.equal(noisyDropLogs.length, 0, "calls when circuit is open should be silently dropped");
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it("logs individual failures while circuit is closed, then disabling when it opens", async () => {
|
|
1015
|
-
const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
|
|
1016
|
-
claudeMemPlugin(api);
|
|
1017
|
-
await fireEvent("gateway_start", {}, {});
|
|
1018
|
-
const logsAfterReset = logs.length;
|
|
1019
|
-
|
|
1020
|
-
// Fire exactly threshold (3) calls
|
|
1021
|
-
for (let i = 0; i < 3; i++) {
|
|
1022
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-log-${i}` });
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const newLogs = logs.slice(logsAfterReset);
|
|
1026
|
-
// At least some failures should have been logged (circuit was active)
|
|
1027
|
-
assert.ok(newLogs.length > 0, "threshold calls should produce log output");
|
|
1028
|
-
// Exactly one disabling warning should appear
|
|
1029
|
-
const disablingLogs = newLogs.filter((l) => l.includes("disabling requests"));
|
|
1030
|
-
assert.equal(disablingLogs.length, 1, "should emit exactly one disabling warning when circuit opens");
|
|
1031
|
-
// The last call (the threshold-crossing one) should NOT log an individual failure
|
|
1032
|
-
const failureLogs = newLogs.filter((l) => l.includes("failed:"));
|
|
1033
|
-
assert.ok(failureLogs.length < 3, "threshold-crossing call should not log an individual failure");
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
it("resets on gateway_start, allowing connections again", async () => {
|
|
1037
|
-
const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
|
|
1038
|
-
claudeMemPlugin(api);
|
|
1039
|
-
await fireEvent("gateway_start", {}, {});
|
|
1040
|
-
|
|
1041
|
-
// Open the circuit by firing threshold+1 calls
|
|
1042
|
-
for (let i = 0; i < 4; i++) {
|
|
1043
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-reset-${i}` });
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// Confirm circuit is open (call is silently dropped)
|
|
1047
|
-
const logCountWhileOpen = logs.length;
|
|
1048
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-while-open" });
|
|
1049
|
-
assert.equal(
|
|
1050
|
-
logs.slice(logCountWhileOpen).filter((l) => l.includes("failed") || l.includes("disabling")).length,
|
|
1051
|
-
0,
|
|
1052
|
-
"call while circuit is open should be silently dropped"
|
|
1053
|
-
);
|
|
1054
|
-
|
|
1055
|
-
// gateway_start resets the circuit
|
|
1056
|
-
await fireEvent("gateway_start", {}, {});
|
|
1057
|
-
|
|
1058
|
-
// Next call should attempt to connect again (not silently drop)
|
|
1059
|
-
const logCountAfterReset = logs.length;
|
|
1060
|
-
await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-after-reset" });
|
|
1061
|
-
const newLogs = logs.slice(logCountAfterReset);
|
|
1062
|
-
assert.ok(
|
|
1063
|
-
newLogs.some((l) => l.includes("failed:") || l.includes("disabling")),
|
|
1064
|
-
"should attempt worker connection after gateway_start reset"
|
|
1065
|
-
);
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
it("HALF_OPEN allows only a single probe — non-2xx keeps circuit open, 2xx closes it", async () => {
|
|
1069
|
-
// ---- Phase 1: open the circuit via network failures (unreachable port) ----
|
|
1070
|
-
// Reset circuit state first
|
|
1071
|
-
const resetMock = createMockApi({ workerPort: 59999 });
|
|
1072
|
-
claudeMemPlugin(resetMock.api);
|
|
1073
|
-
await resetMock.fireEvent("gateway_start", {}, {});
|
|
1074
|
-
|
|
1075
|
-
// Drive 4 failures to ensure circuit is OPEN
|
|
1076
|
-
for (let i = 0; i < 4; i++) {
|
|
1077
|
-
await resetMock.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase1-${i}` });
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// ---- Phase 2: advance clock so cooldown has elapsed ----
|
|
1081
|
-
// _circuitOpenedAt was set during Phase 1 using the real Date.now().
|
|
1082
|
-
// Advancing Date.now by 31s means the next circuitAllow call sees the cooldown elapsed.
|
|
1083
|
-
const realDateNow = Date.now.bind(Date);
|
|
1084
|
-
Date.now = () => realDateNow() + 31_000;
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
// ---- Phase 3: non-2xx probe — circuit should stay OPEN ----
|
|
1088
|
-
// Start a server that returns 500 for all requests
|
|
1089
|
-
let serverA: Server | null = null;
|
|
1090
|
-
const portA: number = await new Promise((resolve) => {
|
|
1091
|
-
serverA = createServer((_req: IncomingMessage, res: ServerResponse) => {
|
|
1092
|
-
res.writeHead(500);
|
|
1093
|
-
res.end();
|
|
1094
|
-
});
|
|
1095
|
-
serverA!.listen(0, () => {
|
|
1096
|
-
const addr = serverA!.address();
|
|
1097
|
-
resolve((addr as any).port);
|
|
1098
|
-
});
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
// Reuse the same module-level circuit state — just change the worker port.
|
|
1102
|
-
// Create a new mock api instance pointed at server A (500 responder).
|
|
1103
|
-
const mockA = createMockApi({ workerPort: portA });
|
|
1104
|
-
claudeMemPlugin(mockA.api);
|
|
1105
|
-
// Do NOT fire gateway_start here — we want the OPEN circuit state from Phase 1.
|
|
1106
|
-
|
|
1107
|
-
// The circuit is OPEN but the mocked clock says cooldown elapsed.
|
|
1108
|
-
// The next call should: transition to HALF_OPEN, set _halfOpenProbeInFlight=true,
|
|
1109
|
-
// send the probe to server A (which returns 500), then call circuitOnFailure
|
|
1110
|
-
// and re-open the circuit.
|
|
1111
|
-
const logCountAtProbe = mockA.logs.length;
|
|
1112
|
-
await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-non2xx" });
|
|
1113
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1114
|
-
|
|
1115
|
-
const probeALogs = mockA.logs.slice(logCountAtProbe);
|
|
1116
|
-
// After a 500 response, circuitOnFailure is called which logs "disabling requests"
|
|
1117
|
-
// (because state was HALF_OPEN) and logger.warn logs the 500 status.
|
|
1118
|
-
assert.ok(
|
|
1119
|
-
probeALogs.some((l) => l.includes("disabling") || l.includes("returned 500") || l.includes("Worker POST")),
|
|
1120
|
-
"non-2xx probe should keep circuit open (expected disabling or 500 status log)"
|
|
1121
|
-
);
|
|
1122
|
-
|
|
1123
|
-
// Verify probe flag resets: a second call with cooldown elapsed should be allowed as a new probe
|
|
1124
|
-
// (i.e., _halfOpenProbeInFlight was cleared by circuitOnFailure).
|
|
1125
|
-
// But without advancing time further the circuit is OPEN again — so calls are dropped.
|
|
1126
|
-
const logCountAfterFailedProbe = mockA.logs.length;
|
|
1127
|
-
await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-concurrent" });
|
|
1128
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1129
|
-
const droppedLogs = mockA.logs.slice(logCountAfterFailedProbe).filter(
|
|
1130
|
-
(l) => l.includes("failed") || l.includes("disabling")
|
|
1131
|
-
);
|
|
1132
|
-
assert.equal(droppedLogs.length, 0, "call should be silently dropped while circuit is OPEN again after failed probe");
|
|
1133
|
-
|
|
1134
|
-
serverA!.close();
|
|
1135
|
-
|
|
1136
|
-
// ---- Phase 4: 2xx probe — circuit should close ----
|
|
1137
|
-
// Re-open the circuit with fresh failures, then probe with a 200-returning server.
|
|
1138
|
-
// Reset circuit state first.
|
|
1139
|
-
const resetMock2 = createMockApi({ workerPort: 59999 });
|
|
1140
|
-
claudeMemPlugin(resetMock2.api);
|
|
1141
|
-
await resetMock2.fireEvent("gateway_start", {}, {});
|
|
1142
|
-
|
|
1143
|
-
// Drive failures (still using mocked Date.now, but _circuitOpenedAt will be set to
|
|
1144
|
-
// the mocked time, so cooldown is NOT elapsed yet from the mocked perspective).
|
|
1145
|
-
// We need to temporarily restore real Date.now while opening the circuit, then
|
|
1146
|
-
// re-mock it for the probe.
|
|
1147
|
-
Date.now = realDateNow;
|
|
1148
|
-
for (let i = 0; i < 4; i++) {
|
|
1149
|
-
await resetMock2.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase4-${i}` });
|
|
1150
|
-
}
|
|
1151
|
-
// Re-advance the clock past cooldown
|
|
1152
|
-
Date.now = () => realDateNow() + 31_000;
|
|
1153
|
-
|
|
1154
|
-
let serverB: Server | null = null;
|
|
1155
|
-
const portB: number = await new Promise((resolve) => {
|
|
1156
|
-
serverB = createServer((_req: IncomingMessage, res: ServerResponse) => {
|
|
1157
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1158
|
-
res.end(JSON.stringify({ sessionDbId: 1, promptNumber: 1, skipped: false }));
|
|
1159
|
-
});
|
|
1160
|
-
serverB!.listen(0, () => {
|
|
1161
|
-
const addr = serverB!.address();
|
|
1162
|
-
resolve((addr as any).port);
|
|
1163
|
-
});
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
const mockB = createMockApi({ workerPort: portB });
|
|
1167
|
-
claudeMemPlugin(mockB.api);
|
|
1168
|
-
// Do NOT fire gateway_start — reuse OPEN circuit state from resetMock2.
|
|
1169
|
-
|
|
1170
|
-
const logCountBeforeSuccessProbe = mockB.logs.length;
|
|
1171
|
-
await mockB.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-2xx" });
|
|
1172
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1173
|
-
|
|
1174
|
-
const successProbeLogs = mockB.logs.slice(logCountBeforeSuccessProbe);
|
|
1175
|
-
assert.ok(
|
|
1176
|
-
successProbeLogs.some((l) => l.includes("restored") || l.includes("circuit closed")),
|
|
1177
|
-
"2xx probe should close the circuit — expected 'restored' or 'circuit closed' log"
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
serverB!.close();
|
|
1181
|
-
} finally {
|
|
1182
|
-
Date.now = realDateNow;
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
});
|
package/openclaw/src/index.ts
CHANGED
|
@@ -264,80 +264,12 @@ function workerBaseUrl(port: number): string {
|
|
|
264
264
|
return `http://${_workerHost}:${port}`;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
// ============================================================================
|
|
268
|
-
// Worker Circuit Breaker
|
|
269
|
-
// ============================================================================
|
|
270
|
-
// Prevents CPU-spinning retry loops when the worker is unreachable.
|
|
271
|
-
// After CIRCUIT_BREAKER_THRESHOLD consecutive network errors, the circuit
|
|
272
|
-
// opens and all worker calls are silently dropped for CIRCUIT_BREAKER_COOLDOWN_MS.
|
|
273
|
-
// After the cooldown, one probe attempt is allowed to check if the worker recovered.
|
|
274
|
-
|
|
275
|
-
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
276
|
-
const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
|
|
277
|
-
|
|
278
|
-
type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
|
|
279
|
-
|
|
280
|
-
let _circuitState: CircuitState = "CLOSED";
|
|
281
|
-
let _circuitFailures = 0;
|
|
282
|
-
let _circuitOpenedAt = 0;
|
|
283
|
-
let _halfOpenProbeInFlight = false;
|
|
284
|
-
|
|
285
|
-
function circuitAllow(logger: PluginLogger): boolean {
|
|
286
|
-
if (_circuitState === "CLOSED") return true;
|
|
287
|
-
if (_circuitState === "OPEN") {
|
|
288
|
-
if (Date.now() - _circuitOpenedAt >= CIRCUIT_BREAKER_COOLDOWN_MS) {
|
|
289
|
-
_circuitState = "HALF_OPEN";
|
|
290
|
-
logger.info("[claude-mem] Circuit breaker: probing worker connection");
|
|
291
|
-
if (_halfOpenProbeInFlight) return false;
|
|
292
|
-
_halfOpenProbeInFlight = true;
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
// HALF_OPEN: allow one probe through
|
|
298
|
-
if (_halfOpenProbeInFlight) return false;
|
|
299
|
-
_halfOpenProbeInFlight = true;
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function circuitOnSuccess(logger: PluginLogger): void {
|
|
304
|
-
if (_circuitState !== "CLOSED") {
|
|
305
|
-
logger.info("[claude-mem] Worker connection restored — circuit closed");
|
|
306
|
-
}
|
|
307
|
-
_circuitState = "CLOSED";
|
|
308
|
-
_circuitFailures = 0;
|
|
309
|
-
_halfOpenProbeInFlight = false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function circuitOnFailure(logger: PluginLogger): void {
|
|
313
|
-
_halfOpenProbeInFlight = false;
|
|
314
|
-
_circuitFailures++;
|
|
315
|
-
if (
|
|
316
|
-
_circuitState === "HALF_OPEN" ||
|
|
317
|
-
(_circuitState === "CLOSED" && _circuitFailures >= CIRCUIT_BREAKER_THRESHOLD)
|
|
318
|
-
) {
|
|
319
|
-
_circuitState = "OPEN";
|
|
320
|
-
_circuitOpenedAt = Date.now();
|
|
321
|
-
logger.warn(
|
|
322
|
-
`[claude-mem] Worker unreachable — disabling requests for ${CIRCUIT_BREAKER_COOLDOWN_MS / 1000}s`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function circuitReset(): void {
|
|
328
|
-
_circuitState = "CLOSED";
|
|
329
|
-
_circuitFailures = 0;
|
|
330
|
-
_circuitOpenedAt = 0;
|
|
331
|
-
_halfOpenProbeInFlight = false;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
267
|
async function workerPost(
|
|
335
268
|
port: number,
|
|
336
269
|
path: string,
|
|
337
270
|
body: Record<string, unknown>,
|
|
338
271
|
logger: PluginLogger
|
|
339
272
|
): Promise<Record<string, unknown> | null> {
|
|
340
|
-
if (!circuitAllow(logger)) return null;
|
|
341
273
|
try {
|
|
342
274
|
const response = await fetch(`${workerBaseUrl(port)}${path}`, {
|
|
343
275
|
method: "POST",
|
|
@@ -345,18 +277,13 @@ async function workerPost(
|
|
|
345
277
|
body: JSON.stringify(body),
|
|
346
278
|
});
|
|
347
279
|
if (!response.ok) {
|
|
348
|
-
circuitOnFailure(logger);
|
|
349
280
|
logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
|
|
350
281
|
return null;
|
|
351
282
|
}
|
|
352
|
-
circuitOnSuccess(logger);
|
|
353
283
|
return (await response.json()) as Record<string, unknown>;
|
|
354
284
|
} catch (error: unknown) {
|
|
355
285
|
const message = error instanceof Error ? error.message : String(error);
|
|
356
|
-
|
|
357
|
-
if (_circuitState !== "OPEN") {
|
|
358
|
-
logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
|
|
359
|
-
}
|
|
286
|
+
logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
|
|
360
287
|
return null;
|
|
361
288
|
}
|
|
362
289
|
}
|
|
@@ -367,24 +294,13 @@ function workerPostFireAndForget(
|
|
|
367
294
|
body: Record<string, unknown>,
|
|
368
295
|
logger: PluginLogger
|
|
369
296
|
): void {
|
|
370
|
-
if (!circuitAllow(logger)) return;
|
|
371
297
|
fetch(`${workerBaseUrl(port)}${path}`, {
|
|
372
298
|
method: "POST",
|
|
373
299
|
headers: { "Content-Type": "application/json" },
|
|
374
300
|
body: JSON.stringify(body),
|
|
375
|
-
}).then((response) => {
|
|
376
|
-
if (!response.ok) {
|
|
377
|
-
circuitOnFailure(logger);
|
|
378
|
-
logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
circuitOnSuccess(logger);
|
|
382
301
|
}).catch((error: unknown) => {
|
|
383
302
|
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
-
|
|
385
|
-
if (_circuitState !== "OPEN") {
|
|
386
|
-
logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
|
|
387
|
-
}
|
|
303
|
+
logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
|
|
388
304
|
});
|
|
389
305
|
}
|
|
390
306
|
|
|
@@ -393,22 +309,16 @@ async function workerGetText(
|
|
|
393
309
|
path: string,
|
|
394
310
|
logger: PluginLogger
|
|
395
311
|
): Promise<string | null> {
|
|
396
|
-
if (!circuitAllow(logger)) return null;
|
|
397
312
|
try {
|
|
398
313
|
const response = await fetch(`${workerBaseUrl(port)}${path}`);
|
|
399
314
|
if (!response.ok) {
|
|
400
|
-
circuitOnFailure(logger);
|
|
401
315
|
logger.warn(`[claude-mem] Worker GET ${path} returned ${response.status}`);
|
|
402
316
|
return null;
|
|
403
317
|
}
|
|
404
|
-
circuitOnSuccess(logger);
|
|
405
318
|
return await response.text();
|
|
406
319
|
} catch (error: unknown) {
|
|
407
320
|
const message = error instanceof Error ? error.message : String(error);
|
|
408
|
-
|
|
409
|
-
if (_circuitState !== "OPEN") {
|
|
410
|
-
logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`);
|
|
411
|
-
}
|
|
321
|
+
logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`);
|
|
412
322
|
return null;
|
|
413
323
|
}
|
|
414
324
|
}
|
|
@@ -946,7 +856,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|
|
946
856
|
// Event: gateway_start — clear session tracking for fresh start
|
|
947
857
|
// ------------------------------------------------------------------
|
|
948
858
|
api.on("gateway_start", async () => {
|
|
949
|
-
circuitReset();
|
|
950
859
|
sessionIds.clear();
|
|
951
860
|
contextCache.clear();
|
|
952
861
|
recentPromptInits.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.1.4",
|
|
4
4
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"ansi-to-html": "^0.7.2",
|
|
116
116
|
"dompurify": "^3.3.1",
|
|
117
117
|
"express": "^4.18.2",
|
|
118
|
-
"glob": "^
|
|
118
|
+
"glob": "^11.0.3",
|
|
119
119
|
"handlebars": "^4.7.8",
|
|
120
120
|
"picocolors": "^1.1.1",
|
|
121
121
|
"react": "^18.3.1",
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "
|
|
10
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; \"$_R/scripts/setup.sh\"",
|
|
11
11
|
"timeout": 300
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
"hooks": [
|
|
20
20
|
{
|
|
21
21
|
"type": "command",
|
|
22
|
-
"command": "export PATH=\"$(
|
|
22
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"",
|
|
23
23
|
"timeout": 300
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"type": "command",
|
|
27
|
-
"command": "export PATH=\"$(
|
|
27
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start; for i in 1 2 3 4 5 6 7 8; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; curl -sf http://localhost:37777/health >/dev/null 2>&1 || exit 1; echo '{\"continue\":true,\"suppressOutput\":true}'",
|
|
28
28
|
"timeout": 60
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"type": "command",
|
|
32
|
-
"command": "export PATH=\"$(
|
|
32
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; for i in 1 2 3 4 5 6 7 8; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code context",
|
|
33
33
|
"timeout": 60
|
|
34
34
|
}
|
|
35
35
|
]
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hooks": [
|
|
41
41
|
{
|
|
42
42
|
"type": "command",
|
|
43
|
-
"command": "export PATH=\"$(
|
|
43
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-init",
|
|
44
44
|
"timeout": 60
|
|
45
45
|
}
|
|
46
46
|
]
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"hooks": [
|
|
53
53
|
{
|
|
54
54
|
"type": "command",
|
|
55
|
-
"command": "export PATH=\"$(
|
|
55
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code observation",
|
|
56
56
|
"timeout": 120
|
|
57
57
|
}
|
|
58
58
|
]
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"hooks": [
|
|
65
65
|
{
|
|
66
66
|
"type": "command",
|
|
67
|
-
"command": "
|
|
67
|
+
"command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code file-context",
|
|
68
68
|
"timeout": 2000
|
|
69
69
|
}
|
|
70
70
|
]
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"hooks": [
|
|
76
76
|
{
|
|
77
77
|
"type": "command",
|
|
78
|
-
"command": "export PATH=\"$(
|
|
78
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code summarize",
|
|
79
79
|
"timeout": 120
|
|
80
80
|
}
|
|
81
81
|
]
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"hooks": [
|
|
87
87
|
{
|
|
88
88
|
"type": "command",
|
|
89
|
-
"command": "export PATH=\"$(
|
|
89
|
+
"command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete",
|
|
90
90
|
"timeout": 30
|
|
91
91
|
}
|
|
92
92
|
]
|
package/plugin/package.json
CHANGED
|
@@ -47,20 +47,12 @@ function fixBrokenScriptPath(argPath) {
|
|
|
47
47
|
* Find Bun executable - checks PATH first, then common install locations
|
|
48
48
|
*/
|
|
49
49
|
function findBun() {
|
|
50
|
-
// Try PATH first
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
encoding: 'utf-8',
|
|
57
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
-
shell: true
|
|
59
|
-
})
|
|
60
|
-
: spawnSync('which', ['bun'], {
|
|
61
|
-
encoding: 'utf-8',
|
|
62
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
63
|
-
});
|
|
50
|
+
// Try PATH first
|
|
51
|
+
const pathCheck = spawnSync(IS_WINDOWS ? 'where' : 'which', ['bun'], {
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
shell: IS_WINDOWS
|
|
55
|
+
});
|
|
64
56
|
|
|
65
57
|
if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
|
|
66
58
|
// On Windows, prefer bun.cmd over bun (bun is a shell script, bun.cmd is the Windows batch file)
|