@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,1092 @@
1
+
2
+
3
+ interface PluginLogger {
4
+ debug?: (message: string) => void;
5
+ info: (message: string) => void;
6
+ warn: (message: string) => void;
7
+ error: (message: string) => void;
8
+ }
9
+
10
+ interface PluginServiceContext {
11
+ config: Record<string, unknown>;
12
+ workspaceDir?: string;
13
+ stateDir: string;
14
+ logger: PluginLogger;
15
+ }
16
+
17
+ interface PluginCommandContext {
18
+ senderId?: string;
19
+ channel: string;
20
+ isAuthorizedSender: boolean;
21
+ args?: string;
22
+ commandBody: string;
23
+ config: Record<string, unknown>;
24
+ }
25
+
26
+ type PluginCommandResult = string | { text: string } | { text: string; format?: string };
27
+
28
+ interface BeforeAgentStartEvent {
29
+ prompt?: string;
30
+ }
31
+
32
+ interface BeforePromptBuildEvent {
33
+ prompt: string;
34
+ messages: unknown[];
35
+ }
36
+
37
+ interface BeforePromptBuildResult {
38
+ systemPrompt?: string;
39
+ prependContext?: string;
40
+ prependSystemContext?: string;
41
+ appendSystemContext?: string;
42
+ }
43
+
44
+ interface ToolResultPersistEvent {
45
+ toolName?: string;
46
+ params?: Record<string, unknown>;
47
+ message?: {
48
+ content?: Array<{ type: string; text?: string }>;
49
+ };
50
+ }
51
+
52
+ interface AgentEndEvent {
53
+ messages?: Array<{
54
+ role: string;
55
+ content: string | Array<{ type: string; text?: string }>;
56
+ }>;
57
+ }
58
+
59
+ interface SessionStartEvent {
60
+ sessionId: string;
61
+ resumedFrom?: string;
62
+ }
63
+
64
+ interface AfterCompactionEvent {
65
+ messageCount: number;
66
+ tokenCount?: number;
67
+ compactedCount: number;
68
+ }
69
+
70
+ interface SessionEndEvent {
71
+ sessionId: string;
72
+ messageCount: number;
73
+ durationMs?: number;
74
+ }
75
+
76
+ interface MessageReceivedEvent {
77
+ from: string;
78
+ content: string;
79
+ timestamp?: number;
80
+ metadata?: Record<string, unknown>;
81
+ }
82
+
83
+ interface EventContext {
84
+ sessionKey?: string;
85
+ workspaceDir?: string;
86
+ agentId?: string;
87
+ }
88
+
89
+ interface MessageContext {
90
+ channelId: string;
91
+ accountId?: string;
92
+ conversationId?: string;
93
+ }
94
+
95
+ type EventCallback<T> = (event: T, ctx: EventContext) => void | Promise<void>;
96
+ type PromptBuildCallback = (event: BeforePromptBuildEvent, ctx: EventContext) => BeforePromptBuildResult | Promise<BeforePromptBuildResult | void> | void;
97
+ type MessageEventCallback<T> = (event: T, ctx: MessageContext) => void | Promise<void>;
98
+
99
+ interface OpenClawPluginApi {
100
+ id: string;
101
+ name: string;
102
+ version?: string;
103
+ source: string;
104
+ config: Record<string, unknown>;
105
+ pluginConfig?: Record<string, unknown>;
106
+ logger: PluginLogger;
107
+ registerService: (service: {
108
+ id: string;
109
+ start: (ctx: PluginServiceContext) => void | Promise<void>;
110
+ stop?: (ctx: PluginServiceContext) => void | Promise<void>;
111
+ }) => void;
112
+ registerCommand: (command: {
113
+ name: string;
114
+ description: string;
115
+ acceptsArgs?: boolean;
116
+ requireAuth?: boolean;
117
+ handler: (ctx: PluginCommandContext) => PluginCommandResult | Promise<PluginCommandResult>;
118
+ }) => void;
119
+ on: ((event: "before_prompt_build", callback: PromptBuildCallback) => void) &
120
+ ((event: "before_agent_start", callback: EventCallback<BeforeAgentStartEvent>) => void) &
121
+ ((event: "tool_result_persist", callback: EventCallback<ToolResultPersistEvent>) => void) &
122
+ ((event: "agent_end", callback: EventCallback<AgentEndEvent>) => void) &
123
+ ((event: "session_start", callback: EventCallback<SessionStartEvent>) => void) &
124
+ ((event: "session_end", callback: EventCallback<SessionEndEvent>) => void) &
125
+ ((event: "message_received", callback: MessageEventCallback<MessageReceivedEvent>) => void) &
126
+ ((event: "after_compaction", callback: EventCallback<AfterCompactionEvent>) => void) &
127
+ ((event: "gateway_start", callback: EventCallback<Record<string, never>>) => void);
128
+ runtime: {
129
+ channel: Record<string, Record<string, (...args: any[]) => Promise<any>>>;
130
+ };
131
+ }
132
+
133
+ interface ObservationSSEPayload {
134
+ id: number;
135
+ memory_session_id: string;
136
+ session_id: string;
137
+ type: string;
138
+ title: string | null;
139
+ subtitle: string | null;
140
+ text: string | null;
141
+ narrative: string | null;
142
+ facts: string | null;
143
+ concepts: string | null;
144
+ files_read: string | null;
145
+ files_modified: string | null;
146
+ project: string | null;
147
+ prompt_number: number;
148
+ created_at_epoch: number;
149
+ }
150
+
151
+ interface SSENewObservationEvent {
152
+ type: "new_observation";
153
+ observation: ObservationSSEPayload;
154
+ timestamp: number;
155
+ }
156
+
157
+ type ConnectionState = "disconnected" | "connected" | "reconnecting";
158
+
159
+ interface FeedEmojiConfig {
160
+ primary?: string;
161
+ claudeCode?: string;
162
+ claudeCodeLabel?: string;
163
+ default?: string;
164
+ agents?: Record<string, string>;
165
+ }
166
+
167
+ interface ClaudeMemPluginConfig {
168
+ syncMemoryFile?: boolean;
169
+ syncMemoryFileExclude?: string[];
170
+ project?: string;
171
+ workerPort?: number;
172
+ workerHost?: string;
173
+ observationFeed?: {
174
+ enabled?: boolean;
175
+ channel?: string;
176
+ to?: string;
177
+ botToken?: string;
178
+ emojis?: FeedEmojiConfig;
179
+ };
180
+ }
181
+
182
+ const MAX_SSE_BUFFER_SIZE = 1024 * 1024;
183
+ const DEFAULT_WORKER_PORT = 37777;
184
+ const DEFAULT_WORKER_HOST = "127.0.0.1";
185
+
186
+ const EMOJI_POOL = [
187
+ "🔧","📐","🔍","💻","🧪","🐛","🛡️","☁️","📦","🎯",
188
+ "🔮","⚡","🌊","🎨","📊","🚀","🔬","🏗️","📝","🎭",
189
+ ];
190
+
191
+ function poolEmojiForAgent(agentId: string): string {
192
+ let hash = 0;
193
+ for (let i = 0; i < agentId.length; i++) {
194
+ hash = ((hash << 5) - hash + agentId.charCodeAt(i)) | 0;
195
+ }
196
+ return EMOJI_POOL[Math.abs(hash) % EMOJI_POOL.length];
197
+ }
198
+
199
+ const DEFAULT_PRIMARY_EMOJI = "🦞";
200
+ const DEFAULT_CLAUDE_CODE_EMOJI = "⌨️";
201
+ const DEFAULT_CLAUDE_CODE_LABEL = "Claude Code Session";
202
+ const DEFAULT_FALLBACK_EMOJI = "🦀";
203
+
204
+ function buildGetSourceLabel(
205
+ emojiConfig: FeedEmojiConfig | undefined
206
+ ): (project: string | null | undefined) => string {
207
+ const primary = emojiConfig?.primary ?? DEFAULT_PRIMARY_EMOJI;
208
+ const claudeCode = emojiConfig?.claudeCode ?? DEFAULT_CLAUDE_CODE_EMOJI;
209
+ const claudeCodeLabel = emojiConfig?.claudeCodeLabel ?? DEFAULT_CLAUDE_CODE_LABEL;
210
+ const fallback = emojiConfig?.default ?? DEFAULT_FALLBACK_EMOJI;
211
+ const pinnedAgents = emojiConfig?.agents ?? {};
212
+
213
+ return function getSourceLabel(project: string | null | undefined): string {
214
+ if (!project) return fallback;
215
+ if (project.startsWith("openclaw-")) {
216
+ const agentId = project.slice("openclaw-".length);
217
+ if (!agentId) return `${primary} openclaw`;
218
+ const emoji = pinnedAgents[agentId] || poolEmojiForAgent(agentId);
219
+ return `${emoji} ${agentId}`;
220
+ }
221
+ if (project === "openclaw") {
222
+ return `${primary} openclaw`;
223
+ }
224
+ const trimmedLabel = claudeCodeLabel.trim();
225
+ if (!trimmedLabel) {
226
+ return `${claudeCode} ${project}`;
227
+ }
228
+ return `${claudeCode} ${trimmedLabel} (${project})`;
229
+ };
230
+ }
231
+
232
+ let _workerHost = DEFAULT_WORKER_HOST;
233
+
234
+ function workerBaseUrl(port: number): string {
235
+ return `http://${_workerHost}:${port}`;
236
+ }
237
+
238
+ const CIRCUIT_BREAKER_THRESHOLD = 3;
239
+ const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
240
+
241
+ type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
242
+
243
+ let _circuitState: CircuitState = "CLOSED";
244
+ let _circuitFailures = 0;
245
+ let _circuitOpenedAt = 0;
246
+ let _halfOpenProbeInFlight = false;
247
+
248
+ function circuitAllow(logger: PluginLogger): boolean {
249
+ if (_circuitState === "CLOSED") return true;
250
+ if (_circuitState === "OPEN") {
251
+ if (Date.now() - _circuitOpenedAt >= CIRCUIT_BREAKER_COOLDOWN_MS) {
252
+ _circuitState = "HALF_OPEN";
253
+ logger.info("[claude-mem] Circuit breaker: probing worker connection");
254
+ if (_halfOpenProbeInFlight) return false;
255
+ _halfOpenProbeInFlight = true;
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ if (_halfOpenProbeInFlight) return false;
261
+ _halfOpenProbeInFlight = true;
262
+ return true;
263
+ }
264
+
265
+ function circuitOnSuccess(logger: PluginLogger): void {
266
+ if (_circuitState !== "CLOSED") {
267
+ logger.info("[claude-mem] Worker connection restored — circuit closed");
268
+ }
269
+ _circuitState = "CLOSED";
270
+ _circuitFailures = 0;
271
+ _halfOpenProbeInFlight = false;
272
+ }
273
+
274
+ function circuitOnFailure(logger: PluginLogger): void {
275
+ _halfOpenProbeInFlight = false;
276
+ _circuitFailures++;
277
+ if (
278
+ _circuitState === "HALF_OPEN" ||
279
+ (_circuitState === "CLOSED" && _circuitFailures >= CIRCUIT_BREAKER_THRESHOLD)
280
+ ) {
281
+ _circuitState = "OPEN";
282
+ _circuitOpenedAt = Date.now();
283
+ logger.warn(
284
+ `[claude-mem] Worker unreachable — disabling requests for ${CIRCUIT_BREAKER_COOLDOWN_MS / 1000}s`
285
+ );
286
+ }
287
+ }
288
+
289
+ function circuitReset(): void {
290
+ _circuitState = "CLOSED";
291
+ _circuitFailures = 0;
292
+ _circuitOpenedAt = 0;
293
+ _halfOpenProbeInFlight = false;
294
+ }
295
+
296
+ async function workerPost(
297
+ port: number,
298
+ path: string,
299
+ body: Record<string, unknown>,
300
+ logger: PluginLogger
301
+ ): Promise<Record<string, unknown> | null> {
302
+ if (!circuitAllow(logger)) return null;
303
+ try {
304
+ const response = await fetch(`${workerBaseUrl(port)}${path}`, {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify(body),
308
+ });
309
+ if (!response.ok) {
310
+ circuitOnFailure(logger);
311
+ logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
312
+ return null;
313
+ }
314
+ circuitOnSuccess(logger);
315
+ return (await response.json()) as Record<string, unknown>;
316
+ } catch (error: unknown) {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ circuitOnFailure(logger);
319
+ if (_circuitState !== "OPEN") {
320
+ logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
321
+ }
322
+ return null;
323
+ }
324
+ }
325
+
326
+ function workerPostFireAndForget(
327
+ port: number,
328
+ path: string,
329
+ body: Record<string, unknown>,
330
+ logger: PluginLogger
331
+ ): void {
332
+ if (!circuitAllow(logger)) return;
333
+ fetch(`${workerBaseUrl(port)}${path}`, {
334
+ method: "POST",
335
+ headers: { "Content-Type": "application/json" },
336
+ body: JSON.stringify(body),
337
+ }).then((response) => {
338
+ if (!response.ok) {
339
+ circuitOnFailure(logger);
340
+ logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
341
+ return;
342
+ }
343
+ circuitOnSuccess(logger);
344
+ }).catch((error: unknown) => {
345
+ const message = error instanceof Error ? error.message : String(error);
346
+ circuitOnFailure(logger);
347
+ if (_circuitState !== "OPEN") {
348
+ logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
349
+ }
350
+ });
351
+ }
352
+
353
+ async function workerGetText(
354
+ port: number,
355
+ path: string,
356
+ logger: PluginLogger
357
+ ): Promise<string | null> {
358
+ if (!circuitAllow(logger)) return null;
359
+ try {
360
+ const response = await fetch(`${workerBaseUrl(port)}${path}`);
361
+ if (!response.ok) {
362
+ circuitOnFailure(logger);
363
+ logger.warn(`[claude-mem] Worker GET ${path} returned ${response.status}`);
364
+ return null;
365
+ }
366
+ circuitOnSuccess(logger);
367
+ return await response.text();
368
+ } catch (error: unknown) {
369
+ const message = error instanceof Error ? error.message : String(error);
370
+ circuitOnFailure(logger);
371
+ if (_circuitState !== "OPEN") {
372
+ logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`);
373
+ }
374
+ return null;
375
+ }
376
+ }
377
+
378
+ async function workerGetJson(
379
+ port: number,
380
+ path: string,
381
+ logger: PluginLogger
382
+ ): Promise<Record<string, unknown> | null> {
383
+ const text = await workerGetText(port, path, logger);
384
+ if (!text) return null;
385
+
386
+ try {
387
+ return JSON.parse(text) as Record<string, unknown>;
388
+ } catch {
389
+ logger.warn(`[claude-mem] Worker GET ${path} returned non-JSON response`);
390
+ return null;
391
+ }
392
+ }
393
+
394
+ function formatObservationMessage(
395
+ observation: ObservationSSEPayload,
396
+ getSourceLabel: (project: string | null | undefined) => string,
397
+ ): string {
398
+ const title = observation.title || "Untitled";
399
+ const source = getSourceLabel(observation.project);
400
+ let message = `${source}\n**${title}**`;
401
+ if (observation.subtitle) {
402
+ message += `\n${observation.subtitle}`;
403
+ }
404
+ return message;
405
+ }
406
+
407
+ const CHANNEL_SEND_MAP: Record<string, { namespace: string; functionName: string }> = {
408
+ telegram: { namespace: "telegram", functionName: "sendMessageTelegram" },
409
+ whatsapp: { namespace: "whatsapp", functionName: "sendMessageWhatsApp" },
410
+ discord: { namespace: "discord", functionName: "sendMessageDiscord" },
411
+ slack: { namespace: "slack", functionName: "sendMessageSlack" },
412
+ signal: { namespace: "signal", functionName: "sendMessageSignal" },
413
+ imessage: { namespace: "imessage", functionName: "sendMessageIMessage" },
414
+ line: { namespace: "line", functionName: "sendMessageLine" },
415
+ };
416
+
417
+ async function sendDirectTelegram(
418
+ botToken: string,
419
+ chatId: string,
420
+ text: string,
421
+ logger: PluginLogger
422
+ ): Promise<void> {
423
+ try {
424
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/json" },
427
+ body: JSON.stringify({
428
+ chat_id: chatId,
429
+ text,
430
+ parse_mode: "Markdown",
431
+ }),
432
+ });
433
+ if (!response.ok) {
434
+ const body = await response.text();
435
+ logger.warn(`[claude-mem] Direct Telegram send failed (${response.status}): ${body}`);
436
+ }
437
+ } catch (error: unknown) {
438
+ const message = error instanceof Error ? error.message : String(error);
439
+ logger.warn(`[claude-mem] Direct Telegram send error: ${message}`);
440
+ }
441
+ }
442
+
443
+ function sendToChannel(
444
+ api: OpenClawPluginApi,
445
+ channel: string,
446
+ to: string,
447
+ text: string,
448
+ botToken?: string
449
+ ): Promise<void> {
450
+ if (botToken && channel === "telegram") {
451
+ return sendDirectTelegram(botToken, to, text, api.logger);
452
+ }
453
+
454
+ const mapping = CHANNEL_SEND_MAP[channel];
455
+ if (!mapping) {
456
+ api.logger.warn(`[claude-mem] Unsupported channel type: ${channel}`);
457
+ return Promise.resolve();
458
+ }
459
+
460
+ const channelApi = api.runtime.channel[mapping.namespace];
461
+ if (!channelApi) {
462
+ api.logger.warn(`[claude-mem] Channel "${channel}" not available in runtime`);
463
+ return Promise.resolve();
464
+ }
465
+
466
+ const senderFunction = channelApi[mapping.functionName];
467
+ if (!senderFunction) {
468
+ api.logger.warn(`[claude-mem] Channel "${channel}" has no ${mapping.functionName} function`);
469
+ return Promise.resolve();
470
+ }
471
+
472
+ const args: unknown[] = channel === "whatsapp"
473
+ ? [to, text, { verbose: false }]
474
+ : [to, text];
475
+
476
+ return senderFunction(...args).catch((error: unknown) => {
477
+ const message = error instanceof Error ? error.message : String(error);
478
+ api.logger.error(`[claude-mem] Failed to send to ${channel}: ${message}`);
479
+ });
480
+ }
481
+
482
+ async function connectToSSEStream(
483
+ api: OpenClawPluginApi,
484
+ port: number,
485
+ channel: string,
486
+ to: string,
487
+ abortController: AbortController,
488
+ setConnectionState: (state: ConnectionState) => void,
489
+ getSourceLabel: (project: string | null | undefined) => string,
490
+ botToken?: string
491
+ ): Promise<void> {
492
+ let backoffMs = 1000;
493
+ const maxBackoffMs = 30000;
494
+
495
+ while (!abortController.signal.aborted) {
496
+ try {
497
+ setConnectionState("reconnecting");
498
+ api.logger.info(`[claude-mem] Connecting to SSE stream at ${workerBaseUrl(port)}/stream`);
499
+
500
+ const response = await fetch(`${workerBaseUrl(port)}/stream`, {
501
+ signal: abortController.signal,
502
+ headers: { Accept: "text/event-stream" },
503
+ });
504
+
505
+ if (!response.ok) {
506
+ throw new Error(`SSE stream returned HTTP ${response.status}`);
507
+ }
508
+
509
+ if (!response.body) {
510
+ throw new Error("SSE stream response has no body");
511
+ }
512
+
513
+ setConnectionState("connected");
514
+ backoffMs = 1000;
515
+ api.logger.info("[claude-mem] Connected to SSE stream");
516
+
517
+ const reader = response.body.getReader();
518
+ const decoder = new TextDecoder();
519
+ let buffer = "";
520
+
521
+ while (true) {
522
+ const { done, value } = await reader.read();
523
+ if (done) break;
524
+
525
+ buffer += decoder.decode(value, { stream: true });
526
+
527
+ if (buffer.length > MAX_SSE_BUFFER_SIZE) {
528
+ api.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer");
529
+ buffer = "";
530
+ }
531
+
532
+ const frames = buffer.split("\n\n");
533
+ buffer = frames.pop() || "";
534
+
535
+ for (const frame of frames) {
536
+ const dataLines = frame
537
+ .split("\n")
538
+ .filter((line) => line.startsWith("data:"))
539
+ .map((line) => line.slice(5).trim());
540
+ if (dataLines.length === 0) continue;
541
+
542
+ const jsonStr = dataLines.join("\n");
543
+ if (!jsonStr) continue;
544
+
545
+ try {
546
+ const parsed = JSON.parse(jsonStr);
547
+ if (parsed.type === "new_observation" && parsed.observation) {
548
+ const event = parsed as SSENewObservationEvent;
549
+ const message = formatObservationMessage(event.observation, getSourceLabel);
550
+ await sendToChannel(api, channel, to, message, botToken);
551
+ }
552
+ } catch (parseError: unknown) {
553
+ const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
554
+ api.logger.warn(`[claude-mem] Failed to parse SSE frame: ${errorMessage}`);
555
+ }
556
+ }
557
+ }
558
+ } catch (error: unknown) {
559
+ if (abortController.signal.aborted) {
560
+ break;
561
+ }
562
+ setConnectionState("reconnecting");
563
+ const errorMessage = error instanceof Error ? error.message : String(error);
564
+ api.logger.warn(`[claude-mem] SSE stream error: ${errorMessage}. Reconnecting in ${backoffMs / 1000}s`);
565
+ }
566
+
567
+ if (abortController.signal.aborted) break;
568
+
569
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
570
+ backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
571
+ }
572
+
573
+ setConnectionState("disconnected");
574
+ }
575
+
576
+ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
577
+ const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
578
+ const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
579
+ _workerHost = userConfig.workerHost || DEFAULT_WORKER_HOST;
580
+ const baseProjectName = userConfig.project || "openclaw";
581
+ const getSourceLabel = buildGetSourceLabel(userConfig.observationFeed?.emojis);
582
+
583
+ function getProjectName(ctx: EventContext): string {
584
+ if (ctx.agentId) {
585
+ return `openclaw-${ctx.agentId}`;
586
+ }
587
+ return baseProjectName;
588
+ }
589
+
590
+ const sessionIds = new Map<string, string>();
591
+ const canonicalSessionKeys = new Map<string, string>();
592
+ const sessionAliasesByCanonicalKey = new Map<string, Set<string>>();
593
+ const recentPromptInits = new Map<string, number>();
594
+ const syncMemoryFile = userConfig.syncMemoryFile !== false;
595
+ const syncMemoryFileExclude = new Set(userConfig.syncMemoryFileExclude || []);
596
+
597
+ function getContentSessionId(sessionKey?: string): string {
598
+ const key = sessionKey || "default";
599
+ if (!sessionIds.has(key)) {
600
+ sessionIds.set(key, `openclaw-${key}-${Date.now()}`);
601
+ }
602
+ return sessionIds.get(key)!;
603
+ }
604
+
605
+ function shouldInjectContext(ctx?: EventContext): boolean {
606
+ if (!syncMemoryFile) return false;
607
+ const agentId = ctx?.agentId;
608
+ if (agentId && syncMemoryFileExclude.has(agentId)) return false;
609
+ return true;
610
+ }
611
+
612
+ type SessionTrackingContext = {
613
+ sessionKey?: string;
614
+ workspaceDir?: string;
615
+ channelId?: string;
616
+ conversationId?: string;
617
+ };
618
+
619
+ function getSessionAliases(ctx: SessionTrackingContext): string[] {
620
+ const aliases = new Set<string>();
621
+ for (const rawKey of [ctx.sessionKey, ctx.conversationId, ctx.channelId]) {
622
+ const key = typeof rawKey === "string" ? rawKey.trim() : "";
623
+ if (key) aliases.add(key);
624
+ }
625
+ if (aliases.size === 0) aliases.add("default");
626
+ return Array.from(aliases);
627
+ }
628
+
629
+ function rememberSessionContext(ctx: SessionTrackingContext): { canonicalKey: string; contentSessionId: string } {
630
+ const aliases = getSessionAliases(ctx);
631
+ let canonicalKey = aliases.find((alias) => canonicalSessionKeys.has(alias));
632
+ canonicalKey = canonicalKey ? canonicalSessionKeys.get(canonicalKey)! : aliases[0];
633
+ let aliasSet = sessionAliasesByCanonicalKey.get(canonicalKey);
634
+ if (!aliasSet) {
635
+ aliasSet = new Set([canonicalKey]);
636
+ sessionAliasesByCanonicalKey.set(canonicalKey, aliasSet);
637
+ }
638
+ for (const alias of aliases) {
639
+ aliasSet.add(alias);
640
+ canonicalSessionKeys.set(alias, canonicalKey);
641
+ }
642
+ const contentSessionId = getContentSessionId(canonicalKey);
643
+ for (const alias of aliasSet) {
644
+ sessionIds.set(alias, contentSessionId);
645
+ }
646
+ return { canonicalKey, contentSessionId };
647
+ }
648
+
649
+ function shouldSkipDuplicatePromptInit(contentSessionId: string, project: string, prompt: string): boolean {
650
+ const now = Date.now();
651
+ for (const [key, timestamp] of recentPromptInits) {
652
+ if (now - timestamp > 2000) recentPromptInits.delete(key);
653
+ }
654
+ const cacheKey = `${contentSessionId}::${project}::${prompt}`;
655
+ const lastSeenAt = recentPromptInits.get(cacheKey);
656
+ recentPromptInits.set(cacheKey, now);
657
+ return typeof lastSeenAt === "number" && now - lastSeenAt <= 2000;
658
+ }
659
+
660
+ function clearSessionContext(ctx: SessionTrackingContext): void {
661
+ const aliases = getSessionAliases(ctx);
662
+ const canonicalKey = aliases
663
+ .map((alias) => canonicalSessionKeys.get(alias))
664
+ .find(Boolean) || aliases[0];
665
+ const knownAliases = sessionAliasesByCanonicalKey.get(canonicalKey) || new Set([canonicalKey, ...aliases]);
666
+ for (const alias of knownAliases) {
667
+ canonicalSessionKeys.delete(alias);
668
+ sessionIds.delete(alias);
669
+ }
670
+ sessionAliasesByCanonicalKey.delete(canonicalKey);
671
+ sessionIds.delete(canonicalKey);
672
+ }
673
+
674
+ const CONTEXT_CACHE_TTL_MS = 60_000;
675
+ const contextCache = new Map<string, { text: string; fetchedAt: number }>();
676
+
677
+ async function getContextForPrompt(ctx?: EventContext): Promise<string | null> {
678
+ const projects = [baseProjectName];
679
+ const agentProject = ctx ? getProjectName(ctx) : null;
680
+ if (agentProject && agentProject !== baseProjectName) {
681
+ projects.push(agentProject);
682
+ }
683
+ const cacheKey = projects.join(",");
684
+
685
+ const cached = contextCache.get(cacheKey);
686
+ if (cached && Date.now() - cached.fetchedAt < CONTEXT_CACHE_TTL_MS) {
687
+ return cached.text;
688
+ }
689
+
690
+ const contextText = await workerGetText(
691
+ workerPort,
692
+ `/api/context/inject?projects=${encodeURIComponent(cacheKey)}`,
693
+ api.logger
694
+ );
695
+ if (contextText && contextText.trim().length > 0) {
696
+ const trimmed = contextText.trim();
697
+ contextCache.set(cacheKey, { text: trimmed, fetchedAt: Date.now() });
698
+ return trimmed;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ // Centralized session-init POST. session_start, after_compaction, and
704
+ // before_agent_start each call this; the 2s dedup guard
705
+ // (shouldSkipDuplicatePromptInit) collapses the redundant inits a single
706
+ // user-message flow produces into one prompt record, while still ensuring a
707
+ // session is initialized even on flows that never reach before_agent_start.
708
+ async function initSessionOnce(ctx: EventContext, promptText: string, via: string): Promise<void> {
709
+ const { contentSessionId } = rememberSessionContext(ctx);
710
+ const projectName = getProjectName(ctx);
711
+
712
+ if (shouldSkipDuplicatePromptInit(contentSessionId, projectName, promptText)) {
713
+ api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName} via=${via}`);
714
+ return;
715
+ }
716
+
717
+ await workerPost(workerPort, "/api/sessions/init", {
718
+ contentSessionId,
719
+ project: projectName,
720
+ prompt: promptText,
721
+ }, api.logger);
722
+
723
+ api.logger.info(`[claude-mem] Session initialized via ${via}: contentSessionId=${contentSessionId} project=${projectName}`);
724
+ }
725
+
726
+ api.on("session_start", async (_event, ctx) => {
727
+ await initSessionOnce(ctx, "session start", "session_start");
728
+ });
729
+
730
+ api.on("message_received", async (event, ctx) => {
731
+ const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
732
+ api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
733
+ });
734
+
735
+ api.on("after_compaction", async (_event, ctx) => {
736
+ await initSessionOnce(ctx, "after compaction", "after_compaction");
737
+ });
738
+
739
+ api.on("before_agent_start", async (event, ctx) => {
740
+ await initSessionOnce(ctx, event.prompt || "agent run", "before_agent_start");
741
+ });
742
+
743
+ api.on("before_prompt_build", async (_event, ctx) => {
744
+ if (!shouldInjectContext(ctx)) return;
745
+
746
+ const contextText = await getContextForPrompt(ctx);
747
+ if (contextText) {
748
+ api.logger.info(`[claude-mem] Context injected via system prompt for agent=${ctx.agentId ?? "unknown"}`);
749
+ return { appendSystemContext: contextText };
750
+ }
751
+ });
752
+
753
+ api.on("tool_result_persist", (event, ctx) => {
754
+ api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
755
+ const toolName = event.toolName;
756
+ if (!toolName) return;
757
+
758
+ if (toolName.startsWith("memory_")) return;
759
+
760
+ const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
761
+
762
+ let toolResponseText = "";
763
+ const content = event.message?.content;
764
+ if (Array.isArray(content)) {
765
+ toolResponseText = content
766
+ .filter((block) => (block.type === "tool_result" || block.type === "text") && "text" in block)
767
+ .map((block) => String(block.text))
768
+ .join("\n");
769
+ }
770
+
771
+ const MAX_TOOL_RESPONSE_LENGTH = 1000;
772
+ if (toolResponseText.length > MAX_TOOL_RESPONSE_LENGTH) {
773
+ toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
774
+ }
775
+
776
+ // Fall back to the process cwd when the event carries no workspaceDir, so a
777
+ // missing ctx field never silently drops a captured observation.
778
+ const workspaceDir = ctx.workspaceDir || process.cwd();
779
+ if (!ctx.workspaceDir) {
780
+ api.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${canonicalKey} tool=${toolName}`);
781
+ }
782
+
783
+ workerPostFireAndForget(workerPort, "/api/sessions/observations", {
784
+ contentSessionId,
785
+ tool_name: toolName,
786
+ tool_input: event.params || {},
787
+ tool_response: toolResponseText,
788
+ cwd: workspaceDir,
789
+ }, api.logger);
790
+ });
791
+
792
+ api.on("agent_end", async (event, ctx) => {
793
+ const { contentSessionId } = rememberSessionContext(ctx);
794
+
795
+ let lastAssistantMessage = "";
796
+ if (Array.isArray(event.messages)) {
797
+ for (let i = event.messages.length - 1; i >= 0; i--) {
798
+ const message = event.messages[i];
799
+ if (message?.role === "assistant") {
800
+ if (typeof message.content === "string") {
801
+ lastAssistantMessage = message.content;
802
+ } else if (Array.isArray(message.content)) {
803
+ lastAssistantMessage = message.content
804
+ .filter((block) => block.type === "text")
805
+ .map((block) => block.text || "")
806
+ .join("\n");
807
+ }
808
+ break;
809
+ }
810
+ }
811
+ }
812
+
813
+ await workerPost(workerPort, "/api/sessions/summarize", {
814
+ contentSessionId,
815
+ last_assistant_message: lastAssistantMessage,
816
+ }, api.logger);
817
+ });
818
+
819
+ api.on("session_end", async (_event, ctx) => {
820
+ clearSessionContext(ctx);
821
+ api.logger.info(`[claude-mem] Session tracking cleaned up`);
822
+ });
823
+
824
+ api.on("gateway_start", async () => {
825
+ circuitReset();
826
+ sessionIds.clear();
827
+ contextCache.clear();
828
+ recentPromptInits.clear();
829
+ canonicalSessionKeys.clear();
830
+ sessionAliasesByCanonicalKey.clear();
831
+ api.logger.info("[claude-mem] Gateway started — session tracking reset");
832
+ });
833
+
834
+ let sseAbortController: AbortController | null = null;
835
+ let connectionState: ConnectionState = "disconnected";
836
+ let connectionPromise: Promise<void> | null = null;
837
+
838
+ api.registerService({
839
+ id: "claude-mem-observation-feed",
840
+ start: async (_ctx) => {
841
+ if (sseAbortController) {
842
+ sseAbortController.abort();
843
+ if (connectionPromise) {
844
+ await connectionPromise;
845
+ connectionPromise = null;
846
+ }
847
+ }
848
+
849
+ const feedConfig = userConfig.observationFeed;
850
+
851
+ if (!feedConfig?.enabled) {
852
+ api.logger.info("[claude-mem] Observation feed disabled");
853
+ return;
854
+ }
855
+
856
+ if (!feedConfig.channel || !feedConfig.to) {
857
+ api.logger.warn("[claude-mem] Observation feed misconfigured — channel or target missing");
858
+ return;
859
+ }
860
+
861
+ api.logger.info(`[claude-mem] Observation feed starting — channel: ${feedConfig.channel}, target: ${feedConfig.to}`);
862
+
863
+ sseAbortController = new AbortController();
864
+ connectionPromise = connectToSSEStream(
865
+ api,
866
+ workerPort,
867
+ feedConfig.channel,
868
+ feedConfig.to,
869
+ sseAbortController,
870
+ (state) => { connectionState = state; },
871
+ getSourceLabel,
872
+ feedConfig.botToken
873
+ );
874
+ },
875
+ stop: async (_ctx) => {
876
+ if (sseAbortController) {
877
+ sseAbortController.abort();
878
+ sseAbortController = null;
879
+ }
880
+ if (connectionPromise) {
881
+ await connectionPromise;
882
+ connectionPromise = null;
883
+ }
884
+ connectionState = "disconnected";
885
+ api.logger.info("[claude-mem] Observation feed stopped — SSE connection closed");
886
+ },
887
+ });
888
+
889
+ function summarizeSearchResults(items: unknown[], limit = 5): string {
890
+ if (!Array.isArray(items) || items.length === 0) {
891
+ return "No results found.";
892
+ }
893
+
894
+ return items
895
+ .slice(0, limit)
896
+ .map((item, index) => {
897
+ const row = item as Record<string, unknown>;
898
+ const title = String(row.title || row.subtitle || row.text || "Untitled");
899
+ const project = row.project ? ` [${String(row.project)}]` : "";
900
+ return `${index + 1}. ${title}${project}`;
901
+ })
902
+ .join("\n");
903
+ }
904
+
905
+ function parseLimit(arg: string | undefined, fallback = 10): number {
906
+ const parsed = Number(arg);
907
+ if (!Number.isFinite(parsed)) return fallback;
908
+ return Math.max(1, Math.min(50, Math.trunc(parsed)));
909
+ }
910
+
911
+ api.registerCommand({
912
+ name: "claude_mem_feed",
913
+ description: "Show or toggle Claude-Mem observation feed status",
914
+ acceptsArgs: true,
915
+ handler: async (ctx) => {
916
+ const feedConfig = userConfig.observationFeed;
917
+
918
+ if (!feedConfig) {
919
+ return { text: "Observation feed not configured. Add observationFeed to your plugin config." };
920
+ }
921
+
922
+ const arg = ctx.args?.trim();
923
+
924
+ if (arg === "on") {
925
+ api.logger.info("[claude-mem] Feed enable requested via command");
926
+ return { text: "Feed enable requested. Update observationFeed.enabled in your plugin config to persist." };
927
+ }
928
+
929
+ if (arg === "off") {
930
+ api.logger.info("[claude-mem] Feed disable requested via command");
931
+ return { text: "Feed disable requested. Update observationFeed.enabled in your plugin config to persist." };
932
+ }
933
+
934
+ return { text: [
935
+ "Claude-Mem Observation Feed",
936
+ `Enabled: ${feedConfig.enabled ? "yes" : "no"}`,
937
+ `Channel: ${feedConfig.channel || "not set"}`,
938
+ `Target: ${feedConfig.to || "not set"}`,
939
+ `Connection: ${connectionState}`,
940
+ ].join("\n") };
941
+ },
942
+ });
943
+
944
+ api.registerCommand({
945
+ name: "claude-mem-search",
946
+ description: "Search Claude-Mem observations by query",
947
+ acceptsArgs: true,
948
+ handler: async (ctx) => {
949
+ const raw = ctx.args?.trim() || "";
950
+ if (!raw) {
951
+ return "Usage: /claude-mem-search <query> [limit]";
952
+ }
953
+
954
+ const pieces = raw.split(/\s+/);
955
+ const maybeLimit = pieces[pieces.length - 1];
956
+ const hasTrailingLimit = /^\d+$/.test(maybeLimit);
957
+ const limit = hasTrailingLimit ? parseLimit(maybeLimit, 10) : 10;
958
+ const query = hasTrailingLimit ? pieces.slice(0, -1).join(" ") : raw;
959
+
960
+ const data = await workerGetJson(
961
+ workerPort,
962
+ `/api/search/observations?query=${encodeURIComponent(query)}&limit=${limit}`,
963
+ api.logger,
964
+ );
965
+
966
+ if (!data) {
967
+ return "Claude-Mem search failed (worker unavailable or invalid response).";
968
+ }
969
+
970
+ const items = Array.isArray(data.items) ? data.items : [];
971
+ return [
972
+ `Claude-Mem Search: \"${query}\"`,
973
+ summarizeSearchResults(items, limit),
974
+ ].join("\n");
975
+ },
976
+ });
977
+
978
+ api.registerCommand({
979
+ name: "claude-mem-recent",
980
+ description: "Show recent Claude-Mem context for a project",
981
+ acceptsArgs: true,
982
+ handler: async (ctx) => {
983
+ const raw = ctx.args?.trim() || "";
984
+ const parts = raw ? raw.split(/\s+/) : [];
985
+ const maybeLimit = parts.length > 0 ? parts[parts.length - 1] : "";
986
+ const hasTrailingLimit = /^\d+$/.test(maybeLimit);
987
+ const limit = hasTrailingLimit ? parseLimit(maybeLimit, 3) : 3;
988
+ const project = hasTrailingLimit ? parts.slice(0, -1).join(" ") : raw;
989
+
990
+ const params = new URLSearchParams();
991
+ params.set("limit", String(limit));
992
+ if (project) params.set("project", project);
993
+
994
+ const data = await workerGetJson(
995
+ workerPort,
996
+ `/api/context/recent?${params.toString()}`,
997
+ api.logger,
998
+ );
999
+
1000
+ if (!data) {
1001
+ return "Claude-Mem recent context failed (worker unavailable or invalid response).";
1002
+ }
1003
+
1004
+ const summaries = Array.isArray(data.session_summaries) ? data.session_summaries : [];
1005
+ const observations = Array.isArray(data.recent_observations) ? data.recent_observations : [];
1006
+
1007
+ return [
1008
+ "Claude-Mem Recent Context",
1009
+ `Project: ${project || "(auto)"}`,
1010
+ `Session summaries: ${summaries.length}`,
1011
+ `Recent observations: ${observations.length}`,
1012
+ summarizeSearchResults(observations, Math.min(5, observations.length || 5)),
1013
+ ].join("\n");
1014
+ },
1015
+ });
1016
+
1017
+ api.registerCommand({
1018
+ name: "claude-mem-timeline",
1019
+ description: "Find best memory match and show nearby timeline events",
1020
+ acceptsArgs: true,
1021
+ handler: async (ctx) => {
1022
+ const raw = ctx.args?.trim() || "";
1023
+ if (!raw) {
1024
+ return "Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";
1025
+ }
1026
+
1027
+ const parts = raw.split(/\s+/);
1028
+ let depthAfter = 5;
1029
+ let depthBefore = 5;
1030
+
1031
+ if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) {
1032
+ depthAfter = parseLimit(parts.pop(), 5);
1033
+ }
1034
+ if (parts.length >= 2 && /^\d+$/.test(parts[parts.length - 1])) {
1035
+ depthBefore = parseLimit(parts.pop(), 5);
1036
+ }
1037
+
1038
+ const query = parts.join(" ");
1039
+ const params = new URLSearchParams({
1040
+ query,
1041
+ mode: "auto",
1042
+ depth_before: String(depthBefore),
1043
+ depth_after: String(depthAfter),
1044
+ });
1045
+
1046
+ const data = await workerGetJson(
1047
+ workerPort,
1048
+ `/api/timeline/by-query?${params.toString()}`,
1049
+ api.logger,
1050
+ );
1051
+
1052
+ if (!data) {
1053
+ return "Claude-Mem timeline lookup failed (worker unavailable or invalid response).";
1054
+ }
1055
+
1056
+ const timeline = Array.isArray(data.timeline) ? data.timeline : [];
1057
+ const anchor = data.anchor ? String(data.anchor) : "(none)";
1058
+
1059
+ return [
1060
+ `Claude-Mem Timeline: \"${query}\"`,
1061
+ `Anchor: ${anchor}`,
1062
+ summarizeSearchResults(timeline, 8),
1063
+ ].join("\n");
1064
+ },
1065
+ });
1066
+
1067
+ api.registerCommand({
1068
+ name: "claude_mem_status",
1069
+ description: "Check Claude-Mem worker health and session status",
1070
+ handler: async () => {
1071
+ const healthText = await workerGetText(workerPort, "/api/health", api.logger);
1072
+ if (!healthText) {
1073
+ return { text: `Claude-Mem worker unreachable at port ${workerPort}` };
1074
+ }
1075
+
1076
+ try {
1077
+ const health = JSON.parse(healthText);
1078
+ return { text: [
1079
+ "Claude-Mem Worker Status",
1080
+ `Status: ${health.status || "unknown"}`,
1081
+ `Port: ${workerPort}`,
1082
+ `Active sessions: ${sessionIds.size}`,
1083
+ `Observation feed: ${connectionState}`,
1084
+ ].join("\n") };
1085
+ } catch {
1086
+ return { text: `Claude-Mem worker responded but returned unexpected data` };
1087
+ }
1088
+ },
1089
+ });
1090
+
1091
+ api.logger.info(`[claude-mem] OpenClaw plugin loaded — v1.0.0 (worker: ${_workerHost}:${workerPort})`);
1092
+ }