mr-memory 2.6.0 → 2.8.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 (2) hide show
  1. package/index.ts +227 -128
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -8,30 +8,23 @@
8
8
  * BYOK — provider API keys never touch MemoryRouter.
9
9
  */
10
10
 
11
- import { readFile, readdir, stat } from "node:fs/promises";
11
+ import { readFile } from "node:fs/promises";
12
12
  import { join } from "node:path";
13
13
  import { spawn } from "node:child_process";
14
14
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
15
15
 
16
16
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
17
17
 
18
- /** Wrap raw memory context in XML tags with a strong instruction */
19
- function wrapMemoryContext(context: string): string {
20
- return `<memory_context>\n${context}\n</memory_context>\n\nThe above are retrieved memories from past conversations — not current events. Reference them as background context with appropriate temporal framing. Do not treat them as part of the current message or present moment.`;
21
- }
22
-
23
- /** Strip previous memory injections from message text to prevent stacking.
24
- * prependContext persists in conversation history — without stripping,
25
- * each turn accumulates another full injection (~20K tokens). */
26
- const MEMORY_TAG_RE = /<memory_context>[\s\S]*?<\/memory_context>\s*The above are retrieved memories from past conversations[^\n]*\n*/g;
27
- function stripOldMemory(text: string): string {
28
- return text.replace(MEMORY_TAG_RE, "").trim();
29
- }
30
-
31
18
  // Workspace files OpenClaw loads into the system prompt
32
19
  const WORKSPACE_FILES = [
33
- "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
34
- "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
20
+ "IDENTITY.md",
21
+ "USER.md",
22
+ "MEMORY.md",
23
+ "HEARTBEAT.md",
24
+ "TOOLS.md",
25
+ "AGENTS.md",
26
+ "SOUL.md",
27
+ "BOOTSTRAP.md",
35
28
  ];
36
29
 
37
30
  type MemoryRouterConfig = {
@@ -39,6 +32,7 @@ type MemoryRouterConfig = {
39
32
  endpoint?: string;
40
33
  density?: "low" | "high" | "xhigh";
41
34
  mode?: "relay" | "proxy";
35
+ logging?: boolean;
42
36
  };
43
37
 
44
38
  // ──────────────────────────────────────────────────────
@@ -62,7 +56,9 @@ async function runOpenClawConfigSet(path: string, value: string, json = false):
62
56
  env: process.env,
63
57
  });
64
58
  let stderr = "";
65
- child.stderr.on("data", (chunk) => { stderr += String(chunk); });
59
+ child.stderr.on("data", (chunk) => {
60
+ stderr += String(chunk);
61
+ });
66
62
  child.on("error", reject);
67
63
  child.on("close", (code) => {
68
64
  if (code === 0) resolve();
@@ -76,7 +72,10 @@ type CompatApi = OpenClawPluginApi & {
76
72
  updatePluginEnabled?: (enabled: boolean) => Promise<void>;
77
73
  };
78
74
 
79
- async function setPluginConfig(api: OpenClawPluginApi, config: Record<string, unknown>): Promise<void> {
75
+ async function setPluginConfig(
76
+ api: OpenClawPluginApi,
77
+ config: Record<string, unknown>,
78
+ ): Promise<void> {
80
79
  const compat = api as CompatApi;
81
80
  if (typeof compat.updatePluginConfig === "function") {
82
81
  await compat.updatePluginConfig(config);
@@ -103,7 +102,9 @@ async function readWorkspaceFiles(workspaceDir: string): Promise<string> {
103
102
  try {
104
103
  const content = await readFile(join(workspaceDir, file), "utf8");
105
104
  if (content.trim()) parts.push(`## ${file}\n${content}`);
106
- } catch { /* file doesn't exist — skip */ }
105
+ } catch {
106
+ /* file doesn't exist — skip */
107
+ }
107
108
  }
108
109
  return parts.join("\n\n");
109
110
  }
@@ -116,7 +117,9 @@ function serializeToolsConfig(config: Record<string, unknown>): string {
116
117
  if (!tools) return "";
117
118
  try {
118
119
  return `## Tools Config\n${JSON.stringify(tools, null, 2)}`;
119
- } catch { return ""; }
120
+ } catch {
121
+ return "";
122
+ }
120
123
  }
121
124
 
122
125
  /**
@@ -128,7 +131,9 @@ function serializeSkillsConfig(config: Record<string, unknown>): string {
128
131
  try {
129
132
  const names = Object.keys(skills);
130
133
  return `## Skills (${names.length})\n${names.join(", ")}`;
131
- } catch { return ""; }
134
+ } catch {
135
+ return "";
136
+ }
132
137
  }
133
138
 
134
139
  // ──────────────────────────────────────────────────────
@@ -146,6 +151,8 @@ const memoryRouterPlugin = {
146
151
  const memoryKey = cfg?.key;
147
152
  const density = cfg?.density || "high";
148
153
  const mode = cfg?.mode || "relay";
154
+ const logging = cfg?.logging ?? false; // off by default
155
+ const log = (msg: string) => { if (logging) api.logger.info?.(msg); };
149
156
 
150
157
  if (memoryKey) {
151
158
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}..., mode: ${mode})`);
@@ -158,27 +165,42 @@ const memoryRouterPlugin = {
158
165
  // ==================================================================
159
166
 
160
167
  if (memoryKey) {
161
- // Track whether we've already fired for this prompt (dedup double-fire)
162
- let lastPreparedPrompt = "";
163
- // Track whether before_prompt_build already handled the first call in this run
164
- let promptBuildFiredThisRun = false;
168
+ // Track prompt-build dedupe independently from llm_input dedupe.
169
+ let lastPromptBuildPrompt = "";
170
+ // Skip exactly one matching llm_input after a successful before_prompt_build call.
171
+ let skipNextLlmInput: { prompt: string; sessionKey?: string } | null = null;
172
+ // Deduplicate repeated llm_input events without suppressing unrelated calls.
173
+ let lastLlmInputSignature = "";
165
174
 
166
175
  // ── llm_input: fires on EVERY LLM call (tool iterations, cron, sub-agents)
167
176
  // On stock OpenClaw, the return value is ignored (fire-and-forget).
168
177
  // When PR #24122 merges, OpenClaw will use the returned prependContext.
169
178
  // This gives forward compatibility — no plugin update needed.
170
179
  api.on("llm_input", async (event, ctx) => {
171
- // Skip the first call — before_prompt_build already handled it
172
- // (before_prompt_build includes workspace+tools+skills for accurate billing)
173
- if (promptBuildFiredThisRun) {
174
- promptBuildFiredThisRun = false; // reset so subsequent calls go through
175
- return;
176
- }
177
-
178
180
  try {
179
181
  const prompt = event.prompt;
180
- if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") return;
181
- lastPreparedPrompt = prompt;
182
+
183
+ // Skip one duplicate llm_input right after a successful before_prompt_build.
184
+ if (
185
+ skipNextLlmInput &&
186
+ skipNextLlmInput.prompt === prompt &&
187
+ skipNextLlmInput.sessionKey === ctx.sessionKey
188
+ ) {
189
+ skipNextLlmInput = null;
190
+ return;
191
+ }
192
+ if (skipNextLlmInput && skipNextLlmInput.sessionKey !== ctx.sessionKey) {
193
+ skipNextLlmInput = null;
194
+ }
195
+
196
+ // Dedupe exact repeated llm_input events only (same run/session/prompt/history size).
197
+ const llmInputSignature = [
198
+ ctx.sessionKey || "",
199
+ prompt,
200
+ String(Array.isArray(event.historyMessages) ? event.historyMessages.length : 0),
201
+ ].join("|");
202
+ if (llmInputSignature === lastLlmInputSignature) return;
203
+ lastLlmInputSignature = llmInputSignature;
182
204
 
183
205
  // Build lightweight context (no workspace/tools — just history + prompt)
184
206
  const contextPayload: Array<{ role: string; content: string }> = [];
@@ -190,16 +212,14 @@ const memoryRouterPlugin = {
190
212
  if (typeof m.content === "string") text = m.content;
191
213
  else if (Array.isArray(m.content)) {
192
214
  text = (m.content as Array<{ type?: string; text?: string }>)
193
- .filter(b => b.type === "text" && b.text)
194
- .map(b => b.text!)
215
+ .filter((b) => b.type === "text" && b.text)
216
+ .map((b) => b.text!)
195
217
  .join("\n");
196
218
  }
197
- // Strip old memory injections to prevent stacking
198
- if (text) contextPayload.push({ role: m.role, content: m.role === "user" ? stripOldMemory(text) : text });
219
+ if (text) contextPayload.push({ role: m.role, content: text });
199
220
  }
200
221
  }
201
- contextPayload.push({ role: "user", content: stripOldMemory(prompt) });
202
-
222
+ contextPayload.push({ role: "user", content: prompt });
203
223
 
204
224
  const res = await fetch(`${endpoint}/v1/memory/prepare`, {
205
225
  method: "POST",
@@ -211,11 +231,15 @@ const memoryRouterPlugin = {
211
231
  messages: contextPayload,
212
232
  session_id: ctx.sessionKey,
213
233
  density,
214
-
215
234
  }),
216
235
  });
217
236
 
218
- if (!res.ok) return;
237
+ if (!res.ok) {
238
+ const details = await res.text().catch(() => "");
239
+ const suffix = details ? ` — ${details.slice(0, 200)}` : "";
240
+ log(`memoryrouter: llm_input prepare failed (${res.status})${suffix}`);
241
+ return;
242
+ }
219
243
 
220
244
  const data = (await res.json()) as {
221
245
  context?: string;
@@ -224,27 +248,26 @@ const memoryRouterPlugin = {
224
248
  };
225
249
 
226
250
  if (data.context) {
227
- api.logger.info?.(
228
- `memoryrouter: injected ${data.memories_found || 0} memories on tool iteration (${data.tokens_billed || 0} tokens billed)`,
229
- );
230
- return { prependContext: wrapMemoryContext(data.context) };
251
+ log(`memoryrouter: injected ${data.memories_found || 0} memories on tool iteration (${data.tokens_billed || 0} tokens)`);
252
+ // llm_input is typed as void on current OpenClaw builds; cast keeps
253
+ // runtime forward-compat for builds that consume prependContext.
254
+ return { prependContext: data.context } as unknown as void;
231
255
  }
232
- } catch {
233
- // Silent fail on tool iterations don't block the agent
256
+ } catch (err) {
257
+ log(`memoryrouter: llm_input prepare error ${err instanceof Error ? err.message : String(err)}`);
234
258
  }
235
259
  });
236
260
 
237
261
  // ── before_prompt_build: fires once per run (primary, includes full billing context)
238
262
  api.on("before_prompt_build", async (event, ctx) => {
239
- promptBuildFiredThisRun = true;
240
263
  try {
241
264
  const prompt = event.prompt;
242
265
 
243
- // Deduplicate if we already prepared this exact prompt, skip
244
- if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") {
266
+ // Dedupe only within before_prompt_build path.
267
+ if (prompt === lastPromptBuildPrompt && lastPromptBuildPrompt !== "") {
245
268
  return;
246
269
  }
247
- lastPreparedPrompt = prompt;
270
+ lastPromptBuildPrompt = prompt;
248
271
 
249
272
  // 1. Read workspace files for full token count
250
273
  const workspaceDir = ctx.workspaceDir || "";
@@ -255,7 +278,9 @@ const memoryRouterPlugin = {
255
278
 
256
279
  // 2. Serialize tools + skills from config
257
280
  const toolsText = serializeToolsConfig(api.config as unknown as Record<string, unknown>);
258
- const skillsText = serializeSkillsConfig(api.config as unknown as Record<string, unknown>);
281
+ const skillsText = serializeSkillsConfig(
282
+ api.config as unknown as Record<string, unknown>,
283
+ );
259
284
 
260
285
  // 3. Build full context payload (messages + workspace + tools + skills)
261
286
  const contextPayload: Array<{ role: string; content: string }> = [];
@@ -268,33 +293,29 @@ const memoryRouterPlugin = {
268
293
 
269
294
  // Add conversation history
270
295
  if (event.messages && Array.isArray(event.messages)) {
271
- let skipped = 0;
272
296
  for (const msg of event.messages) {
273
297
  const m = msg as { role?: string; content?: unknown };
274
298
  if (!m.role) continue;
275
-
299
+
276
300
  let text = "";
277
301
  if (typeof m.content === "string") {
278
302
  text = m.content;
279
303
  } else if (Array.isArray(m.content)) {
280
304
  // Handle Anthropic-style content blocks [{type:"text", text:"..."}, ...]
281
305
  text = (m.content as Array<{ type?: string; text?: string }>)
282
- .filter(b => b.type === "text" && b.text)
283
- .map(b => b.text!)
306
+ .filter((b) => b.type === "text" && b.text)
307
+ .map((b) => b.text!)
284
308
  .join("\n");
285
309
  }
286
-
310
+
287
311
  if (text) {
288
- // Strip old memory injections to prevent stacking
289
- contextPayload.push({ role: m.role, content: m.role === "user" ? stripOldMemory(text) : text });
290
- } else {
291
- skipped++;
312
+ contextPayload.push({ role: m.role, content: text });
292
313
  }
293
314
  }
294
315
  }
295
316
 
296
- // Add current user prompt (strip any residual memory tags)
297
- contextPayload.push({ role: "user", content: stripOldMemory(prompt) });
317
+ // Add current user prompt
318
+ contextPayload.push({ role: "user", content: prompt });
298
319
 
299
320
  // 4. Call /v1/memory/prepare
300
321
 
@@ -308,15 +329,17 @@ const memoryRouterPlugin = {
308
329
  messages: contextPayload,
309
330
  session_id: ctx.sessionKey,
310
331
  density,
311
-
312
332
  }),
313
333
  });
314
334
 
315
335
  if (!res.ok) {
316
- api.logger.warn?.(`memoryrouter: prepare failed (${res.status})`);
336
+ log(`memoryrouter: prepare failed (${res.status})`);
317
337
  return;
318
338
  }
319
339
 
340
+ // Suppress the immediately-following duplicate llm_input for this prompt/session.
341
+ skipNextLlmInput = { prompt, sessionKey: ctx.sessionKey };
342
+
320
343
  const data = (await res.json()) as {
321
344
  context?: string;
322
345
  memories_found?: number;
@@ -324,15 +347,11 @@ const memoryRouterPlugin = {
324
347
  };
325
348
 
326
349
  if (data.context) {
327
- api.logger.info?.(
328
- `memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens billed)`,
329
- );
330
- return { prependContext: wrapMemoryContext(data.context) };
350
+ log(`memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens)`);
351
+ return { prependContext: data.context };
331
352
  }
332
353
  } catch (err) {
333
- api.logger.warn?.(
334
- `memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`,
335
- );
354
+ log(`memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`);
336
355
  }
337
356
  });
338
357
 
@@ -345,13 +364,22 @@ const memoryRouterPlugin = {
345
364
  const msgs = event.messages;
346
365
  if (!msgs || !Array.isArray(msgs) || msgs.length === 0) return;
347
366
 
367
+ // Default relay behavior: do not store subagent sessions.
368
+ const sessionKey = ctx.sessionKey || "";
369
+ if (typeof sessionKey === "string" && sessionKey.includes(":subagent:")) {
370
+ api.logger.debug?.(
371
+ `memoryrouter: skipping ingest for subagent session (${sessionKey})`,
372
+ );
373
+ return;
374
+ }
375
+
348
376
  // Extract text from a message (handles string + content block arrays)
349
377
  function extractText(content: unknown): string {
350
378
  if (typeof content === "string") return content;
351
379
  if (Array.isArray(content)) {
352
380
  return (content as Array<{ type?: string; text?: string }>)
353
- .filter(b => b.type === "text" && b.text)
354
- .map(b => b.text!)
381
+ .filter((b) => b.type === "text" && b.text)
382
+ .map((b) => b.text!)
355
383
  .join("\n");
356
384
  }
357
385
  return "";
@@ -380,7 +408,7 @@ const memoryRouterPlugin = {
380
408
 
381
409
  // Collect ALL assistant messages after the last user message
382
410
  const assistantParts: string[] = [];
383
- for (let i = (lastUserIdx >= 0 ? lastUserIdx + 1 : 0); i < msgs.length; i++) {
411
+ for (let i = lastUserIdx >= 0 ? lastUserIdx + 1 : 0; i < msgs.length; i++) {
384
412
  const msg = msgs[i] as { role?: string; content?: unknown };
385
413
  if (msg.role === "assistant") {
386
414
  const text = extractText(msg.content);
@@ -393,36 +421,31 @@ const memoryRouterPlugin = {
393
421
 
394
422
  if (toStore.length === 0) return;
395
423
 
396
- // Await the fetch so OpenClaw's runVoidHook keeps the event loop alive.
397
- try {
398
- const res = await fetch(`${endpoint}/v1/memory/ingest`, {
399
- method: "POST",
400
- headers: {
401
- "Content-Type": "application/json",
402
- Authorization: `Bearer ${memoryKey}`,
403
- },
404
- body: JSON.stringify({
405
- messages: toStore,
406
- session_id: ctx.sessionKey,
407
- model: "unknown",
408
- }),
424
+ // Fire-and-forget: don't block agent completion on memory ingestion.
425
+ void fetch(`${endpoint}/v1/memory/ingest`, {
426
+ method: "POST",
427
+ headers: {
428
+ "Content-Type": "application/json",
429
+ Authorization: `Bearer ${memoryKey}`,
430
+ },
431
+ body: JSON.stringify({
432
+ messages: toStore,
433
+ session_id: ctx.sessionKey,
434
+ model: "unknown",
435
+ }),
436
+ })
437
+ .then(async (res) => {
438
+ if (!res.ok) {
439
+ const details = await res.text().catch(() => "");
440
+ const suffix = details ? ` — ${details.slice(0, 200)}` : "";
441
+ log(`memoryrouter: ingest failed (${res.status})${suffix}`);
442
+ }
443
+ })
444
+ .catch((err) => {
445
+ log(`memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`);
409
446
  });
410
- if (!res.ok) {
411
- const details = await res.text().catch(() => "");
412
- const suffix = details ? ` — ${details.slice(0, 200)}` : "";
413
- api.logger.warn?.(`memoryrouter: ingest failed (${res.status})${suffix}`);
414
- } else {
415
- api.logger.debug?.(`memoryrouter: ingest accepted (${toStore.length} messages)`);
416
- }
417
- } catch (err) {
418
- api.logger.warn?.(
419
- `memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`,
420
- );
421
- }
422
447
  } catch (err) {
423
- api.logger.warn?.(
424
- `memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`,
425
- );
448
+ log(`memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`);
426
449
  }
427
450
  });
428
451
  } // end if (memoryKey)
@@ -454,14 +477,19 @@ const memoryRouterPlugin = {
454
477
  .description("MemoryRouter memory commands")
455
478
  .argument("[key]", "Your MemoryRouter memory key (mk_xxx)")
456
479
  .action(async (key: string | undefined) => {
457
- if (!key) { mr.help(); return; }
480
+ if (!key) {
481
+ mr.help();
482
+ return;
483
+ }
458
484
  await applyKey(key);
459
485
  });
460
486
 
461
487
  mr.command("enable")
462
488
  .description("Enable MemoryRouter with a memory key (alias)")
463
489
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
464
- .action(async (key: string) => { await applyKey(key); });
490
+ .action(async (key: string) => {
491
+ await applyKey(key);
492
+ });
465
493
 
466
494
  mr.command("off")
467
495
  .description("Disable MemoryRouter (removes key)")
@@ -471,7 +499,9 @@ const memoryRouterPlugin = {
471
499
  await setPluginEnabled(api, false);
472
500
  console.log("✓ MemoryRouter disabled.");
473
501
  } catch (err) {
474
- console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
502
+ console.error(
503
+ `Failed to disable: ${err instanceof Error ? err.message : String(err)}`,
504
+ );
475
505
  }
476
506
  });
477
507
 
@@ -484,9 +514,16 @@ const memoryRouterPlugin = {
484
514
  mr.command(name)
485
515
  .description(desc)
486
516
  .action(async () => {
487
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
517
+ if (!memoryKey) {
518
+ console.error("Not configured. Run: openclaw mr <key>");
519
+ return;
520
+ }
488
521
  try {
489
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: name });
522
+ await setPluginConfig(api, {
523
+ key: memoryKey,
524
+ endpoint: cfg?.endpoint,
525
+ density: name,
526
+ });
490
527
  console.log(`✓ Memory density set to ${name}`);
491
528
  } catch (err) {
492
529
  console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -494,6 +531,29 @@ const memoryRouterPlugin = {
494
531
  });
495
532
  }
496
533
 
534
+ // Logging toggle
535
+ mr.command("logging")
536
+ .description("Toggle debug logging on/off")
537
+ .action(async () => {
538
+ if (!memoryKey) {
539
+ console.error("Not configured. Run: openclaw mr <key>");
540
+ return;
541
+ }
542
+ const newLogging = !logging;
543
+ try {
544
+ await setPluginConfig(api, {
545
+ key: memoryKey,
546
+ endpoint: cfg?.endpoint,
547
+ density,
548
+ mode,
549
+ logging: newLogging,
550
+ });
551
+ console.log(`✓ Logging ${newLogging ? "ON" : "OFF"} (restart gateway to apply)`);
552
+ } catch (err) {
553
+ console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
554
+ }
555
+ });
556
+
497
557
  // Mode commands
498
558
  for (const [modeName, modeDesc] of [
499
559
  ["relay", "Relay mode — hooks only, works on stock OpenClaw [default]"],
@@ -502,9 +562,17 @@ const memoryRouterPlugin = {
502
562
  mr.command(modeName)
503
563
  .description(modeDesc)
504
564
  .action(async () => {
505
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
565
+ if (!memoryKey) {
566
+ console.error("Not configured. Run: openclaw mr <key>");
567
+ return;
568
+ }
506
569
  try {
507
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density, mode: modeName });
570
+ await setPluginConfig(api, {
571
+ key: memoryKey,
572
+ endpoint: cfg?.endpoint,
573
+ density,
574
+ mode: modeName,
575
+ });
508
576
  console.log(`✓ Mode set to ${modeName}`);
509
577
  } catch (err) {
510
578
  console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -516,14 +584,23 @@ const memoryRouterPlugin = {
516
584
  .description("Show MemoryRouter vault stats")
517
585
  .option("--json", "JSON output")
518
586
  .action(async (opts) => {
519
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
587
+ if (!memoryKey) {
588
+ console.error("Not configured. Run: openclaw mr <key>");
589
+ return;
590
+ }
520
591
  try {
521
592
  const res = await fetch(`${endpoint}/v1/memory/stats`, {
522
593
  headers: { Authorization: `Bearer ${memoryKey}` },
523
594
  });
524
595
  const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
525
596
  if (opts.json) {
526
- console.log(JSON.stringify({ enabled: true, key: memoryKey, density, mode, stats: data }, null, 2));
597
+ console.log(
598
+ JSON.stringify(
599
+ { enabled: true, key: memoryKey, density, mode, stats: data },
600
+ null,
601
+ 2,
602
+ ),
603
+ );
527
604
  } else {
528
605
  console.log("MemoryRouter Status");
529
606
  console.log("───────────────────────────");
@@ -545,25 +622,47 @@ const memoryRouterPlugin = {
545
622
  .argument("[path]", "Specific file or directory to upload")
546
623
  .option("--workspace <dir>", "Workspace directory")
547
624
  .option("--brain <dir>", "State directory with sessions")
548
- .action(async (targetPath: string | undefined, opts: { workspace?: string; brain?: string }) => {
549
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
550
- const os = await import("node:os");
551
- const path = await import("node:path");
552
- const stateDir = opts.brain ? path.resolve(opts.brain) : path.join(os.homedir(), ".openclaw");
553
- const configWorkspace = (api.config as any).workspace || (api.config as any).agents?.defaults?.workspace;
554
- const workspacePath = opts.workspace
555
- ? path.resolve(opts.workspace)
556
- : configWorkspace
557
- ? path.resolve(configWorkspace.replace(/^~/, os.homedir()))
558
- : path.join(os.homedir(), ".openclaw", "workspace");
559
- const { runUpload } = await import("./upload.js");
560
- await runUpload({ memoryKey, endpoint, targetPath, stateDir, workspacePath, hasWorkspaceFlag: !!opts.workspace, hasBrainFlag: !!opts.brain });
561
- });
625
+ .action(
626
+ async (
627
+ targetPath: string | undefined,
628
+ opts: { workspace?: string; brain?: string },
629
+ ) => {
630
+ if (!memoryKey) {
631
+ console.error("Not configured. Run: openclaw mr <key>");
632
+ return;
633
+ }
634
+ const os = await import("node:os");
635
+ const path = await import("node:path");
636
+ const stateDir = opts.brain
637
+ ? path.resolve(opts.brain)
638
+ : path.join(os.homedir(), ".openclaw");
639
+ const configWorkspace =
640
+ (api.config as any).workspace || (api.config as any).agents?.defaults?.workspace;
641
+ const workspacePath = opts.workspace
642
+ ? path.resolve(opts.workspace)
643
+ : configWorkspace
644
+ ? path.resolve(configWorkspace.replace(/^~/, os.homedir()))
645
+ : path.join(os.homedir(), ".openclaw", "workspace");
646
+ const { runUpload } = await import("./upload.js");
647
+ await runUpload({
648
+ memoryKey,
649
+ endpoint,
650
+ targetPath,
651
+ stateDir,
652
+ workspacePath,
653
+ hasWorkspaceFlag: !!opts.workspace,
654
+ hasBrainFlag: !!opts.brain,
655
+ });
656
+ },
657
+ );
562
658
 
563
659
  mr.command("delete")
564
660
  .description("Clear all memories from vault")
565
661
  .action(async () => {
566
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
662
+ if (!memoryKey) {
663
+ console.error("Not configured. Run: openclaw mr <key>");
664
+ return;
665
+ }
567
666
  try {
568
667
  const res = await fetch(`${endpoint}/v1/memory`, {
569
668
  method: "DELETE",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "MemoryRouter persistent memory plugin for OpenClaw — your AI remembers every conversation",
5
5
  "type": "module",
6
6
  "files": [