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.
- package/index.ts +281 -206
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,88 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MemoryRouter Plugin for OpenClaw
|
|
2
|
+
* MemoryRouter Plugin for OpenClaw
|
|
3
3
|
*
|
|
4
4
|
* Persistent AI memory via MemoryRouter (memoryrouter.ai).
|
|
5
|
-
* Uses
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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?:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
if (typeof
|
|
69
|
-
await
|
|
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
|
|
78
|
-
if (typeof
|
|
79
|
-
await
|
|
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 ||
|
|
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:
|
|
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
|
-
//
|
|
109
|
-
|
|
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:
|
|
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 {
|
|
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?.(
|
|
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?.(
|
|
245
|
+
api.logger.warn?.(
|
|
246
|
+
`memoryrouter: prepare error — ${err instanceof Error ? err.message : String(err)}`,
|
|
247
|
+
);
|
|
139
248
|
}
|
|
140
249
|
});
|
|
141
250
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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?.(
|
|
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)
|
|
334
|
+
} // end if (memoryKey)
|
|
216
335
|
|
|
217
336
|
// ==================================================================
|
|
218
|
-
// CLI Commands
|
|
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
|
|
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.
|
|
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
|
|
380
|
+
console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
|
|
272
381
|
}
|
|
273
382
|
});
|
|
274
383
|
|
|
275
384
|
// Density commands
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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(`
|
|
438
|
+
console.log(`Mode: ${mode}`);
|
|
439
|
+
console.log(`Density: ${density}`);
|
|
345
440
|
console.log(`Endpoint: ${endpoint}`);
|
|
346
|
-
console.log(
|
|
347
|
-
console.log(
|
|
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
|
|
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
|
|
361
|
-
.option("--brain <dir>", "State directory with sessions
|
|
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
|
-
|
|
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
|
|
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");
|