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.
- package/index.ts +227 -128
- 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
|
|
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",
|
|
34
|
-
"
|
|
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) => {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
162
|
-
let
|
|
163
|
-
//
|
|
164
|
-
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 = "";
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
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;
|
|
231
255
|
}
|
|
232
|
-
} catch {
|
|
233
|
-
|
|
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
|
-
//
|
|
244
|
-
if (prompt ===
|
|
266
|
+
// Dedupe only within before_prompt_build path.
|
|
267
|
+
if (prompt === lastPromptBuildPrompt && lastPromptBuildPrompt !== "") {
|
|
245
268
|
return;
|
|
246
269
|
}
|
|
247
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
297
|
-
contextPayload.push({ role: "user", content:
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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) {
|
|
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) => {
|
|
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(
|
|
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) {
|
|
517
|
+
if (!memoryKey) {
|
|
518
|
+
console.error("Not configured. Run: openclaw mr <key>");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
488
521
|
try {
|
|
489
|
-
await setPluginConfig(api, {
|
|
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) {
|
|
565
|
+
if (!memoryKey) {
|
|
566
|
+
console.error("Not configured. Run: openclaw mr <key>");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
506
569
|
try {
|
|
507
|
-
await setPluginConfig(api, {
|
|
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) {
|
|
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(
|
|
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(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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) {
|
|
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",
|