@bjlee2024/claude-mem 13.4.0

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.
Files changed (101) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.codex-plugin/plugin.json +46 -0
  3. package/LICENSE +202 -0
  4. package/README.md +419 -0
  5. package/dist/npx-cli/index.js +10001 -0
  6. package/dist/opencode-plugin/index.js +67 -0
  7. package/openclaw/Dockerfile.e2e +46 -0
  8. package/openclaw/SKILL.md +462 -0
  9. package/openclaw/TESTING.md +279 -0
  10. package/openclaw/dist/index.js +15 -0
  11. package/openclaw/e2e-verify.sh +222 -0
  12. package/openclaw/install.sh +1653 -0
  13. package/openclaw/openclaw.plugin.json +98 -0
  14. package/openclaw/package.json +21 -0
  15. package/openclaw/src/index.test.ts +1124 -0
  16. package/openclaw/src/index.ts +1092 -0
  17. package/openclaw/test-e2e.sh +40 -0
  18. package/openclaw/test-install.sh +2086 -0
  19. package/openclaw/test-sse-consumer.js +98 -0
  20. package/openclaw/tsconfig.json +26 -0
  21. package/package.json +211 -0
  22. package/plugin/.claude-plugin/plugin.json +24 -0
  23. package/plugin/.codex-plugin/plugin.json +46 -0
  24. package/plugin/.mcp.json +12 -0
  25. package/plugin/hooks/bugfixes-2026-01-10.md +92 -0
  26. package/plugin/hooks/codex-hooks.json +74 -0
  27. package/plugin/hooks/hooks.json +87 -0
  28. package/plugin/modes/code--ar.json +24 -0
  29. package/plugin/modes/code--bn.json +24 -0
  30. package/plugin/modes/code--chill.json +8 -0
  31. package/plugin/modes/code--cs.json +24 -0
  32. package/plugin/modes/code--da.json +24 -0
  33. package/plugin/modes/code--de.json +24 -0
  34. package/plugin/modes/code--el.json +24 -0
  35. package/plugin/modes/code--es.json +24 -0
  36. package/plugin/modes/code--fi.json +24 -0
  37. package/plugin/modes/code--fr.json +24 -0
  38. package/plugin/modes/code--he.json +24 -0
  39. package/plugin/modes/code--hi.json +24 -0
  40. package/plugin/modes/code--hu.json +24 -0
  41. package/plugin/modes/code--id.json +24 -0
  42. package/plugin/modes/code--it.json +24 -0
  43. package/plugin/modes/code--ja.json +24 -0
  44. package/plugin/modes/code--ko.json +24 -0
  45. package/plugin/modes/code--nl.json +24 -0
  46. package/plugin/modes/code--no.json +24 -0
  47. package/plugin/modes/code--pl.json +24 -0
  48. package/plugin/modes/code--pt-br.json +24 -0
  49. package/plugin/modes/code--ro.json +24 -0
  50. package/plugin/modes/code--ru.json +24 -0
  51. package/plugin/modes/code--sv.json +24 -0
  52. package/plugin/modes/code--th.json +24 -0
  53. package/plugin/modes/code--tr.json +24 -0
  54. package/plugin/modes/code--uk.json +24 -0
  55. package/plugin/modes/code--ur.json +25 -0
  56. package/plugin/modes/code--vi.json +24 -0
  57. package/plugin/modes/code--zh.json +24 -0
  58. package/plugin/modes/code.json +139 -0
  59. package/plugin/modes/email-investigation.json +120 -0
  60. package/plugin/modes/law-study--chill.json +7 -0
  61. package/plugin/modes/law-study-CLAUDE.md +85 -0
  62. package/plugin/modes/law-study.json +120 -0
  63. package/plugin/modes/meme-tokens.json +125 -0
  64. package/plugin/package.json +46 -0
  65. package/plugin/scripts/bun-runner.js +216 -0
  66. package/plugin/scripts/context-generator.cjs +795 -0
  67. package/plugin/scripts/mcp-server.cjs +239 -0
  68. package/plugin/scripts/server-beta-service.cjs +9856 -0
  69. package/plugin/scripts/statusline-counts.js +40 -0
  70. package/plugin/scripts/version-check.js +69 -0
  71. package/plugin/scripts/worker-cli.js +19 -0
  72. package/plugin/scripts/worker-service.cjs +2368 -0
  73. package/plugin/scripts/worker-wrapper.cjs +2 -0
  74. package/plugin/skills/babysit/SKILL.md +87 -0
  75. package/plugin/skills/design-is/SKILL.md +312 -0
  76. package/plugin/skills/do/SKILL.md +45 -0
  77. package/plugin/skills/how-it-works/SKILL.md +22 -0
  78. package/plugin/skills/how-it-works/onboarding-explainer.md +17 -0
  79. package/plugin/skills/knowledge-agent/SKILL.md +80 -0
  80. package/plugin/skills/learn-codebase/SKILL.md +21 -0
  81. package/plugin/skills/make-plan/SKILL.md +67 -0
  82. package/plugin/skills/mem-search/SKILL.md +131 -0
  83. package/plugin/skills/oh-my-issues/SKILL.md +226 -0
  84. package/plugin/skills/pathfinder/SKILL.md +111 -0
  85. package/plugin/skills/smart-explore/SKILL.md +190 -0
  86. package/plugin/skills/timeline-report/SKILL.md +211 -0
  87. package/plugin/skills/version-bump/SKILL.md +68 -0
  88. package/plugin/skills/version-bump/scripts/generate_changelog.js +34 -0
  89. package/plugin/skills/weekly-digests/SKILL.md +262 -0
  90. package/plugin/skills/wowerpoint/SKILL.md +205 -0
  91. package/plugin/ui/assets/fonts/monaspace-radon-var.woff +0 -0
  92. package/plugin/ui/assets/fonts/monaspace-radon-var.woff2 +0 -0
  93. package/plugin/ui/claude-mem-logo-for-dark-mode.webp +0 -0
  94. package/plugin/ui/claude-mem-logo-stylized.png +0 -0
  95. package/plugin/ui/claude-mem-logomark.webp +0 -0
  96. package/plugin/ui/icon-thick-completed.svg +8 -0
  97. package/plugin/ui/icon-thick-investigated.svg +8 -0
  98. package/plugin/ui/icon-thick-learned.svg +12 -0
  99. package/plugin/ui/icon-thick-next-steps.svg +8 -0
  100. package/plugin/ui/viewer-bundle.js +65 -0
  101. package/plugin/ui/viewer.html +3145 -0
@@ -0,0 +1,279 @@
1
+ # OpenClaw Claude-Mem Plugin — Testing Guide
2
+
3
+ ## Quick Start (Docker)
4
+
5
+ The fastest way to test the plugin is using the pre-built Docker E2E environment:
6
+
7
+ ```bash
8
+ cd openclaw
9
+
10
+ # Automated test (builds, installs plugin on real OpenClaw, verifies everything)
11
+ ./test-e2e.sh
12
+
13
+ # Interactive shell (for manual exploration)
14
+ ./test-e2e.sh --interactive
15
+
16
+ # Just build the image
17
+ ./test-e2e.sh --build-only
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Test Layers
23
+
24
+ ### 1. Unit Tests (fastest)
25
+
26
+ ```bash
27
+ cd openclaw
28
+ npm test # compiles TypeScript, runs 17 tests
29
+ ```
30
+
31
+ Tests plugin registration, service lifecycle, command handling, SSE integration, and all 6 channel types.
32
+
33
+ ### 2. Smoke Test
34
+
35
+ ```bash
36
+ node test-sse-consumer.js
37
+ ```
38
+
39
+ Quick check that the plugin loads and registers its service + command correctly.
40
+
41
+ ### 3. Container Unit Tests (fresh install)
42
+
43
+ ```bash
44
+ ./test-container.sh # Unit tests in clean Docker
45
+ ./test-container.sh --full # Integration tests with mock worker
46
+ ```
47
+
48
+ ### 4. E2E on Real OpenClaw (Docker)
49
+
50
+ ```bash
51
+ ./test-e2e.sh
52
+ ```
53
+
54
+ This is the most comprehensive test. It:
55
+ 1. Uses the official `ghcr.io/openclaw/openclaw:main` Docker image
56
+ 2. Installs the plugin via `openclaw plugins install` (same as a real user)
57
+ 3. Enables the plugin via `openclaw plugins enable`
58
+ 4. Starts a mock claude-mem worker on port 37777
59
+ 5. Starts the OpenClaw gateway with plugin config
60
+ 6. Verifies the plugin loads, connects to SSE, and processes events
61
+
62
+ **All 16 checks must pass.**
63
+
64
+ ---
65
+
66
+ ## Human E2E Testing (Interactive Docker)
67
+
68
+ For manual walkthrough testing, use the interactive Docker mode:
69
+
70
+ ```bash
71
+ ./test-e2e.sh --interactive
72
+ ```
73
+
74
+ This drops you into a fully-configured OpenClaw container with the plugin pre-installed.
75
+
76
+ ### Step-by-step inside the container
77
+
78
+ #### 1. Verify plugin is installed
79
+
80
+ ```bash
81
+ node openclaw.mjs plugins list
82
+ node openclaw.mjs plugins info claude-mem
83
+ node openclaw.mjs plugins doctor
84
+ ```
85
+
86
+ **Expected:**
87
+ - `claude-mem` appears in the plugins list as "enabled" or "loaded"
88
+ - Info shows version 1.0.0, source at `/home/node/.openclaw/extensions/claude-mem/`
89
+ - Doctor reports no issues
90
+
91
+ #### 2. Inspect plugin files
92
+
93
+ ```bash
94
+ ls -la /home/node/.openclaw/extensions/claude-mem/
95
+ cat /home/node/.openclaw/extensions/claude-mem/openclaw.plugin.json
96
+ cat /home/node/.openclaw/extensions/claude-mem/package.json
97
+ ```
98
+
99
+ **Expected:**
100
+ - `dist/index.js` exists (compiled plugin)
101
+ - `openclaw.plugin.json` has `"id": "claude-mem"` and `"kind": "memory"`
102
+ - `package.json` has `openclaw.extensions` field pointing to `./dist/index.js`
103
+
104
+ #### 3. Start mock worker
105
+
106
+ ```bash
107
+ node /app/mock-worker.js &
108
+ ```
109
+
110
+ Verify it's running:
111
+
112
+ ```bash
113
+ curl -s http://localhost:37777/health
114
+ # → {"status":"ok"}
115
+
116
+ curl -s --max-time 3 http://localhost:37777/stream
117
+ # → data: {"type":"connected","message":"Mock worker SSE stream"}
118
+ # → data: {"type":"new_observation","observation":{...}}
119
+ ```
120
+
121
+ #### 4. Configure and start gateway
122
+
123
+ ```bash
124
+ cat > /home/node/.openclaw/openclaw.json << 'EOF'
125
+ {
126
+ "gateway": {
127
+ "mode": "local",
128
+ "auth": {
129
+ "mode": "token",
130
+ "token": "e2e-test-token"
131
+ }
132
+ },
133
+ "plugins": {
134
+ "slots": {
135
+ "memory": "claude-mem"
136
+ },
137
+ "entries": {
138
+ "claude-mem": {
139
+ "enabled": true,
140
+ "config": {
141
+ "workerPort": 37777,
142
+ "observationFeed": {
143
+ "enabled": true,
144
+ "channel": "telegram",
145
+ "to": "test-chat-id-12345"
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ EOF
153
+
154
+ node openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token
155
+ ```
156
+
157
+ **Expected in gateway logs:**
158
+ - `[claude-mem] OpenClaw plugin loaded — v1.0.0`
159
+ - `[claude-mem] Observation feed starting — channel: telegram, target: test-chat-id-12345`
160
+ - `[claude-mem] Connecting to SSE stream at http://localhost:37777/stream`
161
+ - `[claude-mem] Connected to SSE stream`
162
+
163
+ #### 5. Run automated verification (optional)
164
+
165
+ From a second shell in the container (or after stopping the gateway):
166
+
167
+ ```bash
168
+ /bin/bash /app/e2e-verify.sh
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Manual E2E (Real OpenClaw + Real Worker)
174
+
175
+ For testing with a real claude-mem worker and real messaging channel:
176
+
177
+ ### Prerequisites
178
+
179
+ - OpenClaw gateway installed and configured
180
+ - Claude-Mem worker running on port 37777
181
+ - Plugin built: `cd openclaw && npm run build`
182
+
183
+ ### 1. Install the plugin
184
+
185
+ ```bash
186
+ # Build the plugin
187
+ cd openclaw && npm run build
188
+
189
+ # Install on OpenClaw (from the openclaw/ directory)
190
+ openclaw plugins install .
191
+
192
+ # Enable it
193
+ openclaw plugins enable claude-mem
194
+ ```
195
+
196
+ ### 2. Configure
197
+
198
+ Edit `~/.openclaw/openclaw.json` to add plugin config:
199
+
200
+ ```json
201
+ {
202
+ "plugins": {
203
+ "entries": {
204
+ "claude-mem": {
205
+ "enabled": true,
206
+ "config": {
207
+ "workerPort": 37777,
208
+ "observationFeed": {
209
+ "enabled": true,
210
+ "channel": "telegram",
211
+ "to": "YOUR_CHAT_ID"
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ ```
219
+
220
+ **Supported channels:** `telegram`, `discord`, `signal`, `slack`, `whatsapp`, `line`
221
+
222
+ ### 3. Restart gateway
223
+
224
+ ```bash
225
+ openclaw restart
226
+ ```
227
+
228
+ **Look for in logs:**
229
+ - `[claude-mem] OpenClaw plugin loaded — v1.0.0`
230
+ - `[claude-mem] Connected to SSE stream`
231
+
232
+ ### 4. Trigger an observation
233
+
234
+ Start a Claude Code session with claude-mem enabled and perform any action. The worker will emit a `new_observation` SSE event.
235
+
236
+ ### 5. Verify delivery
237
+
238
+ Check the target messaging channel for:
239
+
240
+ ```
241
+ 🧠 Claude-Mem Observation
242
+ **Observation Title**
243
+ Optional subtitle
244
+ ```
245
+
246
+ ---
247
+
248
+ ## Troubleshooting
249
+
250
+ ### `api.log is not a function`
251
+ The plugin was built against the wrong API. Ensure `src/index.ts` uses `api.logger.info()` not `api.log()`. Rebuild with `npm run build`.
252
+
253
+ ### Worker not running
254
+ - **Symptom:** `SSE stream error: fetch failed. Reconnecting in 1s`
255
+ - **Fix:** Start the worker: `cd /path/to/claude-mem && npm run build-and-sync`
256
+
257
+ ### Port mismatch
258
+ - **Fix:** Ensure `workerPort` in config matches the worker's actual port (default: 37777)
259
+
260
+ ### Channel not configured
261
+ - **Symptom:** `Observation feed misconfigured — channel or target missing`
262
+ - **Fix:** Add both `channel` and `to` to `observationFeed` in config
263
+
264
+ ### Unknown channel type
265
+ - **Fix:** Use: `telegram`, `discord`, `signal`, `slack`, `whatsapp`, or `line`
266
+
267
+ ### Feed disabled
268
+ - **Symptom:** `Observation feed disabled`
269
+ - **Fix:** Set `observationFeed.enabled: true`
270
+
271
+ ### Messages not arriving
272
+ 1. Verify the bot/integration is configured in the target channel
273
+ 2. Check the target ID (`to`) is correct
274
+ 3. Look for `Failed to send to <channel>` in logs
275
+ 4. Test the channel via OpenClaw's built-in tools
276
+
277
+ ### Memory slot conflict
278
+ - **Symptom:** `plugin disabled (memory slot set to "memory-core")`
279
+ - **Fix:** Add `"slots": { "memory": "claude-mem" }` to plugins config
@@ -0,0 +1,15 @@
1
+ var Y="127.0.0.1",X=["\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 V(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return X[Math.abs(s)%X.length]}var ee="\u{1F99E}",ne="\u2328\uFE0F",te="Claude Code Session",re="\u{1F980}";function se(e){let s=e?.primary??ee,o=e?.claudeCode??ne,a=e?.claudeCodeLabel??te,l=e?.default??re,g=e?.agents??{};return function(m){if(!m)return l;if(m.startsWith("openclaw-")){let p=m.slice(9);return p?`${g[p]||V(p)} ${p}`:`${s} openclaw`}if(m==="openclaw")return`${s} openclaw`;let b=a.trim();return b?`${o} ${b} (${m})`:`${o} ${m}`}}var q=Y;function T(e){return`http://${q}:${e}`}var oe=3,Q=3e4,y="CLOSED",j=0,J=0,$=!1;function G(e){return y==="CLOSED"?!0:y==="OPEN"?Date.now()-J>=Q?(y="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),$?!1:($=!0,!0)):!1:$?!1:($=!0,!0)}function z(e){y!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),y="CLOSED",j=0,$=!1}function M(e){$=!1,j++,(y==="HALF_OPEN"||y==="CLOSED"&&j>=oe)&&(y="OPEN",J=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${Q/1e3}s`))}function ie(){y="CLOSED",j=0,J=0,$=!1}async function Z(e,s,o,a){if(!G(a))return null;try{let l=await fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return l.ok?(z(a),await l.json()):(M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`),null)}catch(l){let g=l instanceof Error?l.message:String(l);return M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`),null}}function ae(e,s,o,a){G(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(l=>{if(!l.ok){M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`);return}z(a)}).catch(l=>{let g=l instanceof Error?l.message:String(l);M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`)})}async function H(e,s,o){if(!G(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(z(o),await a.text()):(M(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let l=a instanceof Error?a.message:String(a);return M(o),y!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${l}`),null}}async function W(e,s,o){let a=await H(e,s,o);if(!a)return null;try{return JSON.parse(a)}catch{return o.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function ce(e,s){let o=e.title||"Untitled",l=`${s(e.project)}
2
+ **${o}**`;return e.subtitle&&(l+=`
3
+ ${e.subtitle}`),l}var le={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 ue(e,s,o,a){try{let l=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:o,parse_mode:"Markdown"})});if(!l.ok){let g=await l.text();a.warn(`[claude-mem] Direct Telegram send failed (${l.status}): ${g}`)}}catch(l){let g=l instanceof Error?l.message:String(l);a.warn(`[claude-mem] Direct Telegram send error: ${g}`)}}function ge(e,s,o,a,l){if(l&&s==="telegram")return ue(l,o,a,e.logger);let g=le[s];if(!g)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let v=e.runtime.channel[g.namespace];if(!v)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let m=v[g.functionName];return m?m(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(p=>{let w=p instanceof Error?p.message:String(p);e.logger.error(`[claude-mem] Failed to send to ${s}: ${w}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${g.functionName} function`),Promise.resolve())}async function de(e,s,o,a,l,g,v,m){let b=1e3,p=3e4;for(;!l.signal.aborted;){try{g("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let w=await fetch(`${T(s)}/stream`,{signal:l.signal,headers:{Accept:"text/event-stream"}});if(!w.ok)throw new Error(`SSE stream returned HTTP ${w.status}`);if(!w.body)throw new Error("SSE stream response has no body");g("connected"),b=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let x=w.body.getReader(),D=new TextDecoder,_="";for(;;){let{done:R,value:P}=await x.read();if(R)break;_+=D.decode(P,{stream:!0}),_.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),_="");let L=_.split(`
4
+
5
+ `);_=L.pop()||"";for(let B of L){let F=B.split(`
6
+ `).filter(S=>S.startsWith("data:")).map(S=>S.slice(5).trim());if(F.length===0)continue;let A=F.join(`
7
+ `);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let C=ce(S.observation,v);await ge(e,o,a,C,m)}}catch(S){let O=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${O}`)}}}}catch(w){if(l.signal.aborted)break;g("reconnecting");let x=w instanceof Error?w.message:String(w);e.logger.warn(`[claude-mem] SSE stream error: ${x}. Reconnecting in ${b/1e3}s`)}if(l.signal.aborted)break;await new Promise(w=>setTimeout(w,b)),b=Math.min(b*2,p)}g("disconnected")}function me(e){let s=e.pluginConfig||{},o=s.workerPort||37777;q=s.workerHost||Y;let a=s.project||"openclaw",l=se(s.observationFeed?.emojis);function g(r){return r.agentId?`openclaw-${r.agentId}`:a}let v=new Map,m=new Map,b=new Map,p=new Map,w=s.syncMemoryFile!==!1,x=new Set(s.syncMemoryFileExclude||[]);function D(r){let n=r||"default";return v.has(n)||v.set(n,`openclaw-${n}-${Date.now()}`),v.get(n)}function _(r){if(!w)return!1;let n=r?.agentId;return!(n&&x.has(n))}function R(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let i=typeof t=="string"?t.trim():"";i&&n.add(i)}return n.size===0&&n.add("default"),Array.from(n)}function P(r){let n=R(r),t=n.find(u=>m.has(u));t=t?m.get(t):n[0];let i=b.get(t);i||(i=new Set([t]),b.set(t,i));for(let u of n)i.add(u),m.set(u,t);let c=D(t);for(let u of i)v.set(u,c);return{canonicalKey:t,contentSessionId:c}}function L(r,n,t){let i=Date.now();for(let[d,f]of p)i-f>2e3&&p.delete(d);let c=`${r}::${n}::${t}`,u=p.get(c);return p.set(c,i),typeof u=="number"&&i-u<=2e3}function B(r){let n=R(r),t=n.map(c=>m.get(c)).find(Boolean)||n[0],i=b.get(t)||new Set([t,...n]);for(let c of i)m.delete(c),v.delete(c);b.delete(t),v.delete(t)}let F=6e4,A=new Map;async function S(r){let n=[a],t=r?g(r):null;t&&t!==a&&n.push(t);let i=n.join(","),c=A.get(i);if(c&&Date.now()-c.fetchedAt<F)return c.text;let u=await H(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let d=u.trim();return A.set(i,{text:d,fetchedAt:Date.now()}),d}return null}async function O(r,n,t){let{contentSessionId:i}=P(r),c=g(r);if(L(i,c,n)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${i} project=${c} via=${t}`);return}await Z(o,"/api/sessions/init",{contentSessionId:i,project:c,prompt:n},e.logger),e.logger.info(`[claude-mem] Session initialized via ${t}: contentSessionId=${i} project=${c}`)}e.on("session_start",async(r,n)=>{await O(n,"session start","session_start")}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=P(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${i} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{await O(n,"after compaction","after_compaction")}),e.on("before_agent_start",async(r,n)=>{await O(n,r.prompt||"agent run","before_agent_start")}),e.on("before_prompt_build",async(r,n)=>{if(!_(n))return;let t=await S(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:i,contentSessionId:c}=P(n),u="",d=r.message?.content;Array.isArray(d)&&(u=d.filter(E=>(E.type==="tool_result"||E.type==="text")&&"text"in E).map(E=>String(E.text)).join(`
8
+ `));let f=1e3;u.length>f&&(u=u.slice(0,f));let h=n.workspaceDir||process.cwd();n.workspaceDir||e.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${i} tool=${t}`),ae(o,"/api/sessions/observations",{contentSessionId:c,tool_name:t,tool_input:r.params||{},tool_response:u,cwd:h},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=P(n),i="";if(Array.isArray(r.messages))for(let c=r.messages.length-1;c>=0;c--){let u=r.messages[c];if(u?.role==="assistant"){typeof u.content=="string"?i=u.content:Array.isArray(u.content)&&(i=u.content.filter(d=>d.type==="text").map(d=>d.text||"").join(`
9
+ `));break}}await Z(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger)}),e.on("session_end",async(r,n)=>{B(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{ie(),v.clear(),A.clear(),p.clear(),m.clear(),b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let C=null,I="disconnected",k=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{C&&(C.abort(),k&&(await k,k=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}`),C=new AbortController,k=de(e,o,n.channel,n.to,C,t=>{I=t},l,n.botToken)},stop:async r=>{C&&(C.abort(),C=null),k&&(await k,k=null),I="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function U(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,i)=>{let c=t,u=String(c.title||c.subtitle||c.text||"Untitled"),d=c.project?` [${String(c.project)}]`:"";return`${i+1}. ${u}${d}`}).join(`
10
+ `)}function N(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: ${I}`].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+/),i=t[t.length-1],c=/^\d+$/.test(i),u=c?N(i,10):10,d=c?t.slice(0,-1).join(" "):n,f=await W(o,`/api/search/observations?query=${encodeURIComponent(d)}&limit=${u}`,e.logger);if(!f)return"Claude-Mem search failed (worker unavailable or invalid response).";let h=Array.isArray(f.items)?f.items:[];return[`Claude-Mem Search: "${d}"`,U(h,u)].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+/):[],i=t.length>0?t[t.length-1]:"",c=/^\d+$/.test(i),u=c?N(i,3):3,d=c?t.slice(0,-1).join(" "):n,f=new URLSearchParams;f.set("limit",String(u)),d&&f.set("project",d);let h=await W(o,`/api/context/recent?${f.toString()}`,e.logger);if(!h)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let E=Array.isArray(h.session_summaries)?h.session_summaries:[],K=Array.isArray(h.recent_observations)?h.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${d||"(auto)"}`,`Session summaries: ${E.length}`,`Recent observations: ${K.length}`,U(K,Math.min(5,K.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+/),i=5,c=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(i=N(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(c=N(t.pop(),5));let u=t.join(" "),d=new URLSearchParams({query:u,mode:"auto",depth_before:String(c),depth_after:String(i)}),f=await W(o,`/api/timeline/by-query?${d.toString()}`,e.logger);if(!f)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let h=Array.isArray(f.timeline)?f.timeline:[],E=f.anchor?String(f.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${E}`,U(h,8)].join(`
14
+ `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await H(o,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${o}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${o}`,`Active sessions: ${v.size}`,`Observation feed: ${I}`].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: ${q}:${o})`)}export{me as default};
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ TOTAL=0
8
+
9
+ pass() {
10
+ PASS=$((PASS + 1))
11
+ TOTAL=$((TOTAL + 1))
12
+ echo " PASS: $1"
13
+ }
14
+
15
+ fail() {
16
+ FAIL=$((FAIL + 1))
17
+ TOTAL=$((TOTAL + 1))
18
+ echo " FAIL: $1"
19
+ }
20
+
21
+ section() {
22
+ echo ""
23
+ echo "=== $1 ==="
24
+ }
25
+
26
+ section "Phase 1: Plugin Discovery"
27
+
28
+ PLUGIN_LIST=$(node /app/openclaw.mjs plugins list 2>&1)
29
+ if echo "$PLUGIN_LIST" | grep -q "claude-mem"; then
30
+ pass "Plugin appears in 'plugins list'"
31
+ else
32
+ fail "Plugin NOT found in 'plugins list'"
33
+ echo "$PLUGIN_LIST"
34
+ fi
35
+
36
+ PLUGIN_INFO=$(node /app/openclaw.mjs plugins info claude-mem 2>&1 || true)
37
+ if echo "$PLUGIN_INFO" | grep -qi "claude-mem"; then
38
+ pass "Plugin info shows claude-mem details"
39
+ else
40
+ fail "Plugin info failed"
41
+ echo "$PLUGIN_INFO"
42
+ fi
43
+
44
+ if echo "$PLUGIN_LIST" | grep -A1 "claude-mem" | grep -qi "enabled\|loaded"; then
45
+ pass "Plugin is enabled"
46
+ else
47
+ if echo "$PLUGIN_INFO" | grep -qi "enabled\|loaded"; then
48
+ pass "Plugin is enabled (via info)"
49
+ else
50
+ fail "Plugin does not appear enabled"
51
+ echo "$PLUGIN_INFO"
52
+ fi
53
+ fi
54
+
55
+ DOCTOR_OUT=$(node /app/openclaw.mjs plugins doctor 2>&1 || true)
56
+ if echo "$DOCTOR_OUT" | grep -qi "no.*issue\|0 issue"; then
57
+ pass "Plugin doctor reports no issues"
58
+ else
59
+ fail "Plugin doctor reports issues"
60
+ echo "$DOCTOR_OUT"
61
+ fi
62
+
63
+ section "Phase 2: Plugin Files"
64
+
65
+ EXTENSIONS_DIR="/home/node/.openclaw/extensions/openclaw-plugin"
66
+ if [ ! -d "$EXTENSIONS_DIR" ]; then
67
+ EXTENSIONS_DIR="/home/node/.openclaw/extensions/claude-mem"
68
+ if [ ! -d "$EXTENSIONS_DIR" ]; then
69
+ FOUND_DIR=$(find /home/node/.openclaw/extensions/ -name "openclaw.plugin.json" -exec dirname {} \; 2>/dev/null | head -1 || true)
70
+ if [ -n "$FOUND_DIR" ]; then
71
+ EXTENSIONS_DIR="$FOUND_DIR"
72
+ fi
73
+ fi
74
+ fi
75
+
76
+ if [ -d "$EXTENSIONS_DIR" ]; then
77
+ pass "Plugin directory exists: $EXTENSIONS_DIR"
78
+ else
79
+ fail "Plugin directory not found under /home/node/.openclaw/extensions/"
80
+ ls -la /home/node/.openclaw/extensions/ 2>/dev/null || echo " (extensions dir not found)"
81
+ fi
82
+
83
+ for FILE in "openclaw.plugin.json" "dist/index.js" "package.json"; do
84
+ if [ -f "$EXTENSIONS_DIR/$FILE" ]; then
85
+ pass "File exists: $FILE"
86
+ else
87
+ fail "File missing: $FILE"
88
+ fi
89
+ done
90
+
91
+ section "Phase 3: Mock Worker + Plugin Integration"
92
+
93
+ echo " Starting mock claude-mem worker..."
94
+ node /app/mock-worker.js &
95
+ MOCK_PID=$!
96
+
97
+ for i in $(seq 1 10); do
98
+ if curl -sf http://localhost:37777/health > /dev/null 2>&1; then
99
+ break
100
+ fi
101
+ sleep 0.5
102
+ done
103
+
104
+ if curl -sf http://localhost:37777/health > /dev/null 2>&1; then
105
+ pass "Mock worker health check passed"
106
+ else
107
+ fail "Mock worker health check failed"
108
+ kill $MOCK_PID 2>/dev/null || true
109
+ fi
110
+
111
+ SSE_TEST=$(curl -s --max-time 2 http://localhost:37777/stream 2>/dev/null || true)
112
+ if echo "$SSE_TEST" | grep -q "connected"; then
113
+ pass "SSE stream returns connected event"
114
+ else
115
+ fail "SSE stream did not return connected event"
116
+ echo " Got: $(echo "$SSE_TEST" | head -5)"
117
+ fi
118
+
119
+ section "Phase 4: Gateway Startup with Plugin"
120
+
121
+ mkdir -p /home/node/.openclaw
122
+ cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG'
123
+ {
124
+ "gateway": {
125
+ "mode": "local",
126
+ "auth": {
127
+ "mode": "token",
128
+ "token": "e2e-test-token"
129
+ }
130
+ },
131
+ "plugins": {
132
+ "slots": {
133
+ "memory": "claude-mem"
134
+ },
135
+ "entries": {
136
+ "claude-mem": {
137
+ "enabled": true,
138
+ "config": {
139
+ "workerPort": 37777,
140
+ "observationFeed": {
141
+ "enabled": true,
142
+ "channel": "telegram",
143
+ "to": "test-chat-id-12345"
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ EOFCONFIG
151
+
152
+ pass "OpenClaw config written with plugin enabled"
153
+
154
+ GATEWAY_LOG="/tmp/gateway.log"
155
+ echo " Starting OpenClaw gateway (timeout 15s)..."
156
+ OPENCLAW_GATEWAY_TOKEN=e2e-test-token timeout 15 node /app/openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token > "$GATEWAY_LOG" 2>&1 &
157
+ GATEWAY_PID=$!
158
+
159
+ sleep 5
160
+
161
+ if kill -0 $GATEWAY_PID 2>/dev/null; then
162
+ pass "Gateway process is running"
163
+ else
164
+ fail "Gateway process exited early"
165
+ echo " Gateway log:"
166
+ cat "$GATEWAY_LOG" 2>/dev/null | tail -30
167
+ fi
168
+
169
+ if grep -qi "claude-mem" "$GATEWAY_LOG" 2>/dev/null; then
170
+ pass "Gateway log mentions claude-mem plugin"
171
+ else
172
+ fail "Gateway log does not mention claude-mem"
173
+ echo " Gateway log (last 20 lines):"
174
+ tail -20 "$GATEWAY_LOG" 2>/dev/null
175
+ fi
176
+
177
+ if grep -q "plugin loaded" "$GATEWAY_LOG" 2>/dev/null || grep -q "v1.0.0" "$GATEWAY_LOG" 2>/dev/null; then
178
+ pass "Plugin load message found in gateway log"
179
+ else
180
+ fail "Plugin load message not found"
181
+ fi
182
+
183
+ if grep -qi "observation feed" "$GATEWAY_LOG" 2>/dev/null; then
184
+ pass "Observation feed activity in gateway log"
185
+ else
186
+ fail "No observation feed activity detected"
187
+ fi
188
+
189
+ if grep -qi "connected.*SSE\|SSE.*stream\|connecting.*SSE" "$GATEWAY_LOG" 2>/dev/null; then
190
+ pass "SSE connection activity detected"
191
+ else
192
+ fail "No SSE connection activity in log"
193
+ fi
194
+
195
+ section "Cleanup"
196
+ kill $GATEWAY_PID 2>/dev/null || true
197
+ kill $MOCK_PID 2>/dev/null || true
198
+ wait $GATEWAY_PID 2>/dev/null || true
199
+ wait $MOCK_PID 2>/dev/null || true
200
+ echo " Processes stopped."
201
+
202
+ echo ""
203
+ echo "==============================="
204
+ echo " E2E Test Results"
205
+ echo "==============================="
206
+ echo " Total: $TOTAL"
207
+ echo " Passed: $PASS"
208
+ echo " Failed: $FAIL"
209
+ echo "==============================="
210
+
211
+ if [ "$FAIL" -gt 0 ]; then
212
+ echo ""
213
+ echo " SOME TESTS FAILED"
214
+ echo ""
215
+ echo " Full gateway log:"
216
+ cat "$GATEWAY_LOG" 2>/dev/null
217
+ exit 1
218
+ fi
219
+
220
+ echo ""
221
+ echo " ALL TESTS PASSED"
222
+ exit 0