mr-memory 2.22.2 → 3.0.1
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 +223 -32
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
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
|
+
import { readFile, readdir, stat, lstat } from "node:fs/promises";
|
|
12
|
+
import { join, resolve, relative, isAbsolute, sep } from "node:path";
|
|
13
|
+
import path from "node:path";
|
|
13
14
|
import { spawn } from "node:child_process";
|
|
14
15
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
15
16
|
|
|
@@ -48,6 +49,97 @@ function stripOldMemory(text: string): string {
|
|
|
48
49
|
return text.replace(MEMORY_TAG_RE, "").replace(LEGACY_TAG_RE, "").trim();
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// ── Tool result helper (matches OpenClaw's jsonResult format)
|
|
53
|
+
function jsonToolResult(payload: unknown) {
|
|
54
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], details: payload };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── memory_get helpers (copied from OpenClaw source — pure filesystem, no embeddings)
|
|
58
|
+
function isFileMissingError(err: unknown): boolean {
|
|
59
|
+
return Boolean(err && typeof err === "object" && "code" in err && (err as any).code === "ENOENT");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function statRegularFile(absPath: string): Promise<{ missing: boolean; stat?: any }> {
|
|
63
|
+
let s;
|
|
64
|
+
try {
|
|
65
|
+
s = await lstat(absPath);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (isFileMissingError(err)) return { missing: true };
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (s.isSymbolicLink() || !s.isFile()) throw new Error("path required");
|
|
71
|
+
return { missing: false, stat: s };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeRelPath(value: string): string {
|
|
75
|
+
return value.trim().replace(/^[./]+/, "").replace(/\\/g, "/");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeExtraMemoryPaths(workspaceDir: string, extraPaths?: string[]): string[] {
|
|
79
|
+
if (!extraPaths?.length) return [];
|
|
80
|
+
const resolved = extraPaths
|
|
81
|
+
.map((value) => value.trim())
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.map((value) => isAbsolute(value) ? resolve(value) : resolve(workspaceDir, value));
|
|
84
|
+
return Array.from(new Set(resolved));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isMemoryPath(relPath: string): boolean {
|
|
88
|
+
const normalized = normalizeRelPath(relPath);
|
|
89
|
+
if (!normalized) return false;
|
|
90
|
+
if (normalized === "MEMORY.md" || normalized === "memory.md") return true;
|
|
91
|
+
return normalized.startsWith("memory/");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Exact copy of OpenClaw's manager.readFile — pure local file read with security gate */
|
|
95
|
+
async function memoryGetReadFile(
|
|
96
|
+
workspaceDir: string,
|
|
97
|
+
params: { relPath: string; from?: number; lines?: number },
|
|
98
|
+
extraPaths?: string[],
|
|
99
|
+
): Promise<{ text: string; path: string }> {
|
|
100
|
+
const rawPath = params.relPath.trim();
|
|
101
|
+
if (!rawPath) throw new Error("path required");
|
|
102
|
+
const absPath = isAbsolute(rawPath) ? resolve(rawPath) : resolve(workspaceDir, rawPath);
|
|
103
|
+
const relPath = relative(workspaceDir, absPath).replace(/\\/g, "/");
|
|
104
|
+
const allowedWorkspace = relPath.length > 0 && !relPath.startsWith("..") && !isAbsolute(relPath) && isMemoryPath(relPath);
|
|
105
|
+
let allowedAdditional = false;
|
|
106
|
+
if (!allowedWorkspace && extraPaths && extraPaths.length > 0) {
|
|
107
|
+
const additionalPaths = normalizeExtraMemoryPaths(workspaceDir, extraPaths);
|
|
108
|
+
for (const additionalPath of additionalPaths) try {
|
|
109
|
+
const s = await lstat(additionalPath);
|
|
110
|
+
if (s.isSymbolicLink()) continue;
|
|
111
|
+
if (s.isDirectory()) {
|
|
112
|
+
if (absPath === additionalPath || absPath.startsWith(`${additionalPath}${sep}`)) {
|
|
113
|
+
allowedAdditional = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (s.isFile()) {
|
|
119
|
+
if (absPath === additionalPath && absPath.endsWith(".md")) {
|
|
120
|
+
allowedAdditional = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
if (!allowedWorkspace && !allowedAdditional) throw new Error("path required");
|
|
127
|
+
if (!absPath.endsWith(".md")) throw new Error("path required");
|
|
128
|
+
if ((await statRegularFile(absPath)).missing) return { text: "", path: relPath };
|
|
129
|
+
let content: string;
|
|
130
|
+
try {
|
|
131
|
+
content = await readFile(absPath, "utf-8");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (isFileMissingError(err)) return { text: "", path: relPath };
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (!params.from && !params.lines) return { text: content, path: relPath };
|
|
137
|
+
const fileLines = content.split("\n");
|
|
138
|
+
const start = Math.max(1, params.from ?? 1);
|
|
139
|
+
const count = Math.max(1, params.lines ?? fileLines.length);
|
|
140
|
+
return { text: fileLines.slice(start - 1, start - 1 + count).join("\n"), path: relPath };
|
|
141
|
+
}
|
|
142
|
+
|
|
51
143
|
// ── Ingest sanitization: strip system noise so only real conversations get stored
|
|
52
144
|
// Patterns that indicate system-generated messages (not user conversations)
|
|
53
145
|
const SYSTEM_NOISE_PATTERNS = [
|
|
@@ -587,20 +679,19 @@ const memoryRouterPlugin = {
|
|
|
587
679
|
return;
|
|
588
680
|
}
|
|
589
681
|
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
});
|
|
682
|
+
// Fire and forget — don't block message delivery waiting for ingest
|
|
683
|
+
fetch(`${endpoint}/v1/memory/ingest`, {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: {
|
|
686
|
+
"Content-Type": "application/json",
|
|
687
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
688
|
+
},
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
messages: toStore,
|
|
691
|
+
model: "unknown",
|
|
692
|
+
...(embeddings && { embeddings }),
|
|
693
|
+
}),
|
|
694
|
+
}).then(async (res) => {
|
|
604
695
|
if (!res.ok) {
|
|
605
696
|
const details = await res.text().catch(() => "");
|
|
606
697
|
if (details.includes("payment_required")) {
|
|
@@ -612,17 +703,117 @@ const memoryRouterPlugin = {
|
|
|
612
703
|
} else {
|
|
613
704
|
api.logger.debug?.(`memoryrouter: ingest accepted (${toStore.length} messages)`);
|
|
614
705
|
}
|
|
615
|
-
}
|
|
616
|
-
log(
|
|
617
|
-
|
|
618
|
-
);
|
|
619
|
-
}
|
|
706
|
+
}).catch((err) => {
|
|
707
|
+
log(`memoryrouter: ingest error — ${err instanceof Error ? err.message : String(err)}`);
|
|
708
|
+
});
|
|
620
709
|
} catch (err) {
|
|
621
710
|
log(
|
|
622
711
|
`memoryrouter: agent_end error — ${err instanceof Error ? err.message : String(err)}`,
|
|
623
712
|
);
|
|
624
713
|
}
|
|
625
714
|
});
|
|
715
|
+
// ==================================================================
|
|
716
|
+
// Tools: memory_search + memory_get (replaces OpenClaw built-in)
|
|
717
|
+
// ==================================================================
|
|
718
|
+
|
|
719
|
+
// memory_search — calls MR /v1/memory/search
|
|
720
|
+
api.registerTool((ctx) => {
|
|
721
|
+
return {
|
|
722
|
+
label: "Memory Search",
|
|
723
|
+
name: "memory_search",
|
|
724
|
+
description: "Search memories from past conversations and your workspace. Use when relevant.",
|
|
725
|
+
parameters: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
query: { type: "string" },
|
|
729
|
+
maxResults: { type: "number" },
|
|
730
|
+
minScore: { type: "number" },
|
|
731
|
+
},
|
|
732
|
+
required: ["query"],
|
|
733
|
+
} as any,
|
|
734
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
735
|
+
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
736
|
+
if (!query) return jsonToolResult({ results: [], error: "query required" });
|
|
737
|
+
const limit = typeof params.maxResults === "number" ? params.maxResults : 10;
|
|
738
|
+
try {
|
|
739
|
+
const res = await fetch(`${endpoint}/v1/memory/search`, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: {
|
|
742
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
743
|
+
"Content-Type": "application/json",
|
|
744
|
+
...(embeddings && { "X-Embedding-Model": embeddings }),
|
|
745
|
+
},
|
|
746
|
+
body: JSON.stringify({ query, limit }),
|
|
747
|
+
});
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
const errBody = await res.text().catch(() => "");
|
|
750
|
+
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
751
|
+
}
|
|
752
|
+
const data = await res.json() as {
|
|
753
|
+
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string }>;
|
|
754
|
+
totalMemories?: number;
|
|
755
|
+
tokenCount?: number;
|
|
756
|
+
};
|
|
757
|
+
// Format results similar to OpenClaw's memory_search output
|
|
758
|
+
const results = (data.memories || []).map((m) => ({
|
|
759
|
+
snippet: m.content,
|
|
760
|
+
score: m.score,
|
|
761
|
+
source: "memoryrouter",
|
|
762
|
+
role: m.role,
|
|
763
|
+
window: m.window,
|
|
764
|
+
date: m.timestampHuman,
|
|
765
|
+
}));
|
|
766
|
+
return jsonToolResult({
|
|
767
|
+
results,
|
|
768
|
+
totalFound: data.totalMemories || results.length,
|
|
769
|
+
tokens: data.tokenCount || 0,
|
|
770
|
+
provider: "memoryrouter",
|
|
771
|
+
model: embeddings || "bge",
|
|
772
|
+
});
|
|
773
|
+
} catch (err) {
|
|
774
|
+
return jsonToolResult({ results: [], error: err instanceof Error ? err.message : String(err) });
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
781
|
+
api.registerTool((ctx) => {
|
|
782
|
+
const workspaceDir = ctx.workspaceDir || "";
|
|
783
|
+
// Read extraPaths from config even though memorySearch is disabled
|
|
784
|
+
const memSearchCfg = (ctx.config as any)?.agents?.defaults?.memorySearch;
|
|
785
|
+
const extraPaths: string[] = memSearchCfg?.extraPaths || [];
|
|
786
|
+
return {
|
|
787
|
+
label: "Memory Get",
|
|
788
|
+
name: "memory_get",
|
|
789
|
+
description: "Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; use after memory_search to pull only the needed lines and keep context small.",
|
|
790
|
+
parameters: {
|
|
791
|
+
type: "object",
|
|
792
|
+
properties: {
|
|
793
|
+
path: { type: "string" },
|
|
794
|
+
from: { type: "number" },
|
|
795
|
+
lines: { type: "number" },
|
|
796
|
+
},
|
|
797
|
+
required: ["path"],
|
|
798
|
+
} as any,
|
|
799
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
800
|
+
const relPath = typeof params.path === "string" ? params.path.trim() : "";
|
|
801
|
+
if (!relPath) return jsonToolResult({ path: "", text: "", error: "path required" });
|
|
802
|
+
const from = typeof params.from === "number" ? params.from : undefined;
|
|
803
|
+
const lines = typeof params.lines === "number" ? params.lines : undefined;
|
|
804
|
+
try {
|
|
805
|
+
return jsonToolResult(await memoryGetReadFile(workspaceDir, { relPath, from, lines }, extraPaths));
|
|
806
|
+
} catch (err) {
|
|
807
|
+
return jsonToolResult({
|
|
808
|
+
path: relPath,
|
|
809
|
+
text: "",
|
|
810
|
+
disabled: true,
|
|
811
|
+
error: err instanceof Error ? err.message : String(err),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
});
|
|
626
817
|
} // end if (memoryKey)
|
|
627
818
|
|
|
628
819
|
// ==================================================================
|
|
@@ -634,12 +825,8 @@ const memoryRouterPlugin = {
|
|
|
634
825
|
// but keep workspace file search (local, fast, no conflict)
|
|
635
826
|
async function optimizeMemorySearchConfig(): Promise<void> {
|
|
636
827
|
try {
|
|
637
|
-
//
|
|
638
|
-
await runOpenClawConfigSet("agents.defaults.memorySearch.
|
|
639
|
-
// Disable experimental session memory
|
|
640
|
-
await runOpenClawConfigSet("agents.defaults.memorySearch.experimental.sessionMemory", "false", true);
|
|
641
|
-
// Disable re-indexing on every search (MR handles memory, no need to re-embed constantly)
|
|
642
|
-
await runOpenClawConfigSet("agents.defaults.memorySearch.sync.onSearch", "false", true);
|
|
828
|
+
// Disable built-in memory entirely — MR replaces it with custom tools
|
|
829
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "false", true);
|
|
643
830
|
} catch {
|
|
644
831
|
// Non-fatal — config optimization is best-effort
|
|
645
832
|
}
|
|
@@ -648,8 +835,13 @@ const memoryRouterPlugin = {
|
|
|
648
835
|
// ── Helper: Restore OpenClaw's built-in memorySearch config when MR is disabled
|
|
649
836
|
async function restoreMemorySearchConfig(): Promise<void> {
|
|
650
837
|
try {
|
|
838
|
+
// Re-enable built-in memory system
|
|
839
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "true", true);
|
|
840
|
+
// Restore full sources (workspace + sessions)
|
|
651
841
|
await runOpenClawConfigSet("agents.defaults.memorySearch.sources", JSON.stringify(["memory", "sessions"]), true);
|
|
842
|
+
// Restore session memory
|
|
652
843
|
await runOpenClawConfigSet("agents.defaults.memorySearch.experimental.sessionMemory", "true", true);
|
|
844
|
+
// Restore sync on search
|
|
653
845
|
await runOpenClawConfigSet("agents.defaults.memorySearch.sync.onSearch", "true", true);
|
|
654
846
|
} catch {
|
|
655
847
|
// Non-fatal
|
|
@@ -670,8 +862,8 @@ const memoryRouterPlugin = {
|
|
|
670
862
|
// but disable session transcript scanning (MR handles conversational memory)
|
|
671
863
|
await optimizeMemorySearchConfig();
|
|
672
864
|
console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
|
|
673
|
-
console.log(` •
|
|
674
|
-
console.log(` •
|
|
865
|
+
console.log(` • Built-in memory disabled (MR fully replaces it)`);
|
|
866
|
+
console.log(` • memory_search + memory_get tools registered via plugin`);
|
|
675
867
|
console.log(`\nRun: openclaw mr upload to upload your memories`);
|
|
676
868
|
} catch (err) {
|
|
677
869
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -704,7 +896,7 @@ const memoryRouterPlugin = {
|
|
|
704
896
|
// Restore OpenClaw's built-in session memory scanning
|
|
705
897
|
await restoreMemorySearchConfig();
|
|
706
898
|
console.log("✓ MemoryRouter disabled (key cleared).");
|
|
707
|
-
console.log(" •
|
|
899
|
+
console.log(" • Built-in memory restored (enabled, sessions + workspace)");
|
|
708
900
|
console.log(" • CLI still available — run `openclaw mr <key>` to re-enable");
|
|
709
901
|
} catch (err) {
|
|
710
902
|
console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -728,8 +920,7 @@ const memoryRouterPlugin = {
|
|
|
728
920
|
if (memoryKey) {
|
|
729
921
|
console.log("\nRestoring memory settings...");
|
|
730
922
|
await optimizeMemorySearchConfig();
|
|
731
|
-
console.log(" ✓
|
|
732
|
-
console.log(" ✓ Workspace file search kept active");
|
|
923
|
+
console.log(" ✓ Built-in memory disabled (MR fully replaces it)");
|
|
733
924
|
}
|
|
734
925
|
|
|
735
926
|
console.log("\n✅ mr-memory updated. Restart gateway to apply: openclaw gateway restart");
|