mr-memory 2.22.2 → 3.0.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 +207 -13
- 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 = [
|
|
@@ -623,6 +715,108 @@ const memoryRouterPlugin = {
|
|
|
623
715
|
);
|
|
624
716
|
}
|
|
625
717
|
});
|
|
718
|
+
// ==================================================================
|
|
719
|
+
// Tools: memory_search + memory_get (replaces OpenClaw built-in)
|
|
720
|
+
// ==================================================================
|
|
721
|
+
|
|
722
|
+
// memory_search — calls MR /v1/memory/search
|
|
723
|
+
api.registerTool((ctx) => {
|
|
724
|
+
return {
|
|
725
|
+
label: "Memory Search",
|
|
726
|
+
name: "memory_search",
|
|
727
|
+
description: "Search memories from past conversations and your workspace. Use when relevant.",
|
|
728
|
+
parameters: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: {
|
|
731
|
+
query: { type: "string" },
|
|
732
|
+
maxResults: { type: "number" },
|
|
733
|
+
minScore: { type: "number" },
|
|
734
|
+
},
|
|
735
|
+
required: ["query"],
|
|
736
|
+
} as any,
|
|
737
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
738
|
+
const query = typeof params.query === "string" ? params.query.trim() : "";
|
|
739
|
+
if (!query) return jsonToolResult({ results: [], error: "query required" });
|
|
740
|
+
const limit = typeof params.maxResults === "number" ? params.maxResults : 10;
|
|
741
|
+
try {
|
|
742
|
+
const res = await fetch(`${endpoint}/v1/memory/search`, {
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: {
|
|
745
|
+
Authorization: `Bearer ${memoryKey}`,
|
|
746
|
+
"Content-Type": "application/json",
|
|
747
|
+
...(embeddings && { "X-Embedding-Model": embeddings }),
|
|
748
|
+
},
|
|
749
|
+
body: JSON.stringify({ query, limit }),
|
|
750
|
+
});
|
|
751
|
+
if (!res.ok) {
|
|
752
|
+
const errBody = await res.text().catch(() => "");
|
|
753
|
+
return jsonToolResult({ results: [], error: `Search failed: HTTP ${res.status}`, details: errBody.slice(0, 200) });
|
|
754
|
+
}
|
|
755
|
+
const data = await res.json() as {
|
|
756
|
+
memories?: Array<{ content: string; score: number; role: string; window: string; timestampHuman: string }>;
|
|
757
|
+
totalMemories?: number;
|
|
758
|
+
tokenCount?: number;
|
|
759
|
+
};
|
|
760
|
+
// Format results similar to OpenClaw's memory_search output
|
|
761
|
+
const results = (data.memories || []).map((m, i) => ({
|
|
762
|
+
snippet: m.content,
|
|
763
|
+
score: m.score,
|
|
764
|
+
source: "memoryrouter",
|
|
765
|
+
role: m.role,
|
|
766
|
+
window: m.window,
|
|
767
|
+
date: m.timestampHuman,
|
|
768
|
+
}));
|
|
769
|
+
return jsonToolResult({
|
|
770
|
+
results,
|
|
771
|
+
totalFound: data.totalMemories || results.length,
|
|
772
|
+
tokens: data.tokenCount || 0,
|
|
773
|
+
provider: "memoryrouter",
|
|
774
|
+
model: embeddings || "bge",
|
|
775
|
+
});
|
|
776
|
+
} catch (err) {
|
|
777
|
+
return jsonToolResult({ results: [], error: err instanceof Error ? err.message : String(err) });
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// memory_get — exact copy of OpenClaw's implementation (pure local file read)
|
|
784
|
+
api.registerTool((ctx) => {
|
|
785
|
+
const workspaceDir = ctx.workspaceDir || "";
|
|
786
|
+
// Read extraPaths from config even though memorySearch is disabled
|
|
787
|
+
const memSearchCfg = (ctx.config as any)?.agents?.defaults?.memorySearch;
|
|
788
|
+
const extraPaths: string[] = memSearchCfg?.extraPaths || [];
|
|
789
|
+
return {
|
|
790
|
+
label: "Memory Get",
|
|
791
|
+
name: "memory_get",
|
|
792
|
+
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.",
|
|
793
|
+
parameters: {
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
path: { type: "string" },
|
|
797
|
+
from: { type: "number" },
|
|
798
|
+
lines: { type: "number" },
|
|
799
|
+
},
|
|
800
|
+
required: ["path"],
|
|
801
|
+
} as any,
|
|
802
|
+
execute: async (_toolCallId: string, params: Record<string, unknown>) => {
|
|
803
|
+
const relPath = typeof params.path === "string" ? params.path.trim() : "";
|
|
804
|
+
if (!relPath) return jsonToolResult({ path: "", text: "", error: "path required" });
|
|
805
|
+
const from = typeof params.from === "number" ? params.from : undefined;
|
|
806
|
+
const lines = typeof params.lines === "number" ? params.lines : undefined;
|
|
807
|
+
try {
|
|
808
|
+
return jsonToolResult(await memoryGetReadFile(workspaceDir, { relPath, from, lines }, extraPaths));
|
|
809
|
+
} catch (err) {
|
|
810
|
+
return jsonToolResult({
|
|
811
|
+
path: relPath,
|
|
812
|
+
text: "",
|
|
813
|
+
disabled: true,
|
|
814
|
+
error: err instanceof Error ? err.message : String(err),
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
});
|
|
626
820
|
} // end if (memoryKey)
|
|
627
821
|
|
|
628
822
|
// ==================================================================
|
|
@@ -634,12 +828,8 @@ const memoryRouterPlugin = {
|
|
|
634
828
|
// but keep workspace file search (local, fast, no conflict)
|
|
635
829
|
async function optimizeMemorySearchConfig(): Promise<void> {
|
|
636
830
|
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);
|
|
831
|
+
// Disable built-in memory entirely — MR replaces it with custom tools
|
|
832
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "false", true);
|
|
643
833
|
} catch {
|
|
644
834
|
// Non-fatal — config optimization is best-effort
|
|
645
835
|
}
|
|
@@ -648,8 +838,13 @@ const memoryRouterPlugin = {
|
|
|
648
838
|
// ── Helper: Restore OpenClaw's built-in memorySearch config when MR is disabled
|
|
649
839
|
async function restoreMemorySearchConfig(): Promise<void> {
|
|
650
840
|
try {
|
|
841
|
+
// Re-enable built-in memory system
|
|
842
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "true", true);
|
|
843
|
+
// Restore full sources (workspace + sessions)
|
|
651
844
|
await runOpenClawConfigSet("agents.defaults.memorySearch.sources", JSON.stringify(["memory", "sessions"]), true);
|
|
845
|
+
// Restore session memory
|
|
652
846
|
await runOpenClawConfigSet("agents.defaults.memorySearch.experimental.sessionMemory", "true", true);
|
|
847
|
+
// Restore sync on search
|
|
653
848
|
await runOpenClawConfigSet("agents.defaults.memorySearch.sync.onSearch", "true", true);
|
|
654
849
|
} catch {
|
|
655
850
|
// Non-fatal
|
|
@@ -670,8 +865,8 @@ const memoryRouterPlugin = {
|
|
|
670
865
|
// but disable session transcript scanning (MR handles conversational memory)
|
|
671
866
|
await optimizeMemorySearchConfig();
|
|
672
867
|
console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
|
|
673
|
-
console.log(` •
|
|
674
|
-
console.log(` •
|
|
868
|
+
console.log(` • Built-in memory disabled (MR fully replaces it)`);
|
|
869
|
+
console.log(` • memory_search + memory_get tools registered via plugin`);
|
|
675
870
|
console.log(`\nRun: openclaw mr upload to upload your memories`);
|
|
676
871
|
} catch (err) {
|
|
677
872
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -704,7 +899,7 @@ const memoryRouterPlugin = {
|
|
|
704
899
|
// Restore OpenClaw's built-in session memory scanning
|
|
705
900
|
await restoreMemorySearchConfig();
|
|
706
901
|
console.log("✓ MemoryRouter disabled (key cleared).");
|
|
707
|
-
console.log(" •
|
|
902
|
+
console.log(" • Built-in memory restored (enabled, sessions + workspace)");
|
|
708
903
|
console.log(" • CLI still available — run `openclaw mr <key>` to re-enable");
|
|
709
904
|
} catch (err) {
|
|
710
905
|
console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -728,8 +923,7 @@ const memoryRouterPlugin = {
|
|
|
728
923
|
if (memoryKey) {
|
|
729
924
|
console.log("\nRestoring memory settings...");
|
|
730
925
|
await optimizeMemorySearchConfig();
|
|
731
|
-
console.log(" ✓
|
|
732
|
-
console.log(" ✓ Workspace file search kept active");
|
|
926
|
+
console.log(" ✓ Built-in memory disabled (MR fully replaces it)");
|
|
733
927
|
}
|
|
734
928
|
|
|
735
929
|
console.log("\n✅ mr-memory updated. Restart gateway to apply: openclaw gateway restart");
|