mr-memory 3.7.4 → 3.7.6

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.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * BYOK — provider API keys never touch MemoryRouter.
9
9
  */
10
10
  import { readFile, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
11
+ import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory } from "./memoryrouter-api.js";
11
12
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
12
13
  import os from "node:os";
13
14
  const DEFAULT_ENDPOINT = "https://api.memoryrouter.ai";
@@ -265,9 +266,6 @@ const WORKSPACE_FILES = [
265
266
  "IDENTITY.md", "USER.md", "MEMORY.md", "HEARTBEAT.md",
266
267
  "TOOLS.md", "AGENTS.md", "SOUL.md", "BOOTSTRAP.md",
267
268
  ];
268
- // ──────────────────────────────────────────────────────
269
- // Helpers
270
- // ──────────────────────────────────────────────────────
271
269
  function openClawConfigPath() {
272
270
  return process.env.OPENCLAW_CONFIG || join(os.homedir(), ".openclaw", "openclaw.json");
273
271
  }
@@ -291,8 +289,47 @@ async function writeOpenClawConfigFile(config) {
291
289
  if (err?.code !== "ENOENT")
292
290
  throw err;
293
291
  }
294
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
292
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
293
+ `, "utf8");
294
+ }
295
+ // ──────────────────────────────────────────────────────
296
+ // Full-replace: disable OpenClaw's native memory search so MemoryRouter
297
+ // owns the memory_search tool (no name conflict, no native embeddings 429).
298
+ // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
299
+ // Global + subagents: writes agents.defaults.memorySearch.enabled.
300
+ // ──────────────────────────────────────────────────────
301
+ async function setNativeMemorySearchEnabled(enabled) {
302
+ const config = await readOpenClawConfigFile();
303
+ config.agents ??= {};
304
+ config.agents.defaults ??= {};
305
+ config.agents.defaults.memorySearch ??= {};
306
+ if (config.agents.defaults.memorySearch.enabled === enabled) return false; // no-op
307
+ config.agents.defaults.memorySearch.enabled = enabled;
308
+ await writeOpenClawConfigFile(config);
309
+ return true;
310
+ }
311
+
312
+ // Boot-time self-heal: if a key is configured but native search is still on,
313
+ // flip it off so already-installed users are fixed on restart after updating
314
+ // (no need to re-run `openclaw mr <key>`).
315
+ async function selfHealNativeMemorySearch(api) {
316
+ try {
317
+ const config = await readOpenClawConfigFile();
318
+ const ms = config?.agents?.defaults?.memorySearch;
319
+ if (ms?.enabled === false) return; // already disabled — nothing to do
320
+ const changed = await setNativeMemorySearchEnabled(false);
321
+ if (changed) {
322
+ api.logger.info?.(
323
+ "memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.",
324
+ );
325
+ }
326
+ } catch (err) {
327
+ api.logger.info?.(
328
+ `memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`,
329
+ );
330
+ }
295
331
  }
332
+
296
333
  async function updateOpenClawPluginEntry(api, patch) {
297
334
  const config = await readOpenClawConfigFile();
298
335
  config.plugins ??= {};
@@ -402,6 +439,8 @@ const memoryRouterPlugin = {
402
439
  api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
403
440
  if (agentKeys)
404
441
  api.logger.info?.(`memoryrouter: ${Object.keys(agentKeys).length} agent key(s) configured`);
442
+ // Self-heal already-configured installs: disable native search if still on.
443
+ void selfHealNativeMemorySearch(api);
405
444
  }
406
445
  else {
407
446
  api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
@@ -545,17 +584,10 @@ If setting up MemoryRouter for another agent:
545
584
  contextPayload.push({ role: "user", content: taskPrompt });
546
585
  }
547
586
  }
548
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
549
- method: "POST",
550
- headers: {
551
- "Content-Type": "application/json",
552
- Authorization: `Bearer ${activeKey}`,
553
- },
554
- body: JSON.stringify({
555
- messages: contextPayload,
556
- density,
557
- ...(embeddings && { embeddings }),
558
- }),
587
+ const res = await prepareMemory(endpoint, activeKey, {
588
+ messages: contextPayload,
589
+ density,
590
+ ...(embeddings && { embeddings }),
559
591
  });
560
592
  if (!res.ok) {
561
593
  const errBody = await res.json().catch(() => null);
@@ -650,17 +682,10 @@ If setting up MemoryRouter for another agent:
650
682
  }
651
683
  }
652
684
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
653
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
654
- method: "POST",
655
- headers: {
656
- "Content-Type": "application/json",
657
- Authorization: `Bearer ${activeKey}`,
658
- },
659
- body: JSON.stringify({
660
- messages: contextPayload,
661
- density,
662
- ...(embeddings && { embeddings }),
663
- }),
685
+ const res = await prepareMemory(endpoint, activeKey, {
686
+ messages: contextPayload,
687
+ density,
688
+ ...(embeddings && { embeddings }),
664
689
  });
665
690
  if (!res.ok) {
666
691
  const errBody = await res.json().catch(() => null);
@@ -762,17 +787,10 @@ If setting up MemoryRouter for another agent:
762
787
  return;
763
788
  }
764
789
  // Fire and forget — don't block message delivery waiting for ingest
765
- fetch(`${endpoint}/v1/memory/ingest`, {
766
- method: "POST",
767
- headers: {
768
- "Content-Type": "application/json",
769
- Authorization: `Bearer ${activeKey}`,
770
- },
771
- body: JSON.stringify({
772
- messages: toStore,
773
- model: "unknown",
774
- ...(embeddings && { embeddings }),
775
- }),
790
+ ingestMemory(endpoint, activeKey, {
791
+ messages: toStore,
792
+ model: "unknown",
793
+ ...(embeddings && { embeddings }),
776
794
  }).then(async (res) => {
777
795
  if (!res.ok) {
778
796
  const details = await res.text().catch(() => "");
@@ -822,15 +840,7 @@ If setting up MemoryRouter for another agent:
822
840
  return jsonToolResult({ results: [], error: "query required" });
823
841
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
824
842
  try {
825
- const res = await fetch(`${endpoint}/v1/memory/search`, {
826
- method: "POST",
827
- headers: {
828
- Authorization: `Bearer ${toolKey}`,
829
- "Content-Type": "application/json",
830
- ...(embeddings && { "X-Embedding-Model": embeddings }),
831
- },
832
- body: JSON.stringify({ query, limit }),
833
- });
843
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
834
844
  if (!res.ok) {
835
845
  const errBody = await res.text().catch(() => "");
836
846
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -911,6 +921,12 @@ If setting up MemoryRouter for another agent:
911
921
  try {
912
922
  await setPluginConfig(api, { key });
913
923
  await setPluginEnabled(api, true);
924
+ try {
925
+ const changed = await setNativeMemorySearchEnabled(false);
926
+ if (changed) console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
927
+ } catch (err) {
928
+ console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
929
+ }
914
930
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
915
931
  console.log(` • memory_search + memory_get tools registered via plugin`);
916
932
  console.log(`\nRun: openclaw mr upload to upload your memories`);
@@ -943,6 +959,13 @@ If setting up MemoryRouter for another agent:
943
959
  // This avoids the catch-22 where disabling the plugin removes the `mr` command
944
960
  await setPluginConfig(api, {});
945
961
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
962
+ // Restore native memory search so the user isn't left with NO memory search.
963
+ try {
964
+ const changed = await setNativeMemorySearchEnabled(true);
965
+ if (changed) console.log(" • Native memory search restored");
966
+ } catch (err) {
967
+ console.error(` ⚠ Could not restore native memory search: ${err instanceof Error ? err.message : String(err)}`);
968
+ }
946
969
  console.log("✓ MemoryRouter disabled (key cleared).");
947
970
  console.log(" • CLI still available — run `openclaw mr <key>` to re-enable");
948
971
  }
@@ -1023,12 +1046,7 @@ If setting up MemoryRouter for another agent:
1023
1046
  return;
1024
1047
  }
1025
1048
  try {
1026
- const statsUrl = embeddings
1027
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1028
- : `${endpoint}/v1/memory/stats`;
1029
- const res = await fetch(statsUrl, {
1030
- headers: { Authorization: `Bearer ${effectiveKey}` },
1031
- });
1049
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1032
1050
  const data = (await res.json());
1033
1051
  if (opts.json) {
1034
1052
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1086,15 +1104,7 @@ If setting up MemoryRouter for another agent:
1086
1104
  }
1087
1105
  const limit = parseInt(opts.limit, 10) || 50;
1088
1106
  try {
1089
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1090
- method: "POST",
1091
- headers: {
1092
- Authorization: `Bearer ${effectiveKey}`,
1093
- "Content-Type": "application/json",
1094
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1095
- },
1096
- body: JSON.stringify({ query, limit }),
1097
- });
1107
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1098
1108
  if (!res.ok) {
1099
1109
  const err = await res.json();
1100
1110
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1142,13 +1152,7 @@ If setting up MemoryRouter for another agent:
1142
1152
  return;
1143
1153
  }
1144
1154
  try {
1145
- const deleteUrl = embeddings
1146
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1147
- : `${endpoint}/v1/memory`;
1148
- const res = await fetch(deleteUrl, {
1149
- method: "DELETE",
1150
- headers: { Authorization: `Bearer ${effectiveKey}` },
1151
- });
1155
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1152
1156
  const data = (await res.json());
1153
1157
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1154
1158
  console.log(`✓ ${data.message || `Vault cleared${modelLabel}`}`);
package/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { readFile, readdir, stat, lstat, writeFile, copyFile, mkdir } from "node:fs/promises";
12
+ import { deleteMemories, getMemoryStats, ingestMemory, prepareMemory, searchMemory } from "./memoryrouter-api.js";
12
13
  import { join, resolve, relative, isAbsolute, sep, dirname } from "node:path";
13
14
  import path from "node:path";
14
15
  import os from "node:os";
@@ -319,6 +320,44 @@ async function writeOpenClawConfigFile(config: JsonObject): Promise<void> {
319
320
  `, "utf8");
320
321
  }
321
322
 
323
+ // ──────────────────────────────────────────────────────
324
+ // Full-replace: disable OpenClaw's native memory search so MemoryRouter
325
+ // owns the memory_search tool (no name conflict, no native embeddings 429).
326
+ // memoryFlush (durable MEMORY.md writes) is intentionally left untouched.
327
+ // Global + subagents: writes agents.defaults.memorySearch.enabled.
328
+ // ──────────────────────────────────────────────────────
329
+ async function setNativeMemorySearchEnabled(enabled: boolean): Promise<boolean> {
330
+ const config = await readOpenClawConfigFile();
331
+ config.agents ??= {};
332
+ config.agents.defaults ??= {};
333
+ config.agents.defaults.memorySearch ??= {};
334
+ if (config.agents.defaults.memorySearch.enabled === enabled) return false; // no-op
335
+ config.agents.defaults.memorySearch.enabled = enabled;
336
+ await writeOpenClawConfigFile(config);
337
+ return true;
338
+ }
339
+
340
+ // Boot-time self-heal: if a key is configured but native search is still on,
341
+ // flip it off so already-installed users are fixed on restart after updating
342
+ // (no need to re-run `openclaw mr <key>`).
343
+ async function selfHealNativeMemorySearch(api: OpenClawPluginApi): Promise<void> {
344
+ try {
345
+ const config = await readOpenClawConfigFile();
346
+ const ms = config?.agents?.defaults?.memorySearch;
347
+ if (ms?.enabled === false) return; // already disabled — nothing to do
348
+ const changed = await setNativeMemorySearchEnabled(false);
349
+ if (changed) {
350
+ api.logger.info?.(
351
+ "memoryrouter: disabled native memory search (full-replace). Restart gateway to apply.",
352
+ );
353
+ }
354
+ } catch (err) {
355
+ api.logger.info?.(
356
+ `memoryrouter: self-heal skipped — ${err instanceof Error ? err.message : String(err)}`,
357
+ );
358
+ }
359
+ }
360
+
322
361
  async function updateOpenClawPluginEntry(api: OpenClawPluginApi, patch: JsonObject): Promise<void> {
323
362
  const config = await readOpenClawConfigFile();
324
363
  config.plugins ??= {};
@@ -425,6 +464,8 @@ const memoryRouterPlugin = {
425
464
  if (hasAnyKey) {
426
465
  if (memoryKey) api.logger.info?.(`memoryrouter: active (key: ${memoryKey.slice(0, 6)}...)`);
427
466
  if (agentKeys) api.logger.info?.(`memoryrouter: ${Object.keys(agentKeys).length} agent key(s) configured`);
467
+ // Self-heal already-configured installs: disable native search if still on.
468
+ void selfHealNativeMemorySearch(api);
428
469
  } else {
429
470
  api.logger.info?.("memoryrouter: no key configured — run: openclaw mr <key>");
430
471
  api.logger.info?.("memoryrouter: get your free API key at https://memoryrouter.ai");
@@ -570,17 +611,10 @@ If setting up MemoryRouter for another agent:
570
611
  }
571
612
 
572
613
 
573
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
574
- method: "POST",
575
- headers: {
576
- "Content-Type": "application/json",
577
- Authorization: `Bearer ${activeKey}`,
578
- },
579
- body: JSON.stringify({
580
- messages: contextPayload,
581
- density,
582
- ...(embeddings && { embeddings }),
583
- }),
614
+ const res = await prepareMemory(endpoint, activeKey, {
615
+ messages: contextPayload,
616
+ density,
617
+ ...(embeddings && { embeddings }),
584
618
  });
585
619
 
586
620
  if (!res.ok) {
@@ -686,17 +720,10 @@ If setting up MemoryRouter for another agent:
686
720
 
687
721
  // 4. Call /v1/memory/prepare (no session_id — always search core vault)
688
722
 
689
- const res = await fetch(`${endpoint}/v1/memory/prepare`, {
690
- method: "POST",
691
- headers: {
692
- "Content-Type": "application/json",
693
- Authorization: `Bearer ${activeKey}`,
694
- },
695
- body: JSON.stringify({
696
- messages: contextPayload,
697
- density,
698
- ...(embeddings && { embeddings }),
699
- }),
723
+ const res = await prepareMemory(endpoint, activeKey, {
724
+ messages: contextPayload,
725
+ density,
726
+ ...(embeddings && { embeddings }),
700
727
  });
701
728
 
702
729
  if (!res.ok) {
@@ -811,17 +838,10 @@ If setting up MemoryRouter for another agent:
811
838
  }
812
839
 
813
840
  // Fire and forget — don't block message delivery waiting for ingest
814
- fetch(`${endpoint}/v1/memory/ingest`, {
815
- method: "POST",
816
- headers: {
817
- "Content-Type": "application/json",
818
- Authorization: `Bearer ${activeKey}`,
819
- },
820
- body: JSON.stringify({
821
- messages: toStore,
822
- model: "unknown",
823
- ...(embeddings && { embeddings }),
824
- }),
841
+ ingestMemory(endpoint, activeKey, {
842
+ messages: toStore,
843
+ model: "unknown",
844
+ ...(embeddings && { embeddings }),
825
845
  }).then(async (res) => {
826
846
  if (!res.ok) {
827
847
  const details = await res.text().catch(() => "");
@@ -869,15 +889,7 @@ If setting up MemoryRouter for another agent:
869
889
  if (!query) return jsonToolResult({ results: [], error: "query required" });
870
890
  const limit = typeof params.maxResults === "number" ? params.maxResults : 50;
871
891
  try {
872
- const res = await fetch(`${endpoint}/v1/memory/search`, {
873
- method: "POST",
874
- headers: {
875
- Authorization: `Bearer ${toolKey}`,
876
- "Content-Type": "application/json",
877
- ...(embeddings && { "X-Embedding-Model": embeddings }),
878
- },
879
- body: JSON.stringify({ query, limit }),
880
- });
892
+ const res = await searchMemory(endpoint, toolKey, { query, limit, embeddings });
881
893
  if (!res.ok) {
882
894
  const errBody = await res.text().catch(() => "");
883
895
  return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
@@ -963,6 +975,12 @@ If setting up MemoryRouter for another agent:
963
975
  try {
964
976
  await setPluginConfig(api, { key });
965
977
  await setPluginEnabled(api, true);
978
+ try {
979
+ const changed = await setNativeMemorySearchEnabled(false);
980
+ if (changed) console.log(` • Native memory search disabled — MemoryRouter now owns memory_search`);
981
+ } catch (err) {
982
+ console.error(` ⚠ Could not disable native memory search: ${err instanceof Error ? err.message : String(err)}`);
983
+ }
966
984
  console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
967
985
  console.log(` • memory_search + memory_get tools registered via plugin`);
968
986
  console.log(`\nRun: openclaw mr upload to upload your memories`);
@@ -994,6 +1012,13 @@ If setting up MemoryRouter for another agent:
994
1012
  // This avoids the catch-22 where disabling the plugin removes the `mr` command
995
1013
  await setPluginConfig(api, {});
996
1014
  // Note: we intentionally do NOT call setPluginEnabled(api, false)
1015
+ // Restore native memory search so the user isn't left with NO memory search.
1016
+ try {
1017
+ const changed = await setNativeMemorySearchEnabled(true);
1018
+ if (changed) console.log(" • Native memory search restored");
1019
+ } catch (err) {
1020
+ console.error(` ⚠ Could not restore native memory search: ${err instanceof Error ? err.message : String(err)}`);
1021
+ }
997
1022
  console.log("✓ MemoryRouter disabled (key cleared).");
998
1023
  console.log(" • CLI still available — run `openclaw mr <key>` to re-enable");
999
1024
  } catch (err) {
@@ -1075,12 +1100,7 @@ If setting up MemoryRouter for another agent:
1075
1100
  return;
1076
1101
  }
1077
1102
  try {
1078
- const statsUrl = embeddings
1079
- ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
1080
- : `${endpoint}/v1/memory/stats`;
1081
- const res = await fetch(statsUrl, {
1082
- headers: { Authorization: `Bearer ${effectiveKey}` },
1083
- });
1103
+ const res = await getMemoryStats(endpoint, effectiveKey, embeddings);
1084
1104
  const data = (await res.json()) as { totalVectors?: number; totalTokens?: number };
1085
1105
  if (opts.json) {
1086
1106
  console.log(JSON.stringify({ enabled: true, key: effectiveKey, density, stats: data }, null, 2));
@@ -1132,15 +1152,7 @@ If setting up MemoryRouter for another agent:
1132
1152
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1133
1153
  const limit = parseInt(opts.limit, 10) || 50;
1134
1154
  try {
1135
- const res = await fetch(`${endpoint}/v1/memory/search`, {
1136
- method: "POST",
1137
- headers: {
1138
- Authorization: `Bearer ${effectiveKey}`,
1139
- "Content-Type": "application/json",
1140
- ...(embeddings && { "X-Embedding-Model": embeddings }),
1141
- },
1142
- body: JSON.stringify({ query, limit }),
1143
- });
1155
+ const res = await searchMemory(endpoint, effectiveKey, { query, limit, embeddings });
1144
1156
  if (!res.ok) {
1145
1157
  const err = await res.json() as { error?: string };
1146
1158
  console.error(`Search failed: ${err.error || `HTTP ${res.status}`}`);
@@ -1205,13 +1217,7 @@ If setting up MemoryRouter for another agent:
1205
1217
  const effectiveKey = opts.key || memoryKey;
1206
1218
  if (!effectiveKey) { console.error("Not configured. Run: openclaw mr <key> or pass --key <key>"); return; }
1207
1219
  try {
1208
- const deleteUrl = embeddings
1209
- ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
1210
- : `${endpoint}/v1/memory`;
1211
- const res = await fetch(deleteUrl, {
1212
- method: "DELETE",
1213
- headers: { Authorization: `Bearer ${effectiveKey}` },
1214
- });
1220
+ const res = await deleteMemories(endpoint, effectiveKey, embeddings);
1215
1221
  const data = (await res.json()) as { message?: string };
1216
1222
  const modelLabel = embeddings ? ` (${embeddings})` : "";
1217
1223
  console.log(`✓ ${data.message || `Vault cleared${modelLabel}`}`);
@@ -0,0 +1,49 @@
1
+ const JSON_HEADERS = { "Content-Type": "application/json" };
2
+ export async function prepareMemory(endpoint, key, payload) {
3
+ return fetch(`${endpoint}/v1/memory/prepare`, {
4
+ method: "POST",
5
+ headers: {
6
+ ...JSON_HEADERS,
7
+ Authorization: `Bearer ${key}`,
8
+ },
9
+ body: JSON.stringify(payload),
10
+ });
11
+ }
12
+ export function ingestMemory(endpoint, key, payload) {
13
+ return fetch(`${endpoint}/v1/memory/ingest`, {
14
+ method: "POST",
15
+ headers: {
16
+ ...JSON_HEADERS,
17
+ Authorization: `Bearer ${key}`,
18
+ },
19
+ body: JSON.stringify(payload),
20
+ });
21
+ }
22
+ export async function searchMemory(endpoint, key, payload) {
23
+ return fetch(`${endpoint}/v1/memory/search`, {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${key}`,
27
+ "Content-Type": "application/json",
28
+ ...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
29
+ },
30
+ body: JSON.stringify({ query: payload.query, limit: payload.limit }),
31
+ });
32
+ }
33
+ export async function getMemoryStats(endpoint, key, embeddings) {
34
+ const statsUrl = embeddings
35
+ ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
36
+ : `${endpoint}/v1/memory/stats`;
37
+ return fetch(statsUrl, {
38
+ headers: { Authorization: `Bearer ${key}` },
39
+ });
40
+ }
41
+ export async function deleteMemories(endpoint, key, embeddings) {
42
+ const deleteUrl = embeddings
43
+ ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
44
+ : `${endpoint}/v1/memory`;
45
+ return fetch(deleteUrl, {
46
+ method: "DELETE",
47
+ headers: { Authorization: `Bearer ${key}` },
48
+ });
49
+ }
@@ -0,0 +1,60 @@
1
+ const JSON_HEADERS = { "Content-Type": "application/json" };
2
+
3
+ type PreparePayload = {
4
+ messages: Array<{ role: string; content: string }>;
5
+ density: string;
6
+ embeddings?: string;
7
+ };
8
+
9
+ export async function prepareMemory(endpoint: string, key: string, payload: PreparePayload): Promise<Response> {
10
+ return fetch(`${endpoint}/v1/memory/prepare`, {
11
+ method: "POST",
12
+ headers: {
13
+ ...JSON_HEADERS,
14
+ Authorization: `Bearer ${key}`,
15
+ },
16
+ body: JSON.stringify(payload),
17
+ });
18
+ }
19
+
20
+ export function ingestMemory(endpoint: string, key: string, payload: { messages: Array<{ role: string; content: string }>; model: string; embeddings?: string }): Promise<Response> {
21
+ return fetch(`${endpoint}/v1/memory/ingest`, {
22
+ method: "POST",
23
+ headers: {
24
+ ...JSON_HEADERS,
25
+ Authorization: `Bearer ${key}`,
26
+ },
27
+ body: JSON.stringify(payload),
28
+ });
29
+ }
30
+
31
+ export async function searchMemory(endpoint: string, key: string, payload: { query: string; limit: number; embeddings?: string }): Promise<Response> {
32
+ return fetch(`${endpoint}/v1/memory/search`, {
33
+ method: "POST",
34
+ headers: {
35
+ Authorization: `Bearer ${key}`,
36
+ "Content-Type": "application/json",
37
+ ...(payload.embeddings && { "X-Embedding-Model": payload.embeddings }),
38
+ },
39
+ body: JSON.stringify({ query: payload.query, limit: payload.limit }),
40
+ });
41
+ }
42
+
43
+ export async function getMemoryStats(endpoint: string, key: string, embeddings?: string): Promise<Response> {
44
+ const statsUrl = embeddings
45
+ ? `${endpoint}/v1/memory/stats?embeddings=${encodeURIComponent(embeddings)}`
46
+ : `${endpoint}/v1/memory/stats`;
47
+ return fetch(statsUrl, {
48
+ headers: { Authorization: `Bearer ${key}` },
49
+ });
50
+ }
51
+
52
+ export async function deleteMemories(endpoint: string, key: string, embeddings?: string): Promise<Response> {
53
+ const deleteUrl = embeddings
54
+ ? `${endpoint}/v1/memory?embeddings=${encodeURIComponent(embeddings)}`
55
+ : `${endpoint}/v1/memory`;
56
+ return fetch(deleteUrl, {
57
+ method: "DELETE",
58
+ headers: { Authorization: `Bearer ${key}` },
59
+ });
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
4
4
  "description": "mr-memory is the MemoryRouter plugin for OpenClaw — persistent memory across every conversation",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,6 +9,10 @@
9
9
  "upload.js",
10
10
  "index.ts",
11
11
  "upload.ts",
12
+ "memoryrouter-api.js",
13
+ "memoryrouter-api.ts",
14
+ "upload-api.js",
15
+ "upload-api.ts",
12
16
  "openclaw.plugin.json"
13
17
  ],
14
18
  "keywords": [
package/upload-api.js ADDED
@@ -0,0 +1,40 @@
1
+ const MAX_HTTP_RETRIES = 3;
2
+ function sleep(ms) {
3
+ return new Promise((resolve) => setTimeout(resolve, ms));
4
+ }
5
+ export async function checkMemoryRouterHealth(endpoint) {
6
+ try {
7
+ const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
8
+ return res.ok;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export async function uploadMemoryBatch(params) {
15
+ const uploadUrl = `${params.endpoint}/v1/memory/upload`;
16
+ let lastError = null;
17
+ for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
18
+ try {
19
+ const res = await fetch(uploadUrl, {
20
+ method: "POST",
21
+ headers: {
22
+ Authorization: `Bearer ${params.memoryKey}`,
23
+ "Content-Type": "text/plain",
24
+ ...(params.embeddings && { "X-Embedding-Model": params.embeddings }),
25
+ },
26
+ body: params.jsonlBody,
27
+ signal: AbortSignal.timeout(30000),
28
+ });
29
+ if (res.ok || res.status < 500)
30
+ return res;
31
+ lastError = new Error(`HTTP ${res.status}`);
32
+ }
33
+ catch (err) {
34
+ lastError = err instanceof Error ? err : new Error(String(err));
35
+ }
36
+ if (attempt < MAX_HTTP_RETRIES)
37
+ await sleep(1000 * attempt);
38
+ }
39
+ throw lastError ?? new Error(`${params.label}: failed after ${MAX_HTTP_RETRIES} attempts`);
40
+ }
package/upload-api.ts ADDED
@@ -0,0 +1,45 @@
1
+ const MAX_HTTP_RETRIES = 3;
2
+
3
+ function sleep(ms: number): Promise<void> {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+
7
+ export async function checkMemoryRouterHealth(endpoint: string): Promise<boolean> {
8
+ try {
9
+ const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
10
+ return res.ok;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ export async function uploadMemoryBatch(params: {
17
+ endpoint: string;
18
+ memoryKey: string;
19
+ jsonlBody: string;
20
+ embeddings?: string;
21
+ label: string;
22
+ }): Promise<Response> {
23
+ const uploadUrl = `${params.endpoint}/v1/memory/upload`;
24
+ let lastError: Error | null = null;
25
+ for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
26
+ try {
27
+ const res = await fetch(uploadUrl, {
28
+ method: "POST",
29
+ headers: {
30
+ Authorization: `Bearer ${params.memoryKey}`,
31
+ "Content-Type": "text/plain",
32
+ ...(params.embeddings && { "X-Embedding-Model": params.embeddings }),
33
+ },
34
+ body: params.jsonlBody,
35
+ signal: AbortSignal.timeout(30000),
36
+ });
37
+ if (res.ok || res.status < 500) return res;
38
+ lastError = new Error(`HTTP ${res.status}`);
39
+ } catch (err) {
40
+ lastError = err instanceof Error ? err : new Error(String(err));
41
+ }
42
+ if (attempt < MAX_HTTP_RETRIES) await sleep(1000 * attempt);
43
+ }
44
+ throw lastError ?? new Error(`${params.label}: failed after ${MAX_HTTP_RETRIES} attempts`);
45
+ }
package/upload.js CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { checkMemoryRouterHealth, uploadMemoryBatch } from "./upload-api.js";
6
7
  // ── Sanitization utilities (shared with index.ts ingest logic)
7
8
  // These MUST stay in sync with the patterns in index.ts
8
9
  const MEMORY_TAG_RE = /<mr-memory>[\s\S]*?<\/mr-memory>\s*/g;
@@ -132,7 +133,6 @@ const MAX_BATCH_BYTES = 2_000_000;
132
133
  const MAX_BATCH_COUNT_DEFAULT = 100;
133
134
  const MAX_BATCH_COUNT_QWEN = 25;
134
135
  const BATCH_SLEEP_MS = 150;
135
- const MAX_HTTP_RETRIES = 3;
136
136
  function sleep(ms) {
137
137
  return new Promise((resolve) => setTimeout(resolve, ms));
138
138
  }
@@ -302,33 +302,10 @@ async function discoverBrainFiles(stateDir) {
302
302
  }
303
303
  return files;
304
304
  }
305
- async function fetchWithRetry(url, init, label) {
306
- let lastError = null;
307
- for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
308
- try {
309
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(30000) });
310
- if (res.ok || res.status < 500)
311
- return res;
312
- lastError = new Error(`HTTP ${res.status}`);
313
- }
314
- catch (err) {
315
- lastError = err instanceof Error ? err : new Error(String(err));
316
- }
317
- if (attempt < MAX_HTTP_RETRIES)
318
- await sleep(1000 * attempt);
319
- }
320
- throw lastError ?? new Error(`${label}: failed after ${MAX_HTTP_RETRIES} attempts`);
321
- }
322
305
  export async function runUpload(params) {
323
306
  const { memoryKey, endpoint, targetPath, stateDir, embeddings } = params;
324
- const uploadUrl = `${endpoint}/v1/memory/upload`;
325
307
  // Validate API reachability
326
- try {
327
- const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
328
- if (!res.ok)
329
- throw new Error(`HTTP ${res.status}`);
330
- }
331
- catch {
308
+ if (!(await checkMemoryRouterHealth(endpoint))) {
332
309
  console.error("Error: Could not reach MemoryRouter API.");
333
310
  return;
334
311
  }
@@ -427,15 +404,13 @@ export async function runUpload(params) {
427
404
  process.stdout.write(` Batch ${i + 1}/${batches.length} (${batch.length} items)... `);
428
405
  }
429
406
  try {
430
- const response = await fetchWithRetry(uploadUrl, {
431
- method: "POST",
432
- headers: {
433
- Authorization: `Bearer ${memoryKey}`,
434
- "Content-Type": "text/plain",
435
- ...(embeddings && { "X-Embedding-Model": embeddings }),
436
- },
437
- body: jsonlBody,
438
- }, `Batch ${i + 1}`);
407
+ const response = await uploadMemoryBatch({
408
+ endpoint,
409
+ memoryKey,
410
+ jsonlBody,
411
+ embeddings,
412
+ label: `Batch ${i + 1}`,
413
+ });
439
414
  const result = (await response.json());
440
415
  const batchStored = result.stats?.stored ?? result.stats?.inputItems ?? batch.length;
441
416
  const batchFailed = result.stats?.failed ?? 0;
package/upload.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
+ import { checkMemoryRouterHealth, uploadMemoryBatch } from "./upload-api.js";
7
8
 
8
9
  // ── Sanitization utilities (shared with index.ts ingest logic)
9
10
  // These MUST stay in sync with the patterns in index.ts
@@ -146,7 +147,6 @@ const MAX_BATCH_BYTES = 2_000_000;
146
147
  const MAX_BATCH_COUNT_DEFAULT = 100;
147
148
  const MAX_BATCH_COUNT_QWEN = 25;
148
149
  const BATCH_SLEEP_MS = 150;
149
- const MAX_HTTP_RETRIES = 3;
150
150
 
151
151
  function sleep(ms: number): Promise<void> {
152
152
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -327,25 +327,6 @@ async function discoverBrainFiles(stateDir: string): Promise<string[]> {
327
327
  return files;
328
328
  }
329
329
 
330
- async function fetchWithRetry(
331
- url: string,
332
- init: RequestInit,
333
- label: string,
334
- ): Promise<Response> {
335
- let lastError: Error | null = null;
336
- for (let attempt = 1; attempt <= MAX_HTTP_RETRIES; attempt++) {
337
- try {
338
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(30000) });
339
- if (res.ok || res.status < 500) return res;
340
- lastError = new Error(`HTTP ${res.status}`);
341
- } catch (err) {
342
- lastError = err instanceof Error ? err : new Error(String(err));
343
- }
344
- if (attempt < MAX_HTTP_RETRIES) await sleep(1000 * attempt);
345
- }
346
- throw lastError ?? new Error(`${label}: failed after ${MAX_HTTP_RETRIES} attempts`);
347
- }
348
-
349
330
  export async function runUpload(params: {
350
331
  memoryKey: string;
351
332
  endpoint: string;
@@ -357,13 +338,8 @@ export async function runUpload(params: {
357
338
  embeddings?: string;
358
339
  }): Promise<void> {
359
340
  const { memoryKey, endpoint, targetPath, stateDir, embeddings } = params;
360
- const uploadUrl = `${endpoint}/v1/memory/upload`;
361
-
362
341
  // Validate API reachability
363
- try {
364
- const res = await fetch(`${endpoint}/health`, { signal: AbortSignal.timeout(5000) });
365
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
366
- } catch {
342
+ if (!(await checkMemoryRouterHealth(endpoint))) {
367
343
  console.error("Error: Could not reach MemoryRouter API.");
368
344
  return;
369
345
  }
@@ -467,19 +443,13 @@ export async function runUpload(params: {
467
443
  }
468
444
 
469
445
  try {
470
- const response = await fetchWithRetry(
471
- uploadUrl,
472
- {
473
- method: "POST",
474
- headers: {
475
- Authorization: `Bearer ${memoryKey}`,
476
- "Content-Type": "text/plain",
477
- ...(embeddings && { "X-Embedding-Model": embeddings }),
478
- },
479
- body: jsonlBody,
480
- },
481
- `Batch ${i + 1}`,
482
- );
446
+ const response = await uploadMemoryBatch({
447
+ endpoint,
448
+ memoryKey,
449
+ jsonlBody,
450
+ embeddings,
451
+ label: `Batch ${i + 1}`,
452
+ });
483
453
 
484
454
  const result = (await response.json()) as {
485
455
  stats?: { stored?: number; failed?: number; inputItems?: number; path?: string; diagnostic?: string };