mr-memory 2.7.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 -133
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -8,33 +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
- /** Wrap API response in extraction markers so we can strip it next turn. */
20
- function wrapForInjection(context: string): string {
21
- return `<mr-memory>\n${context}\n</mr-memory>`;
22
- }
23
-
24
- /** Strip previous memory injections from message text to prevent stacking.
25
- * prependContext persists in conversation history — without stripping,
26
- * each turn accumulates another full injection (~20K tokens). */
27
- const MEMORY_TAG_RE = /<mr-memory>[\s\S]*?<\/mr-memory>\s*/g;
28
- /** Legacy tag pattern for backward compat (pre-2.7.0 injections still in history) */
29
- const LEGACY_TAG_RE = /<memory_context>[\s\S]*?<\/memory_context>\s*(?:The above are retrieved memories|IMPORTANT: The above block contains retrieved memories)[^\n]*\n*/g;
30
- function stripOldMemory(text: string): string {
31
- return text.replace(MEMORY_TAG_RE, "").replace(LEGACY_TAG_RE, "").trim();
32
- }
33
-
34
18
  // Workspace files OpenClaw loads into the system prompt
35
19
  const WORKSPACE_FILES = [
36
- "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
37
- "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",
38
28
  ];
39
29
 
40
30
  type MemoryRouterConfig = {
@@ -42,6 +32,7 @@ type MemoryRouterConfig = {
42
32
  endpoint?: string;
43
33
  density?: "low" | "high" | "xhigh";
44
34
  mode?: "relay" | "proxy";
35
+ logging?: boolean;
45
36
  };
46
37
 
47
38
  // ──────────────────────────────────────────────────────
@@ -65,7 +56,9 @@ async function runOpenClawConfigSet(path: string, value: string, json = false):
65
56
  env: process.env,
66
57
  });
67
58
  let stderr = "";
68
- child.stderr.on("data", (chunk) => { stderr += String(chunk); });
59
+ child.stderr.on("data", (chunk) => {
60
+ stderr += String(chunk);
61
+ });
69
62
  child.on("error", reject);
70
63
  child.on("close", (code) => {
71
64
  if (code === 0) resolve();
@@ -79,7 +72,10 @@ type CompatApi = OpenClawPluginApi & {
79
72
  updatePluginEnabled?: (enabled: boolean) => Promise<void>;
80
73
  };
81
74
 
82
- 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> {
83
79
  const compat = api as CompatApi;
84
80
  if (typeof compat.updatePluginConfig === "function") {
85
81
  await compat.updatePluginConfig(config);
@@ -106,7 +102,9 @@ async function readWorkspaceFiles(workspaceDir: string): Promise<string> {
106
102
  try {
107
103
  const content = await readFile(join(workspaceDir, file), "utf8");
108
104
  if (content.trim()) parts.push(`## ${file}\n${content}`);
109
- } catch { /* file doesn't exist — skip */ }
105
+ } catch {
106
+ /* file doesn't exist — skip */
107
+ }
110
108
  }
111
109
  return parts.join("\n\n");
112
110
  }
@@ -119,7 +117,9 @@ function serializeToolsConfig(config: Record<string, unknown>): string {
119
117
  if (!tools) return "";
120
118
  try {
121
119
  return `## Tools Config\n${JSON.stringify(tools, null, 2)}`;
122
- } catch { return ""; }
120
+ } catch {
121
+ return "";
122
+ }
123
123
  }
124
124
 
125
125
  /**
@@ -131,7 +131,9 @@ function serializeSkillsConfig(config: Record<string, unknown>): string {
131
131
  try {
132
132
  const names = Object.keys(skills);
133
133
  return `## Skills (${names.length})\n${names.join(", ")}`;
134
- } catch { return ""; }
134
+ } catch {
135
+ return "";
136
+ }
135
137
  }
136
138
 
137
139
  // ──────────────────────────────────────────────────────
@@ -149,6 +151,8 @@ const memoryRouterPlugin = {
149
151
  const memoryKey = cfg?.key;
150
152
  const density = cfg?.density || "high";
151
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); };
152
156
 
153
157
  if (memoryKey) {
154
158
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}..., mode: ${mode})`);
@@ -161,28 +165,42 @@ const memoryRouterPlugin = {
161
165
  // ==================================================================
162
166
 
163
167
  if (memoryKey) {
164
- // Track whether we've already fired for this prompt (dedup double-fire)
165
- let lastPreparedPrompt = "";
166
- // Track whether before_prompt_build already handled the first call in this run
167
- 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 = "";
168
174
 
169
175
  // ── llm_input: fires on EVERY LLM call (tool iterations, cron, sub-agents)
170
176
  // On stock OpenClaw, the return value is ignored (fire-and-forget).
171
177
  // When PR #24122 merges, OpenClaw will use the returned prependContext.
172
178
  // This gives forward compatibility — no plugin update needed.
173
179
  api.on("llm_input", async (event, ctx) => {
174
- api.logger.warn?.(`memoryrouter: llm_input fired (sessionKey=${ctx.sessionKey}, promptBuildFired=${promptBuildFiredThisRun})`);
175
- // Skip the first call — before_prompt_build already handled it
176
- // (before_prompt_build includes workspace+tools+skills for accurate billing)
177
- if (promptBuildFiredThisRun) {
178
- promptBuildFiredThisRun = false; // reset so subsequent calls go through
179
- return;
180
- }
181
-
182
180
  try {
183
181
  const prompt = event.prompt;
184
- if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") return;
185
- 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;
186
204
 
187
205
  // Build lightweight context (no workspace/tools — just history + prompt)
188
206
  const contextPayload: Array<{ role: string; content: string }> = [];
@@ -194,16 +212,14 @@ const memoryRouterPlugin = {
194
212
  if (typeof m.content === "string") text = m.content;
195
213
  else if (Array.isArray(m.content)) {
196
214
  text = (m.content as Array<{ type?: string; text?: string }>)
197
- .filter(b => b.type === "text" && b.text)
198
- .map(b => b.text!)
215
+ .filter((b) => b.type === "text" && b.text)
216
+ .map((b) => b.text!)
199
217
  .join("\n");
200
218
  }
201
- // Strip old memory injections to prevent stacking
202
- if (text) contextPayload.push({ role: m.role, content: m.role === "user" ? stripOldMemory(text) : text });
219
+ if (text) contextPayload.push({ role: m.role, content: text });
203
220
  }
204
221
  }
205
- contextPayload.push({ role: "user", content: stripOldMemory(prompt) });
206
-
222
+ contextPayload.push({ role: "user", content: prompt });
207
223
 
208
224
  const res = await fetch(`${endpoint}/v1/memory/prepare`, {
209
225
  method: "POST",
@@ -215,11 +231,15 @@ const memoryRouterPlugin = {
215
231
  messages: contextPayload,
216
232
  session_id: ctx.sessionKey,
217
233
  density,
218
-
219
234
  }),
220
235
  });
221
236
 
222
- 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
+ }
223
243
 
224
244
  const data = (await res.json()) as {
225
245
  context?: string;
@@ -228,28 +248,26 @@ const memoryRouterPlugin = {
228
248
  };
229
249
 
230
250
  if (data.context) {
231
- api.logger.info?.(
232
- `memoryrouter: injected ${data.memories_found || 0} memories on tool iteration (${data.tokens_billed || 0} tokens billed)`,
233
- );
234
- return { prependContext: wrapForInjection(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;
235
255
  }
236
- } catch {
237
- // 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)}`);
238
258
  }
239
259
  });
240
260
 
241
261
  // ── before_prompt_build: fires once per run (primary, includes full billing context)
242
262
  api.on("before_prompt_build", async (event, ctx) => {
243
- promptBuildFiredThisRun = true;
244
- api.logger.warn?.(`memoryrouter: before_prompt_build fired (sessionKey=${ctx.sessionKey}, promptLen=${event.prompt?.length})`);
245
263
  try {
246
264
  const prompt = event.prompt;
247
265
 
248
- // Deduplicate if we already prepared this exact prompt, skip
249
- if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") {
266
+ // Dedupe only within before_prompt_build path.
267
+ if (prompt === lastPromptBuildPrompt && lastPromptBuildPrompt !== "") {
250
268
  return;
251
269
  }
252
- lastPreparedPrompt = prompt;
270
+ lastPromptBuildPrompt = prompt;
253
271
 
254
272
  // 1. Read workspace files for full token count
255
273
  const workspaceDir = ctx.workspaceDir || "";
@@ -260,7 +278,9 @@ const memoryRouterPlugin = {
260
278
 
261
279
  // 2. Serialize tools + skills from config
262
280
  const toolsText = serializeToolsConfig(api.config as unknown as Record<string, unknown>);
263
- 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
+ );
264
284
 
265
285
  // 3. Build full context payload (messages + workspace + tools + skills)
266
286
  const contextPayload: Array<{ role: string; content: string }> = [];
@@ -273,33 +293,29 @@ const memoryRouterPlugin = {
273
293
 
274
294
  // Add conversation history
275
295
  if (event.messages && Array.isArray(event.messages)) {
276
- let skipped = 0;
277
296
  for (const msg of event.messages) {
278
297
  const m = msg as { role?: string; content?: unknown };
279
298
  if (!m.role) continue;
280
-
299
+
281
300
  let text = "";
282
301
  if (typeof m.content === "string") {
283
302
  text = m.content;
284
303
  } else if (Array.isArray(m.content)) {
285
304
  // Handle Anthropic-style content blocks [{type:"text", text:"..."}, ...]
286
305
  text = (m.content as Array<{ type?: string; text?: string }>)
287
- .filter(b => b.type === "text" && b.text)
288
- .map(b => b.text!)
306
+ .filter((b) => b.type === "text" && b.text)
307
+ .map((b) => b.text!)
289
308
  .join("\n");
290
309
  }
291
-
310
+
292
311
  if (text) {
293
- // Strip old memory injections to prevent stacking
294
- contextPayload.push({ role: m.role, content: m.role === "user" ? stripOldMemory(text) : text });
295
- } else {
296
- skipped++;
312
+ contextPayload.push({ role: m.role, content: text });
297
313
  }
298
314
  }
299
315
  }
300
316
 
301
- // Add current user prompt (strip any residual memory tags)
302
- contextPayload.push({ role: "user", content: stripOldMemory(prompt) });
317
+ // Add current user prompt
318
+ contextPayload.push({ role: "user", content: prompt });
303
319
 
304
320
  // 4. Call /v1/memory/prepare
305
321
 
@@ -313,15 +329,17 @@ const memoryRouterPlugin = {
313
329
  messages: contextPayload,
314
330
  session_id: ctx.sessionKey,
315
331
  density,
316
-
317
332
  }),
318
333
  });
319
334
 
320
335
  if (!res.ok) {
321
- api.logger.warn?.(`memoryrouter: prepare failed (${res.status})`);
336
+ log(`memoryrouter: prepare failed (${res.status})`);
322
337
  return;
323
338
  }
324
339
 
340
+ // Suppress the immediately-following duplicate llm_input for this prompt/session.
341
+ skipNextLlmInput = { prompt, sessionKey: ctx.sessionKey };
342
+
325
343
  const data = (await res.json()) as {
326
344
  context?: string;
327
345
  memories_found?: number;
@@ -329,15 +347,11 @@ const memoryRouterPlugin = {
329
347
  };
330
348
 
331
349
  if (data.context) {
332
- api.logger.info?.(
333
- `memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens billed)`,
334
- );
335
- return { prependContext: wrapForInjection(data.context) };
350
+ log(`memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens)`);
351
+ return { prependContext: data.context };
336
352
  }
337
353
  } catch (err) {
338
- api.logger.warn?.(
339
- `memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`,
340
- );
354
+ log(`memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`);
341
355
  }
342
356
  });
343
357
 
@@ -350,13 +364,22 @@ const memoryRouterPlugin = {
350
364
  const msgs = event.messages;
351
365
  if (!msgs || !Array.isArray(msgs) || msgs.length === 0) return;
352
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
+
353
376
  // Extract text from a message (handles string + content block arrays)
354
377
  function extractText(content: unknown): string {
355
378
  if (typeof content === "string") return content;
356
379
  if (Array.isArray(content)) {
357
380
  return (content as Array<{ type?: string; text?: string }>)
358
- .filter(b => b.type === "text" && b.text)
359
- .map(b => b.text!)
381
+ .filter((b) => b.type === "text" && b.text)
382
+ .map((b) => b.text!)
360
383
  .join("\n");
361
384
  }
362
385
  return "";
@@ -385,7 +408,7 @@ const memoryRouterPlugin = {
385
408
 
386
409
  // Collect ALL assistant messages after the last user message
387
410
  const assistantParts: string[] = [];
388
- 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++) {
389
412
  const msg = msgs[i] as { role?: string; content?: unknown };
390
413
  if (msg.role === "assistant") {
391
414
  const text = extractText(msg.content);
@@ -398,36 +421,31 @@ const memoryRouterPlugin = {
398
421
 
399
422
  if (toStore.length === 0) return;
400
423
 
401
- // Await the fetch so OpenClaw's runVoidHook keeps the event loop alive.
402
- try {
403
- const res = await fetch(`${endpoint}/v1/memory/ingest`, {
404
- method: "POST",
405
- headers: {
406
- "Content-Type": "application/json",
407
- Authorization: `Bearer ${memoryKey}`,
408
- },
409
- body: JSON.stringify({
410
- messages: toStore,
411
- session_id: ctx.sessionKey,
412
- model: "unknown",
413
- }),
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)}`);
414
446
  });
415
- if (!res.ok) {
416
- const details = await res.text().catch(() => "");
417
- const suffix = details ? ` — ${details.slice(0, 200)}` : "";
418
- api.logger.warn?.(`memoryrouter: ingest failed (${res.status})${suffix}`);
419
- } else {
420
- api.logger.debug?.(`memoryrouter: ingest accepted (${toStore.length} messages)`);
421
- }
422
- } catch (err) {
423
- api.logger.warn?.(
424
- `memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`,
425
- );
426
- }
427
447
  } catch (err) {
428
- api.logger.warn?.(
429
- `memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`,
430
- );
448
+ log(`memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`);
431
449
  }
432
450
  });
433
451
  } // end if (memoryKey)
@@ -459,14 +477,19 @@ const memoryRouterPlugin = {
459
477
  .description("MemoryRouter memory commands")
460
478
  .argument("[key]", "Your MemoryRouter memory key (mk_xxx)")
461
479
  .action(async (key: string | undefined) => {
462
- if (!key) { mr.help(); return; }
480
+ if (!key) {
481
+ mr.help();
482
+ return;
483
+ }
463
484
  await applyKey(key);
464
485
  });
465
486
 
466
487
  mr.command("enable")
467
488
  .description("Enable MemoryRouter with a memory key (alias)")
468
489
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
469
- .action(async (key: string) => { await applyKey(key); });
490
+ .action(async (key: string) => {
491
+ await applyKey(key);
492
+ });
470
493
 
471
494
  mr.command("off")
472
495
  .description("Disable MemoryRouter (removes key)")
@@ -476,7 +499,9 @@ const memoryRouterPlugin = {
476
499
  await setPluginEnabled(api, false);
477
500
  console.log("✓ MemoryRouter disabled.");
478
501
  } catch (err) {
479
- 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
+ );
480
505
  }
481
506
  });
482
507
 
@@ -489,9 +514,16 @@ const memoryRouterPlugin = {
489
514
  mr.command(name)
490
515
  .description(desc)
491
516
  .action(async () => {
492
- 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
+ }
493
521
  try {
494
- 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
+ });
495
527
  console.log(`✓ Memory density set to ${name}`);
496
528
  } catch (err) {
497
529
  console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -499,6 +531,29 @@ const memoryRouterPlugin = {
499
531
  });
500
532
  }
501
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
+
502
557
  // Mode commands
503
558
  for (const [modeName, modeDesc] of [
504
559
  ["relay", "Relay mode — hooks only, works on stock OpenClaw [default]"],
@@ -507,9 +562,17 @@ const memoryRouterPlugin = {
507
562
  mr.command(modeName)
508
563
  .description(modeDesc)
509
564
  .action(async () => {
510
- 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
+ }
511
569
  try {
512
- 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
+ });
513
576
  console.log(`✓ Mode set to ${modeName}`);
514
577
  } catch (err) {
515
578
  console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -521,14 +584,23 @@ const memoryRouterPlugin = {
521
584
  .description("Show MemoryRouter vault stats")
522
585
  .option("--json", "JSON output")
523
586
  .action(async (opts) => {
524
- 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
+ }
525
591
  try {
526
592
  const res = await fetch(`${endpoint}/v1/memory/stats`, {
527
593
  headers: { Authorization: `Bearer ${memoryKey}` },
528
594
  });
529
595
  const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
530
596
  if (opts.json) {
531
- 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
+ );
532
604
  } else {
533
605
  console.log("MemoryRouter Status");
534
606
  console.log("───────────────────────────");
@@ -550,25 +622,47 @@ const memoryRouterPlugin = {
550
622
  .argument("[path]", "Specific file or directory to upload")
551
623
  .option("--workspace <dir>", "Workspace directory")
552
624
  .option("--brain <dir>", "State directory with sessions")
553
- .action(async (targetPath: string | undefined, opts: { workspace?: string; brain?: string }) => {
554
- if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
555
- const os = await import("node:os");
556
- const path = await import("node:path");
557
- const stateDir = opts.brain ? path.resolve(opts.brain) : path.join(os.homedir(), ".openclaw");
558
- const configWorkspace = (api.config as any).workspace || (api.config as any).agents?.defaults?.workspace;
559
- const workspacePath = opts.workspace
560
- ? path.resolve(opts.workspace)
561
- : configWorkspace
562
- ? path.resolve(configWorkspace.replace(/^~/, os.homedir()))
563
- : path.join(os.homedir(), ".openclaw", "workspace");
564
- const { runUpload } = await import("./upload.js");
565
- await runUpload({ memoryKey, endpoint, targetPath, stateDir, workspacePath, hasWorkspaceFlag: !!opts.workspace, hasBrainFlag: !!opts.brain });
566
- });
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
+ );
567
658
 
568
659
  mr.command("delete")
569
660
  .description("Clear all memories from vault")
570
661
  .action(async () => {
571
- 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
+ }
572
666
  try {
573
667
  const res = await fetch(`${endpoint}/v1/memory`, {
574
668
  method: "DELETE",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "2.7.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": [