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.
- package/index.ts +227 -133
- 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
|
|
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",
|
|
37
|
-
"
|
|
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) => {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
165
|
-
let
|
|
166
|
-
//
|
|
167
|
-
let
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return { prependContext:
|
|
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
|
-
|
|
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
|
-
//
|
|
249
|
-
if (prompt ===
|
|
266
|
+
// Dedupe only within before_prompt_build path.
|
|
267
|
+
if (prompt === lastPromptBuildPrompt && lastPromptBuildPrompt !== "") {
|
|
250
268
|
return;
|
|
251
269
|
}
|
|
252
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
302
|
-
contextPayload.push({ role: "user", content:
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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) {
|
|
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) => {
|
|
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(
|
|
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) {
|
|
517
|
+
if (!memoryKey) {
|
|
518
|
+
console.error("Not configured. Run: openclaw mr <key>");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
493
521
|
try {
|
|
494
|
-
await setPluginConfig(api, {
|
|
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) {
|
|
565
|
+
if (!memoryKey) {
|
|
566
|
+
console.error("Not configured. Run: openclaw mr <key>");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
511
569
|
try {
|
|
512
|
-
await setPluginConfig(api, {
|
|
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) {
|
|
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(
|
|
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(
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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) {
|
|
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",
|