mr-memory 2.22.1 → 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 +208 -11
- 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,10 +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);
|
|
831
|
+
// Disable built-in memory entirely — MR replaces it with custom tools
|
|
832
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "false", true);
|
|
641
833
|
} catch {
|
|
642
834
|
// Non-fatal — config optimization is best-effort
|
|
643
835
|
}
|
|
@@ -646,8 +838,14 @@ const memoryRouterPlugin = {
|
|
|
646
838
|
// ── Helper: Restore OpenClaw's built-in memorySearch config when MR is disabled
|
|
647
839
|
async function restoreMemorySearchConfig(): Promise<void> {
|
|
648
840
|
try {
|
|
841
|
+
// Re-enable built-in memory system
|
|
842
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.enabled", "true", true);
|
|
843
|
+
// Restore full sources (workspace + sessions)
|
|
649
844
|
await runOpenClawConfigSet("agents.defaults.memorySearch.sources", JSON.stringify(["memory", "sessions"]), true);
|
|
845
|
+
// Restore session memory
|
|
650
846
|
await runOpenClawConfigSet("agents.defaults.memorySearch.experimental.sessionMemory", "true", true);
|
|
847
|
+
// Restore sync on search
|
|
848
|
+
await runOpenClawConfigSet("agents.defaults.memorySearch.sync.onSearch", "true", true);
|
|
651
849
|
} catch {
|
|
652
850
|
// Non-fatal
|
|
653
851
|
}
|
|
@@ -667,8 +865,8 @@ const memoryRouterPlugin = {
|
|
|
667
865
|
// but disable session transcript scanning (MR handles conversational memory)
|
|
668
866
|
await optimizeMemorySearchConfig();
|
|
669
867
|
console.log(`✓ MemoryRouter enabled. Key: ${key.slice(0, 6)}...${key.slice(-3)}`);
|
|
670
|
-
console.log(` •
|
|
671
|
-
console.log(` •
|
|
868
|
+
console.log(` • Built-in memory disabled (MR fully replaces it)`);
|
|
869
|
+
console.log(` • memory_search + memory_get tools registered via plugin`);
|
|
672
870
|
console.log(`\nRun: openclaw mr upload to upload your memories`);
|
|
673
871
|
} catch (err) {
|
|
674
872
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -701,7 +899,7 @@ const memoryRouterPlugin = {
|
|
|
701
899
|
// Restore OpenClaw's built-in session memory scanning
|
|
702
900
|
await restoreMemorySearchConfig();
|
|
703
901
|
console.log("✓ MemoryRouter disabled (key cleared).");
|
|
704
|
-
console.log(" •
|
|
902
|
+
console.log(" • Built-in memory restored (enabled, sessions + workspace)");
|
|
705
903
|
console.log(" • CLI still available — run `openclaw mr <key>` to re-enable");
|
|
706
904
|
} catch (err) {
|
|
707
905
|
console.error(`Failed to disable: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -725,8 +923,7 @@ const memoryRouterPlugin = {
|
|
|
725
923
|
if (memoryKey) {
|
|
726
924
|
console.log("\nRestoring memory settings...");
|
|
727
925
|
await optimizeMemorySearchConfig();
|
|
728
|
-
console.log(" ✓
|
|
729
|
-
console.log(" ✓ Workspace file search kept active");
|
|
926
|
+
console.log(" ✓ Built-in memory disabled (MR fully replaces it)");
|
|
730
927
|
}
|
|
731
928
|
|
|
732
929
|
console.log("\n✅ mr-memory updated. Restart gateway to apply: openclaw gateway restart");
|