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.
Files changed (2) hide show
  1. package/index.ts +208 -11
  2. 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
- // Set sources to only "memory" (workspace files), remove "sessions"
638
- await runOpenClawConfigSet("agents.defaults.memorySearch.sources", JSON.stringify(["memory"]), true);
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(` • Session memory scanning disabled (MR handles this)`);
671
- console.log(` • Workspace file search kept active`);
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(" • Session memory scanning restored");
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(" ✓ Session memory scanning disabled (MR handles this)");
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mr-memory",
3
- "version": "2.22.1",
3
+ "version": "3.0.0",
4
4
  "description": "MemoryRouter persistent memory plugin for OpenClaw — your AI remembers every conversation",
5
5
  "type": "module",
6
6
  "files": [