@lotargo/memory_plugin 1.4.5 → 1.4.601

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.
@@ -22,12 +22,9 @@ export const DEFAULT_CONFIG = {
22
22
  };
23
23
 
24
24
  let cachedConfig = null;
25
+ let cachedMtimeMs = 0;
25
26
 
26
- export function getConfig() {
27
- if (cachedConfig) {
28
- return cachedConfig;
29
- }
30
-
27
+ function loadConfigFromDisk() {
31
28
  ensureDirSync();
32
29
 
33
30
  if (fs.existsSync(CONFIG_FILE)) {
@@ -35,6 +32,7 @@ export function getConfig() {
35
32
  const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
36
33
  const parsed = JSON.parse(raw);
37
34
  cachedConfig = Object.freeze({ ...DEFAULT_CONFIG, ...parsed });
35
+ cachedMtimeMs = fs.statSync(CONFIG_FILE).mtimeMs;
38
36
  return cachedConfig;
39
37
  } catch (err) {
40
38
  console.warn("Failed to read config file, falling back to defaults:", err.message);
@@ -46,11 +44,24 @@ export function getConfig() {
46
44
  return cachedConfig;
47
45
  }
48
46
 
47
+ export function getConfig() {
48
+ try {
49
+ const mtimeMs = fs.statSync(CONFIG_FILE).mtimeMs;
50
+ if (cachedConfig && mtimeMs === cachedMtimeMs) {
51
+ return cachedConfig;
52
+ }
53
+ } catch (err) {
54
+ // Config file missing — fall through to load/create.
55
+ }
56
+ return loadConfigFromDisk();
57
+ }
58
+
49
59
  export function saveConfig(newConfig) {
50
60
  ensureDirSync();
51
61
  cachedConfig = Object.freeze({ ...DEFAULT_CONFIG, ...newConfig });
52
62
  try {
53
63
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cachedConfig, null, 2), "utf-8");
64
+ cachedMtimeMs = fs.statSync(CONFIG_FILE).mtimeMs;
54
65
  } catch (err) {
55
66
  console.error("Failed to write config file:", err.message);
56
67
  }
@@ -1,6 +1,6 @@
1
- const { readFile, writeFile, mkdir, cp, readdir, unlink } = await import("fs/promises");
1
+ const { mkdir, cp, readdir } = await import("fs/promises");
2
2
  const { existsSync } = await import("fs");
3
- const { join, basename, dirname, resolve } = await import("path");
3
+ const { join, dirname } = await import("path");
4
4
  const { homedir } = await import("os");
5
5
  const { fileURLToPath } = await import("url");
6
6
  const {
@@ -18,6 +18,20 @@ const {
18
18
  inDateRange,
19
19
  } = await import("../mcp-server/fact_format.js");
20
20
 
21
+ const {
22
+ MEMORY_DIR,
23
+ GLOBAL_KEY,
24
+ canonicalPath,
25
+ projectName,
26
+ projectKey,
27
+ scopeKey,
28
+ readMemory,
29
+ writeMemory,
30
+ listProjectStores,
31
+ storeFilePath,
32
+ today,
33
+ } = await import("../mcp-server/memory.js");
34
+
21
35
  // Resolve a fact reference (1-based number, metadata id, or text) to an index.
22
36
  function resolveFactIndex(entries, ref) {
23
37
  const trimmed = String(ref || "").trim();
@@ -31,9 +45,7 @@ function resolveFactIndex(entries, ref) {
31
45
  }
32
46
 
33
47
  const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
34
- const MEMORY_DIR = join(CONFIG_DIR, "memory");
35
48
  const SKILLS_DIR = join(CONFIG_DIR, "skills");
36
- const GLOBAL_KEY = "global";
37
49
 
38
50
  async function ensureDir() {
39
51
  if (!existsSync(MEMORY_DIR)) await mkdir(MEMORY_DIR, { recursive: true });
@@ -54,154 +66,6 @@ async function ensureDir() {
54
66
  } catch (e) {}
55
67
  }
56
68
 
57
- function canonicalPath(dir) {
58
- let p = resolve(dir || process.cwd());
59
- if (process.platform === "win32") {
60
- p = p.replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_, d) => `${d.toLowerCase()}:`);
61
- }
62
- return p;
63
- }
64
-
65
- // Display label for a project (basename of the resolved directory).
66
- function projectName(worktree, directory) {
67
- const dir = worktree || directory;
68
- return dir ? basename(resolve(dir)) : "default";
69
- }
70
-
71
- // Project store key = full directory path (removes basename collisions).
72
- function projectKey(worktree, directory) {
73
- return canonicalPath(worktree || directory);
74
- }
75
-
76
- function scopeKey(scope, worktree, directory) {
77
- return scope === "global" ? GLOBAL_KEY : projectKey(worktree, directory);
78
- }
79
-
80
- function slugify(key) {
81
- return key.replace(/[^a-zA-Z0-9_-]/g, "_");
82
- }
83
-
84
- function memoryPath(key) {
85
- return join(MEMORY_DIR, `${slugify(key)}.md`);
86
- }
87
-
88
- function memoryFileName(key) {
89
- return basename(memoryPath(key));
90
- }
91
-
92
- function parseMeta(content) {
93
- const m = content.match(/<!-- path: (.+?) -->/);
94
- return { path: m ? m[1].trim() : null };
95
- }
96
-
97
- function isSimpleKey(key) {
98
- return /^[a-zA-Z0-9_-]+$/.test(key);
99
- }
100
-
101
- // Lazy migration: when reading a project path store that doesn't exist yet but a
102
- // legacy <basename>.md store (without path binding) does, claim it under the path.
103
- async function maybeMigrateLegacy(key) {
104
- if (key === GLOBAL_KEY || isSimpleKey(key)) return null;
105
- const legacyBasename = basename(key);
106
- if (!legacyBasename) return null;
107
- const legacyFp = join(MEMORY_DIR, `${legacyBasename}.md`);
108
- if (slugify(key) === legacyBasename || !existsSync(legacyFp)) return null;
109
- const content = await readFile(legacyFp, "utf-8");
110
- if (parseMeta(content).path) return null; // already bound to another project
111
- // Collision guard: a different path with the same basename is already bound,
112
- // so this legacy store is ambiguous and must not be silently claimed.
113
- const files = await readdir(MEMORY_DIR).catch(() => []);
114
- for (const f of files) {
115
- if (!f.endsWith(".md") || f === `${legacyBasename}.md` || f === `${GLOBAL_KEY}.md`) continue;
116
- try {
117
- const other = parseMeta(await readFile(join(MEMORY_DIR, f), "utf-8")).path;
118
- if (other && basename(other) === legacyBasename) return null;
119
- } catch (e) {}
120
- }
121
- const facts = content.split("\n").filter((l) => l.startsWith("- ["));
122
- await writeMemory(key, facts);
123
- try {
124
- await unlink(legacyFp);
125
- } catch (e) {}
126
- return facts;
127
- }
128
-
129
- async function readMemory(key) {
130
- const fp = memoryPath(key);
131
- if (existsSync(fp)) {
132
- const content = await readFile(fp, "utf-8");
133
- return content.split("\n").filter((l) => l.startsWith("- ["));
134
- }
135
- const migrated = await maybeMigrateLegacy(key);
136
- return migrated || [];
137
- }
138
-
139
- async function readMemoryRaw(key) {
140
- return (await readMemory(key)).map((e) => e.slice(2));
141
- }
142
-
143
- async function writeMemory(key, entries) {
144
- const lines = [];
145
- if (key === GLOBAL_KEY) {
146
- lines.push("# Global Memory", "");
147
- } else {
148
- lines.push(`# Memory: ${basename(key) || key}`, "");
149
- if (!isSimpleKey(key)) {
150
- lines.push(`<!-- path: ${key} -->`, "");
151
- }
152
- }
153
- const content = lines.join("\n") + "\n" + (entries.length ? entries.join("\n") + "\n" : "");
154
- await writeFile(memoryPath(key), content);
155
- }
156
-
157
- async function listProjectStores() {
158
- const stores = [];
159
- const files = await readdir(MEMORY_DIR).catch(() => []);
160
- for (const f of files) {
161
- if (!f.endsWith(".md") || f === `${GLOBAL_KEY}.md`) continue;
162
- let content = "";
163
- try {
164
- content = await readFile(join(MEMORY_DIR, f), "utf-8");
165
- } catch (e) {
166
- continue;
167
- }
168
- const facts = content.split("\n").filter((l) => l.startsWith("- ["));
169
- const meta = parseMeta(content);
170
- const key = meta.path || f.slice(0, -3);
171
- stores.push({
172
- key,
173
- path: meta.path,
174
- basename: basename(meta.path || key) || key,
175
- file: f,
176
- count: facts.length,
177
- legacy: !meta.path,
178
- });
179
- }
180
- stores.sort((a, b) => a.basename.localeCompare(b.basename));
181
- return stores;
182
- }
183
-
184
- async function migrateLegacyStore(legacyKey, targetDir) {
185
- const legacyFp = join(MEMORY_DIR, `${legacyKey.replace(/[^a-zA-Z0-9_-]/g, "_")}.md`);
186
- if (!existsSync(legacyFp)) return { ok: false, reason: "not_found", key: legacyKey };
187
- const content = await readFile(legacyFp, "utf-8");
188
- if (parseMeta(content).path) return { ok: false, reason: "already_bound", key: legacyKey };
189
- const targetKey = projectKey(targetDir, null);
190
- const facts = content.split("\n").filter((l) => l.startsWith("- ["));
191
- await writeMemory(targetKey, facts);
192
- try {
193
- await unlink(legacyFp);
194
- } catch (e) {}
195
- return { ok: true, key: targetKey, file: memoryPath(targetKey), facts: facts.length };
196
- }
197
-
198
- function today() {
199
- const d = new Date();
200
- const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
201
- const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
202
- return `${date} ${time}`;
203
- }
204
-
205
69
  async function notify(client, message, variant = "success") {
206
70
  if (!client?.tui?.showToast) {
207
71
  await client?.app?.log({
@@ -459,7 +323,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
459
323
  if (results.length) results.push("");
460
324
  results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
461
325
  matched.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, key)}`));
462
- results.push(`Store file: ${memoryPath(key)}`);
326
+ results.push(`Store file: ${storeFilePath(key)}`);
463
327
  };
464
328
 
465
329
  if (scope === "list_projects") {
@@ -595,8 +459,8 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
595
459
  `Version: ${version}`,
596
460
  `MEMORY_DIR: ${MEMORY_DIR}`,
597
461
  `SQLite DB: ${dbPath}`,
598
- `Global store: ${memoryPath(GLOBAL_KEY)}`,
599
- `Project store: ${memoryPath(activeProjectKey)}`,
462
+ `Global store: ${storeFilePath(GLOBAL_KEY)}`,
463
+ `Project store: ${storeFilePath(activeProjectKey)}`,
600
464
  ];
601
465
  if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
602
466
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotargo/memory_plugin",
3
- "version": "1.4.5",
3
+ "version": "1.4.601",
4
4
  "description": "100% local hybrid RAG memory for AI coding agents (OpenCode, Claude Code, Codex, Antigravity). MCP server + plugin: persistent user facts, document ingestion, vector + SQLite FTS5 retrieval across sessions.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",