mr-memory 2.0.0 → 2.2.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 +281 -206
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,88 +1,127 @@
1
1
  /**
2
- * MemoryRouter Plugin for OpenClaw (v2 — Relay Architecture)
2
+ * MemoryRouter Plugin for OpenClaw
3
3
  *
4
4
  * Persistent AI memory via MemoryRouter (memoryrouter.ai).
5
- * Uses OpenClaw's native plugin hooks (before_agent_start + agent_end)
6
- * to inject relevant memories and capture conversations.
5
+ * Uses before_agent_start + agent_end hooks to inject/store memories
6
+ * via the MemoryRouter relay API. No proxy interception needed.
7
7
  *
8
- * No patching required. Works with stock OpenClaw.
8
+ * BYOK provider API keys never touch MemoryRouter.
9
9
  */
10
10
 
11
+ import { readFile, readdir, stat } from "node:fs/promises";
12
+ import { join } from "node:path";
11
13
  import { spawn } from "node:child_process";
12
14
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
13
15
 
14
16
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
15
17
 
18
+ // Workspace files OpenClaw loads into the system prompt
19
+ const WORKSPACE_FILES = [
20
+ "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
21
+ "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
22
+ ];
23
+
16
24
  type MemoryRouterConfig = {
17
25
  key: string;
18
26
  endpoint?: string;
19
- density?: 'low' | 'high' | 'xhigh';
27
+ density?: "low" | "high" | "xhigh";
28
+ mode?: "relay" | "proxy";
20
29
  };
21
30
 
31
+ // ──────────────────────────────────────────────────────
32
+ // Helpers
33
+ // ──────────────────────────────────────────────────────
34
+
22
35
  function resolveOpenClawInvocation(): { command: string; args: string[] } {
23
36
  const entry = process.argv[1];
24
- if (entry) {
25
- return {
26
- command: process.execPath,
27
- args: [entry],
28
- };
29
- }
30
-
31
- return {
32
- command: "openclaw",
33
- args: [],
34
- };
37
+ if (entry) return { command: process.execPath, args: [entry] };
38
+ return { command: "openclaw", args: [] };
35
39
  }
36
40
 
37
41
  async function runOpenClawConfigSet(path: string, value: string, json = false): Promise<void> {
38
42
  const base = resolveOpenClawInvocation();
39
43
  const args = [...base.args, "config", "set", path, value];
40
- if (json) {
41
- args.push("--json");
42
- }
44
+ if (json) args.push("--json");
43
45
 
44
46
  await new Promise<void>((resolve, reject) => {
45
47
  const child = spawn(base.command, args, {
46
48
  stdio: ["ignore", "ignore", "pipe"],
47
49
  env: process.env,
48
50
  });
49
-
50
51
  let stderr = "";
51
- child.stderr.on("data", (chunk) => {
52
- stderr += String(chunk);
53
- });
54
-
55
- child.on("error", (err) => reject(err));
52
+ child.stderr.on("data", (chunk) => { stderr += String(chunk); });
53
+ child.on("error", reject);
56
54
  child.on("close", (code) => {
57
- if (code === 0) {
58
- resolve();
59
- return;
60
- }
61
- reject(new Error(`openclaw config set failed (exit ${code}): ${stderr.trim()}`));
55
+ if (code === 0) resolve();
56
+ else reject(new Error(`openclaw config set failed (exit ${code}): ${stderr.trim()}`));
62
57
  });
63
58
  });
64
59
  }
65
60
 
61
+ type CompatApi = OpenClawPluginApi & {
62
+ updatePluginConfig?: (config: Record<string, unknown>) => Promise<void>;
63
+ updatePluginEnabled?: (enabled: boolean) => Promise<void>;
64
+ };
65
+
66
66
  async function setPluginConfig(api: OpenClawPluginApi, config: Record<string, unknown>): Promise<void> {
67
- const anyApi = api as any;
68
- if (typeof anyApi.updatePluginConfig === "function") {
69
- await anyApi.updatePluginConfig(config);
67
+ const compat = api as CompatApi;
68
+ if (typeof compat.updatePluginConfig === "function") {
69
+ await compat.updatePluginConfig(config);
70
70
  return;
71
71
  }
72
-
73
72
  await runOpenClawConfigSet(`plugins.entries.${api.id}.config`, JSON.stringify(config), true);
74
73
  }
75
74
 
76
75
  async function setPluginEnabled(api: OpenClawPluginApi, enabled: boolean): Promise<void> {
77
- const anyApi = api as any;
78
- if (typeof anyApi.updatePluginEnabled === "function") {
79
- await anyApi.updatePluginEnabled(enabled);
76
+ const compat = api as CompatApi;
77
+ if (typeof compat.updatePluginEnabled === "function") {
78
+ await compat.updatePluginEnabled(enabled);
80
79
  return;
81
80
  }
82
-
83
81
  await runOpenClawConfigSet(`plugins.entries.${api.id}.enabled`, enabled ? "true" : "false", true);
84
82
  }
85
83
 
84
+ /**
85
+ * Read all workspace files and return as a single text blob for token counting.
86
+ */
87
+ async function readWorkspaceFiles(workspaceDir: string): Promise<string> {
88
+ const parts: string[] = [];
89
+ for (const file of WORKSPACE_FILES) {
90
+ try {
91
+ const content = await readFile(join(workspaceDir, file), "utf8");
92
+ if (content.trim()) parts.push(`## ${file}\n${content}`);
93
+ } catch { /* file doesn't exist — skip */ }
94
+ }
95
+ return parts.join("\n\n");
96
+ }
97
+
98
+ /**
99
+ * Build a text representation of tools config for token counting.
100
+ */
101
+ function serializeToolsConfig(config: Record<string, unknown>): string {
102
+ const tools = config.tools;
103
+ if (!tools) return "";
104
+ try {
105
+ return `## Tools Config\n${JSON.stringify(tools, null, 2)}`;
106
+ } catch { return ""; }
107
+ }
108
+
109
+ /**
110
+ * Build a text representation of skills for token counting.
111
+ */
112
+ function serializeSkillsConfig(config: Record<string, unknown>): string {
113
+ const skills = (config as any).skills?.entries;
114
+ if (!skills || typeof skills !== "object") return "";
115
+ try {
116
+ const names = Object.keys(skills);
117
+ return `## Skills (${names.length})\n${names.join(", ")}`;
118
+ } catch { return ""; }
119
+ }
120
+
121
+ // ──────────────────────────────────────────────────────
122
+ // Plugin Definition
123
+ // ──────────────────────────────────────────────────────
124
+
86
125
  const memoryRouterPlugin = {
87
126
  id: "mr-memory",
88
127
  name: "MemoryRouter",
@@ -92,34 +131,96 @@ const memoryRouterPlugin = {
92
131
  const cfg = api.pluginConfig as MemoryRouterConfig | undefined;
93
132
  const endpoint = cfg?.endpoint?.replace(/\/v1\/?$/, "") || DEFAULT_ENDPOINT;
94
133
  const memoryKey = cfg?.key;
95
- const density = cfg?.density || 'high';
96
-
97
- // ==================================================================
98
- // Core: Relay architecture — memory via API hooks (no patching needed)
99
- // ==================================================================
134
+ const density = cfg?.density || "high";
135
+ const mode = cfg?.mode || "relay";
100
136
 
101
137
  if (memoryKey) {
102
- api.logger.info?.(`memoryrouter: registered (endpoint: ${endpoint})`);
138
+ api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}..., mode: ${mode})`);
103
139
  } else {
104
140
  api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
105
141
  }
106
142
 
143
+ // ==================================================================
144
+ // Core: before_agent_start — search memories, inject context
145
+ // ==================================================================
146
+
107
147
  if (memoryKey) {
108
- // RETRIEVAL inject relevant memories before every agent turn
109
- api.on("before_agent_start", async (event: Record<string, unknown>) => {
110
- const prompt = event.prompt as string | undefined;
111
- if (!prompt || prompt.length < 5) return;
148
+ // Track whether we've already fired for this prompt (dedup double-fire)
149
+ let lastPreparedPrompt = "";
112
150
 
151
+ api.on("before_prompt_build", async (event, ctx) => {
113
152
  try {
153
+ const prompt = event.prompt;
154
+
155
+ // Deduplicate — if we already prepared this exact prompt, skip
156
+ if (prompt === lastPreparedPrompt && lastPreparedPrompt !== "") {
157
+ return;
158
+ }
159
+ lastPreparedPrompt = prompt;
160
+
161
+ // 1. Read workspace files for full token count
162
+ const workspaceDir = ctx.workspaceDir || "";
163
+ let workspaceText = "";
164
+ if (workspaceDir) {
165
+ workspaceText = await readWorkspaceFiles(workspaceDir);
166
+ }
167
+
168
+ // 2. Serialize tools + skills from config
169
+ const toolsText = serializeToolsConfig(api.config as unknown as Record<string, unknown>);
170
+ const skillsText = serializeSkillsConfig(api.config as unknown as Record<string, unknown>);
171
+
172
+ // 3. Build full context payload (messages + workspace + tools + skills)
173
+ const contextPayload: Array<{ role: string; content: string }> = [];
174
+
175
+ // Add workspace context as a system-level entry
176
+ const fullContext = [workspaceText, toolsText, skillsText].filter(Boolean).join("\n\n");
177
+ if (fullContext) {
178
+ contextPayload.push({ role: "system", content: fullContext });
179
+ }
180
+
181
+ // Add conversation history
182
+ if (event.messages && Array.isArray(event.messages)) {
183
+ let skipped = 0;
184
+ for (const msg of event.messages) {
185
+ const m = msg as { role?: string; content?: unknown };
186
+ if (!m.role) continue;
187
+
188
+ let text = "";
189
+ if (typeof m.content === "string") {
190
+ text = m.content;
191
+ } else if (Array.isArray(m.content)) {
192
+ // Handle Anthropic-style content blocks [{type:"text", text:"..."}, ...]
193
+ text = (m.content as Array<{ type?: string; text?: string }>)
194
+ .filter(b => b.type === "text" && b.text)
195
+ .map(b => b.text!)
196
+ .join("\n");
197
+ }
198
+
199
+ if (text) {
200
+ contextPayload.push({ role: m.role, content: text });
201
+ } else {
202
+ skipped++;
203
+ }
204
+ }
205
+ }
206
+
207
+ // Add current user prompt
208
+ contextPayload.push({ role: "user", content: prompt });
209
+
210
+ // 4. Call /v1/memory/prepare
211
+ const densityMap: Record<string, number> = { low: 40, high: 80, xhigh: 160 };
212
+ const contextLimit = densityMap[density] || 80;
213
+
114
214
  const res = await fetch(`${endpoint}/v1/memory/prepare`, {
115
215
  method: "POST",
116
216
  headers: {
117
- "Authorization": `Bearer ${memoryKey}`,
118
217
  "Content-Type": "application/json",
218
+ Authorization: `Bearer ${memoryKey}`,
119
219
  },
120
220
  body: JSON.stringify({
121
- messages: [{ role: "user", content: prompt }],
221
+ messages: contextPayload,
122
222
  density,
223
+ context_limit: contextLimit,
123
224
  }),
124
225
  });
125
226
 
@@ -128,94 +229,112 @@ const memoryRouterPlugin = {
128
229
  return;
129
230
  }
130
231
 
131
- const data = await res.json() as { context: string | null; tokens_billed: number; memories_found: number };
232
+ const data = (await res.json()) as {
233
+ context?: string;
234
+ memories_found?: number;
235
+ tokens_billed?: number;
236
+ };
132
237
 
133
238
  if (data.context) {
134
- api.logger.info?.(`memoryrouter: injected ${data.memories_found} memories (${data.tokens_billed} tokens)`);
239
+ api.logger.info?.(
240
+ `memoryrouter: injected ${data.memories_found || 0} memories (${data.tokens_billed || 0} tokens billed)`,
241
+ );
135
242
  return { prependContext: data.context };
136
243
  }
137
244
  } catch (err) {
138
- api.logger.warn?.(`memoryrouter: prepare error: ${String(err)}`);
245
+ api.logger.warn?.(
246
+ `memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`,
247
+ );
139
248
  }
140
249
  });
141
250
 
142
- // STORAGE — capture conversation after every agent turn
143
- // Only stores the LAST user message + assistant response per turn (no duplication)
144
- api.on("agent_end", async (event: Record<string, unknown>) => {
145
- if (!event.success || !event.messages) return;
146
-
147
- const msgs = event.messages as Array<Record<string, unknown>>;
148
- if (!msgs.length) return;
251
+ // ==================================================================
252
+ // Core: agent_end store this turn's conversation
253
+ // ==================================================================
149
254
 
255
+ api.on("agent_end", async (event, ctx) => {
150
256
  try {
151
- // Walk backwards: find the last assistant, then the last user before it
152
- let assistantMsg: { role: string; content: string } | null = null;
153
- let userMsg: { role: string; content: string } | null = null;
154
-
155
- for (let i = msgs.length - 1; i >= 0; i--) {
156
- const role = msgs[i].role as string;
157
- if (!role) continue;
158
-
159
- // Extract text content
160
- let text = "";
161
- const content = msgs[i].content;
162
- if (typeof content === "string") {
163
- text = content;
164
- } else if (Array.isArray(content)) {
165
- for (const block of content as Array<Record<string, unknown>>) {
166
- if (typeof block.text === "string") {
167
- text += (text ? "\n" : "") + block.text;
168
- }
169
- }
257
+ const msgs = event.messages;
258
+ if (!msgs || !Array.isArray(msgs) || msgs.length === 0) return;
259
+
260
+ // Extract text from a message (handles string + content block arrays)
261
+ function extractText(content: unknown): string {
262
+ if (typeof content === "string") return content;
263
+ if (Array.isArray(content)) {
264
+ return (content as Array<{ type?: string; text?: string }>)
265
+ .filter(b => b.type === "text" && b.text)
266
+ .map(b => b.text!)
267
+ .join("\n");
170
268
  }
269
+ return "";
270
+ }
171
271
 
172
- if (!text || text.length < 10) continue;
173
-
174
- if (!assistantMsg && role === "assistant") {
175
- assistantMsg = { role, content: text };
176
- }
177
- if (assistantMsg && !userMsg && role === "user") {
178
- // Strip any injected memory context from user messages
179
- let cleanText = text;
180
- if (cleanText.includes("[Memory Context]")) {
181
- cleanText = cleanText.replace(/\[Memory Context\][\s\S]*?\n\n/g, "").trim();
182
- }
183
- if (cleanText.length >= 10) {
184
- userMsg = { role, content: cleanText };
185
- }
272
+ // Find the last user message, then collect ALL assistant messages after it
273
+ // This captures the full response even if she sent multiple messages
274
+ let lastUserIdx = -1;
275
+ for (let i = msgs.length - 1; i >= 0; i--) {
276
+ const msg = msgs[i] as { role?: string; content?: unknown };
277
+ const text = extractText(msg.content);
278
+ if (msg.role === "user" && text) {
279
+ lastUserIdx = i;
186
280
  break;
187
281
  }
188
282
  }
189
283
 
190
- // Only store this turn's pair — no duplication
191
284
  const toStore: Array<{ role: string; content: string }> = [];
192
- if (userMsg) toStore.push(userMsg);
193
- if (assistantMsg) toStore.push(assistantMsg);
285
+
286
+ // Add the user message
287
+ if (lastUserIdx >= 0) {
288
+ const userMsg = msgs[lastUserIdx] as { content?: unknown };
289
+ const userText = extractText(userMsg.content);
290
+ if (userText) toStore.push({ role: "user", content: userText });
291
+ }
292
+
293
+ // Collect ALL assistant messages after the last user message
294
+ const assistantParts: string[] = [];
295
+ for (let i = (lastUserIdx >= 0 ? lastUserIdx + 1 : 0); i < msgs.length; i++) {
296
+ const msg = msgs[i] as { role?: string; content?: unknown };
297
+ if (msg.role === "assistant") {
298
+ const text = extractText(msg.content);
299
+ if (text) assistantParts.push(text);
300
+ }
301
+ }
302
+ if (assistantParts.length > 0) {
303
+ toStore.push({ role: "assistant", content: assistantParts.join("\n\n") });
304
+ }
194
305
 
195
306
  if (toStore.length === 0) return;
196
307
 
197
- const res = await fetch(`${endpoint}/v1/memory/ingest`, {
308
+ // Fire and forget don't block the response
309
+ fetch(`${endpoint}/v1/memory/ingest`, {
198
310
  method: "POST",
199
311
  headers: {
200
- "Authorization": `Bearer ${memoryKey}`,
201
312
  "Content-Type": "application/json",
313
+ Authorization: `Bearer ${memoryKey}`,
202
314
  },
203
315
  body: JSON.stringify({
204
316
  messages: toStore,
317
+ session_id: ctx.sessionKey,
205
318
  }),
319
+ }).then(async (res) => {
320
+ if (!res.ok) {
321
+ api.logger.warn?.(`memoryrouter: ingest failed (${res.status})`);
322
+ }
323
+ }).catch((err) => {
324
+ api.logger.warn?.(
325
+ `memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`,
326
+ );
206
327
  });
207
-
208
- if (!res.ok) {
209
- api.logger.warn?.(`memoryrouter: ingest failed (${res.status})`);
210
- }
211
328
  } catch (err) {
212
- api.logger.warn?.(`memoryrouter: ingest error: ${String(err)}`);
329
+ api.logger.warn?.(
330
+ `memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`,
331
+ );
213
332
  }
214
333
  });
215
- } // end if (memoryKey) for relay hooks
334
+ } // end if (memoryKey)
216
335
 
217
336
  // ==================================================================
218
- // CLI Commands (always registered — even without key, for enable/off)
337
+ // CLI Commands
219
338
  // ==================================================================
220
339
 
221
340
  api.registerCli(
@@ -225,7 +344,6 @@ const memoryRouterPlugin = {
225
344
  console.error("Invalid key format. Keys start with 'mk' (e.g. mk_xxx)");
226
345
  return;
227
346
  }
228
-
229
347
  try {
230
348
  await setPluginConfig(api, { key });
231
349
  await setPluginEnabled(api, true);
@@ -234,30 +352,22 @@ const memoryRouterPlugin = {
234
352
  } catch (err) {
235
353
  const message = err instanceof Error ? err.message : String(err);
236
354
  console.error(`Failed to enable MemoryRouter: ${message}`);
237
- console.error("Fallback: openclaw config set plugins.entries.mr-memory.config.key <key>");
238
- console.error("Then: openclaw config set plugins.entries.mr-memory.enabled true --json");
239
355
  }
240
356
  };
241
357
 
242
- const mr = program.command("mr")
358
+ const mr = program
359
+ .command("mr")
243
360
  .description("MemoryRouter memory commands")
244
361
  .argument("[key]", "Your MemoryRouter memory key (mk_xxx)")
245
362
  .action(async (key: string | undefined) => {
246
- if (!key) {
247
- // No key provided — show help
248
- mr.help();
249
- return;
250
- }
363
+ if (!key) { mr.help(); return; }
251
364
  await applyKey(key);
252
365
  });
253
366
 
254
- // Backward compat: `openclaw mr enable <key>` still works
255
367
  mr.command("enable")
256
368
  .description("Enable MemoryRouter with a memory key (alias)")
257
369
  .argument("<key>", "Your MemoryRouter memory key (mk_xxx)")
258
- .action(async (key: string) => {
259
- await applyKey(key);
260
- });
370
+ .action(async (key: string) => { await applyKey(key); });
261
371
 
262
372
  mr.command("off")
263
373
  .description("Disable MemoryRouter (removes key)")
@@ -265,143 +375,110 @@ const memoryRouterPlugin = {
265
375
  try {
266
376
  await setPluginConfig(api, {});
267
377
  await setPluginEnabled(api, false);
268
- console.log("✓ MemoryRouter disabled. LLM calls go direct to provider.");
269
- console.log(" Key removed. Re-enable with: openclaw mr <key>");
378
+ console.log("✓ MemoryRouter disabled.");
270
379
  } catch (err) {
271
- console.error(`Failed to disable MemoryRouter: ${err instanceof Error ? err.message : String(err)}`);
380
+ console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
272
381
  }
273
382
  });
274
383
 
275
384
  // Density commands
276
- mr.command("xhigh")
277
- .description("Set memory density to xhigh (160 results, ~50k tokens)")
278
- .action(async () => {
279
- if (!memoryKey) {
280
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
281
- return;
282
- }
283
- try {
284
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: 'xhigh' });
285
- console.log("✓ Memory density set to xhigh (160 results, ~50k tokens)");
286
- console.log(" Maximum context injection for deepest memory.");
287
- } catch (err) {
288
- console.error(`Failed to set density: ${err instanceof Error ? err.message : String(err)}`);
289
- }
290
- });
291
-
292
- mr.command("high")
293
- .description("Set memory density to high (80 results, ~24k tokens) [default]")
294
- .action(async () => {
295
- if (!memoryKey) {
296
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
297
- return;
298
- }
299
- try {
300
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: 'high' });
301
- console.log("✓ Memory density set to high (80 results, ~24k tokens)");
302
- } catch (err) {
303
- console.error(`Failed to set density: ${err instanceof Error ? err.message : String(err)}`);
304
- }
305
- });
385
+ for (const [name, desc] of [
386
+ ["xhigh", "Set density to xhigh (160 results, ~50k tokens)"],
387
+ ["high", "Set density to high (80 results, ~24k tokens) [default]"],
388
+ ["low", "Set density to low (40 results, ~12k tokens)"],
389
+ ] as const) {
390
+ mr.command(name)
391
+ .description(desc)
392
+ .action(async () => {
393
+ if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
394
+ try {
395
+ await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: name });
396
+ console.log(`✓ Memory density set to ${name}`);
397
+ } catch (err) {
398
+ console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
399
+ }
400
+ });
401
+ }
306
402
 
307
- mr.command("low")
308
- .description("Set memory density to low (40 results, ~12k tokens)")
309
- .action(async () => {
310
- if (!memoryKey) {
311
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
312
- return;
313
- }
314
- try {
315
- await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density: 'low' });
316
- console.log("✓ Memory density set to low (40 results, ~12k tokens)");
317
- console.log(" Lighter context for faster responses or smaller models.");
318
- } catch (err) {
319
- console.error(`Failed to set density: ${err instanceof Error ? err.message : String(err)}`);
320
- }
321
- });
403
+ // Mode commands
404
+ for (const [modeName, modeDesc] of [
405
+ ["relay", "Relay mode — hooks only, works on stock OpenClaw [default]"],
406
+ ["proxy", "Proxy mode — memory on every LLM call (requires registerStreamFnWrapper)"],
407
+ ] as const) {
408
+ mr.command(modeName)
409
+ .description(modeDesc)
410
+ .action(async () => {
411
+ if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
412
+ try {
413
+ await setPluginConfig(api, { key: memoryKey, endpoint: cfg?.endpoint, density, mode: modeName });
414
+ console.log(`✓ Mode set to ${modeName}`);
415
+ } catch (err) {
416
+ console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
417
+ }
418
+ });
419
+ }
322
420
 
323
421
  mr.command("status")
324
422
  .description("Show MemoryRouter vault stats")
325
423
  .option("--json", "JSON output")
326
424
  .action(async (opts) => {
327
- if (!memoryKey) {
328
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
329
- return;
330
- }
425
+ if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
331
426
  try {
332
427
  const res = await fetch(`${endpoint}/v1/memory/stats`, {
333
428
  headers: { Authorization: `Bearer ${memoryKey}` },
334
429
  });
335
- const data = await res.json() as Record<string, unknown>;
336
-
430
+ const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
337
431
  if (opts.json) {
338
- console.log(JSON.stringify({ enabled: true, key: memoryKey, density, stats: data }, null, 2));
432
+ console.log(JSON.stringify({ enabled: true, key: memoryKey, density, mode, stats: data }, null, 2));
339
433
  } else {
340
434
  console.log("MemoryRouter Status");
341
435
  console.log("───────────────────────────");
342
436
  console.log(`Enabled: ✓ Yes`);
343
437
  console.log(`Key: ${memoryKey.slice(0, 6)}...${memoryKey.slice(-3)}`);
344
- console.log(`Density: ${density} (openclaw mr xhigh|high|low)`);
438
+ console.log(`Mode: ${mode}`);
439
+ console.log(`Density: ${density}`);
345
440
  console.log(`Endpoint: ${endpoint}`);
346
- console.log("");
347
- console.log("Vault Stats:");
348
- const stats = data as { totalVectors?: number; totalTokens?: number };
349
- console.log(` Memories: ${stats.totalVectors ?? 0}`);
350
- console.log(` Tokens: ${stats.totalTokens ?? 0}`);
441
+ console.log(`Memories: ${data.totalVectors ?? 0}`);
442
+ console.log(`Tokens: ${data.totalTokens ?? 0}`);
351
443
  }
352
444
  } catch (err) {
353
- console.error(`Failed to fetch stats: ${err instanceof Error ? err.message : String(err)}`);
445
+ console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
354
446
  }
355
447
  });
356
448
 
357
449
  mr.command("upload")
358
450
  .description("Upload workspace + session history to vault")
359
451
  .argument("[path]", "Specific file or directory to upload")
360
- .option("--workspace <dir>", "Workspace directory (default: cwd)")
361
- .option("--brain <dir>", "State directory with sessions (default: ~/.openclaw)")
452
+ .option("--workspace <dir>", "Workspace directory")
453
+ .option("--brain <dir>", "State directory with sessions")
362
454
  .action(async (targetPath: string | undefined, opts: { workspace?: string; brain?: string }) => {
363
- if (!memoryKey) {
364
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
365
- return;
366
- }
455
+ if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
367
456
  const os = await import("node:os");
368
457
  const path = await import("node:path");
369
458
  const stateDir = opts.brain ? path.resolve(opts.brain) : path.join(os.homedir(), ".openclaw");
370
- // Use OpenClaw's configured workspace, not cwd
371
- const configWorkspace = api.config.workspace || api.config.agents?.defaults?.workspace;
459
+ const configWorkspace = (api.config as any).workspace || (api.config as any).agents?.defaults?.workspace;
372
460
  const workspacePath = opts.workspace
373
461
  ? path.resolve(opts.workspace)
374
462
  : configWorkspace
375
463
  ? path.resolve(configWorkspace.replace(/^~/, os.homedir()))
376
464
  : path.join(os.homedir(), ".openclaw", "workspace");
377
465
  const { runUpload } = await import("./upload.js");
378
- await runUpload({
379
- memoryKey,
380
- endpoint,
381
- targetPath,
382
- stateDir,
383
- workspacePath,
384
- hasWorkspaceFlag: !!opts.workspace,
385
- hasBrainFlag: !!opts.brain,
386
- });
466
+ await runUpload({ memoryKey, endpoint, targetPath, stateDir, workspacePath, hasWorkspaceFlag: !!opts.workspace, hasBrainFlag: !!opts.brain });
387
467
  });
388
468
 
389
469
  mr.command("delete")
390
470
  .description("Clear all memories from vault")
391
471
  .action(async () => {
392
- if (!memoryKey) {
393
- console.error("MemoryRouter not configured. Run: openclaw mr <key>");
394
- return;
395
- }
472
+ if (!memoryKey) { console.error("Not configured. Run: openclaw mr <key>"); return; }
396
473
  try {
397
474
  const res = await fetch(`${endpoint}/v1/memory`, {
398
475
  method: "DELETE",
399
476
  headers: { Authorization: `Bearer ${memoryKey}` },
400
477
  });
401
- const data = await res.json() as { message?: string };
478
+ const data = (await res.json()) as { message?: string };
402
479
  console.log(`✓ ${data.message || "Vault cleared"}`);
403
480
  } catch (err) {
404
- console.error(`Failed to clear vault: ${err instanceof Error ? err.message : String(err)}`);
481
+ console.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
405
482
  }
406
483
  });
407
484
  },
@@ -415,9 +492,7 @@ const memoryRouterPlugin = {
415
492
  api.registerService({
416
493
  id: "mr-memory",
417
494
  start: () => {
418
- if (memoryKey) {
419
- api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
420
- }
495
+ if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
421
496
  },
422
497
  stop: () => {
423
498
  api.logger.info?.("memoryrouter: stopped");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "MemoryRouter persistent memory plugin for OpenClaw — your AI remembers every conversation",
5
5
  "type": "module",
6
6
  "files": [