@lotargo/memory_plugin 1.4.621 → 1.5.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 (34) hide show
  1. package/README.md +352 -366
  2. package/mcp-server/admin/auth.js +31 -4
  3. package/mcp-server/cli/direct_commands.js +313 -0
  4. package/mcp-server/cli/handlers/cloud_actions.js +138 -0
  5. package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
  6. package/mcp-server/cli/handlers/engine_actions.js +214 -0
  7. package/mcp-server/cli/handlers/prompt_actions.js +24 -0
  8. package/mcp-server/cli/handlers/storage_actions.js +749 -0
  9. package/mcp-server/cli/quick_stats.js +39 -0
  10. package/mcp-server/cli/ui.js +565 -0
  11. package/mcp-server/cli.js +324 -2085
  12. package/mcp-server/config/auth_store.js +56 -9
  13. package/mcp-server/config/config_manager.js +1 -0
  14. package/mcp-server/db/database.js +14 -1
  15. package/mcp-server/db/migrations.js +28 -0
  16. package/mcp-server/fact_format.js +244 -177
  17. package/mcp-server/identity.js +152 -0
  18. package/mcp-server/index.js +42 -679
  19. package/mcp-server/memory.js +50 -63
  20. package/mcp-server/prompt_manager.js +1 -1
  21. package/mcp-server/tools/helpers.js +39 -0
  22. package/mcp-server/tools/identity_tools.js +277 -0
  23. package/mcp-server/tools/index.js +9 -0
  24. package/mcp-server/tools/memory_tools.js +506 -0
  25. package/mcp-server/tools/rag_tools.js +235 -0
  26. package/opencode-plugin/index.js +460 -48
  27. package/package.json +7 -3
  28. package/skills/using-memory/SKILL.md +31 -14
  29. package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
  30. package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
  31. package/mcp-server/benchmarks/quality_evaluator.js +0 -600
  32. package/mcp-server/benchmarks/run_benchmarks.js +0 -347
  33. package/mcp-server/benchmarks/stress_ingestion.js +0 -195
  34. package/mcp-server/benchmarks/test_dual_layer.js +0 -140
@@ -7,10 +7,28 @@ import { MEMORY_DIR, ensureDirSync } from "../memory.js";
7
7
 
8
8
  const SECRETS_FILE = path.join(MEMORY_DIR, "auth_secrets.enc");
9
9
 
10
+ // ── Module-level caches ─────────────────────────────────────────────────────
11
+ // These avoid re-running expensive sync operations (execSync, PBKDF2, file I/O)
12
+ // on every CLI navigation or tool call. invalidateAuthCache() must be called
13
+ // whenever secrets are deleted or the API key is removed.
14
+ let _cachedMachineId = undefined; // undefined = not yet resolved
15
+ let _cachedFingerprint = null;
16
+ let _cachedEncryptionKey = null;
17
+ let _cachedSecrets = undefined; // undefined = not loaded, null = no secrets, object = cached
18
+ let _cachedSecretsMtime = 0;
19
+
20
+ export function invalidateAuthCache() {
21
+ _cachedSecrets = undefined;
22
+ _cachedSecretsMtime = 0;
23
+ // Keep machineId / fingerprint / key cached — they don't change per-session.
24
+ }
25
+
10
26
  // Stable per-machine identifier. Must NOT rely on volatile values (e.g.
11
27
  // os.networkInterfaces() — VPN adapters, hotspot IPs and IPv6 privacy
12
28
  // addresses rotate constantly and would silently change the AES key).
13
29
  function getMachineId() {
30
+ if (_cachedMachineId !== undefined) return _cachedMachineId;
31
+ let id = null;
14
32
  try {
15
33
  if (process.platform === "win32") {
16
34
  const out = execSync("reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", {
@@ -18,12 +36,12 @@ function getMachineId() {
18
36
  stdio: ["ignore", "pipe", "ignore"],
19
37
  });
20
38
  const m = out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{36})/i);
21
- if (m) return m[1].toLowerCase();
39
+ if (m) id = m[1].toLowerCase();
22
40
  } else if (process.platform === "linux") {
23
41
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
24
42
  try {
25
43
  const v = fs.readFileSync(p, "utf8").trim();
26
- if (v) return v;
44
+ if (v) { id = v; break; }
27
45
  } catch {}
28
46
  }
29
47
  } else if (process.platform === "darwin") {
@@ -32,15 +50,17 @@ function getMachineId() {
32
50
  stdio: ["ignore", "pipe", "ignore"],
33
51
  });
34
52
  const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
35
- if (m) return m[1];
53
+ if (m) id = m[1];
36
54
  }
37
55
  } catch {}
38
- return null;
56
+ _cachedMachineId = id;
57
+ return _cachedMachineId;
39
58
  }
40
59
 
41
60
  // Generate a deterministic hardware + system fingerprint (stable across reboots,
42
61
  // network changes and user sessions on the same machine).
43
62
  function getSystemFingerprint() {
63
+ if (_cachedFingerprint !== null && _cachedFingerprint !== undefined) return _cachedFingerprint;
44
64
  const parts = [
45
65
  getMachineId() || "no-machine-id",
46
66
  os.hostname() || "localhost",
@@ -48,15 +68,18 @@ function getSystemFingerprint() {
48
68
  os.platform() || "unknown",
49
69
  os.arch() || "unknown",
50
70
  ];
51
- return parts.join("|");
71
+ _cachedFingerprint = parts.join("|");
72
+ return _cachedFingerprint;
52
73
  }
53
74
 
54
75
  // Derive a 256-bit (32 bytes) key using PBKDF2 with salt derived from fingerprint
55
76
  function deriveEncryptionKey() {
77
+ if (_cachedEncryptionKey) return _cachedEncryptionKey;
56
78
  const fingerprint = getSystemFingerprint();
57
79
  const salt = crypto.createHash("sha256").update(fingerprint).digest();
58
80
  // PBKDF2 with 10,000 iterations to derive a secure 32-byte key
59
- return crypto.pbkdf2Sync(fingerprint, salt, 10000, 32, "sha256");
81
+ _cachedEncryptionKey = crypto.pbkdf2Sync(fingerprint, salt, 10000, 32, "sha256");
82
+ return _cachedEncryptionKey;
60
83
  }
61
84
 
62
85
  // Encrypt data using AES-256-GCM
@@ -153,17 +176,33 @@ export function resolveEnvSecrets() {
153
176
  // Read the encrypted store file ONLY (no environment merge). Returns the raw
154
177
  // parsed record, or null when the file is missing / undecryptable.
155
178
  function readStoredSecrets() {
156
- if (!fs.existsSync(SECRETS_FILE)) return null;
179
+ try {
180
+ if (fs.existsSync(SECRETS_FILE)) {
181
+ const mtime = fs.statSync(SECRETS_FILE).mtimeMs;
182
+ if (_cachedSecrets !== undefined && mtime === _cachedSecretsMtime) {
183
+ return _cachedSecrets;
184
+ }
185
+ _cachedSecretsMtime = mtime;
186
+ } else {
187
+ // File doesn't exist — clear cache
188
+ if (_cachedSecrets !== undefined && _cachedSecrets === null) return null;
189
+ _cachedSecrets = null;
190
+ _cachedSecretsMtime = 0;
191
+ return null;
192
+ }
193
+ } catch {}
157
194
  try {
158
195
  const encrypted = fs.readFileSync(SECRETS_FILE, "utf-8").trim();
159
- if (!encrypted) return null;
160
- return JSON.parse(decryptData(encrypted));
196
+ if (!encrypted) { _cachedSecrets = null; return null; }
197
+ _cachedSecrets = JSON.parse(decryptData(encrypted));
198
+ return _cachedSecrets;
161
199
  } catch (err) {
162
200
  console.error(
163
201
  "Failed to decrypt or load cloud secrets:",
164
202
  err.message,
165
203
  "— the file was encrypted with a different machine key. Re-run login to recreate it."
166
204
  );
205
+ _cachedSecrets = null;
167
206
  return null;
168
207
  }
169
208
  }
@@ -187,8 +226,14 @@ export function saveSecrets(secrets) {
187
226
  const plainText = JSON.stringify(secrets);
188
227
  const encrypted = encryptData(plainText);
189
228
  fs.writeFileSync(SECRETS_FILE, encrypted, "utf-8");
229
+ _cachedSecrets = undefined; // invalidate so readStoredSecrets re-reads
230
+ _cachedSecretsMtime = 0;
231
+ if (typeof _onSecretsChanged === "function") _onSecretsChanged();
190
232
  }
191
233
 
234
+ let _onSecretsChanged = null;
235
+ export function onSecretsChanged(cb) { _onSecretsChanged = cb; }
236
+
192
237
  // Load secrets securely. Priority (highest first):
193
238
  // 1. Env account API token (TURSO_API_TOKEN) — reused from the store when a
194
239
  // session was already minted for this exact token, else returned with
@@ -240,6 +285,8 @@ export function deleteSecrets() {
240
285
  if (fs.existsSync(SECRETS_FILE)) {
241
286
  try {
242
287
  fs.unlinkSync(SECRETS_FILE);
288
+ _cachedSecrets = null;
289
+ _cachedSecretsMtime = 0;
243
290
  return true;
244
291
  } catch (err) {
245
292
  console.error("Failed to delete secrets file:", err.message);
@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG = {
15
15
  onnxThreads: 0, // ONNX WASM threads: 0 = auto-detect CPU cores, or 1-16
16
16
  executionDevice: "cpu", // "cpu" | "webgpu"
17
17
  mode: "only-local", // "only-local" | "only-cloud" | "hybrid-sync"
18
+ injectLimit: 10,
18
19
  conflictStrategy: "merge", // "merge" | "cloud-wins" | "local-wins"
19
20
  tursoUrl: "", // Connection endpoint URL for Turso DB
20
21
  failoverUrl: "", // Failover connection endpoint URL (Fly.io + LiteFS)
@@ -9,6 +9,8 @@ import { createClient } from "@libsql/client";
9
9
 
10
10
  let dbInstance = null;
11
11
  let dbInitPromise = null;
12
+ let dbLastFailAt = 0;
13
+ const DB_FAIL_COOLDOWN_MS = 5_000; // don't retry cloud init within 5s of a failure
12
14
 
13
15
  export const STORAGE_DIR = join(MEMORY_DIR, "storage");
14
16
  export const BLOBS_DIR = join(STORAGE_DIR, "blobs");
@@ -207,10 +209,20 @@ export async function getDatabase(customPath = null, forceMode = null) {
207
209
  if (dbInstance && dbInstance.mode === mode) {
208
210
  return dbInstance;
209
211
  }
212
+ // After a failed init, wait before retrying to avoid hammering cloud auth
213
+ if (!dbInitPromise && dbLastFailAt && (Date.now() - dbLastFailAt) < DB_FAIL_COOLDOWN_MS) {
214
+ throw new Error("Database initialization failed recently. Retrying in a few seconds...");
215
+ }
210
216
  // Deduplicate concurrent default-DB initialization so migrations never run
211
217
  // on multiple connections at once (avoids "database is locked" crashes).
212
218
  if (!dbInitPromise) {
213
- dbInitPromise = openDatabase(null, mode).finally(() => {
219
+ dbInitPromise = openDatabase(null, mode).then((result) => {
220
+ dbLastFailAt = 0;
221
+ return result;
222
+ }).catch((err) => {
223
+ dbLastFailAt = Date.now();
224
+ throw err;
225
+ }).finally(() => {
214
226
  dbInitPromise = null;
215
227
  });
216
228
  }
@@ -222,6 +234,7 @@ export async function getDatabase(customPath = null, forceMode = null) {
222
234
 
223
235
  export function closeDatabase() {
224
236
  dbInitPromise = null;
237
+ dbLastFailAt = 0;
225
238
  if (dbInstance) {
226
239
  dbInstance.close();
227
240
  dbInstance = null;
@@ -110,6 +110,34 @@ const MIGRATIONS = [
110
110
  } catch (e) {}
111
111
  },
112
112
  },
113
+ {
114
+ version: 4,
115
+ name: "004_git_project_identity",
116
+ up: async (db) => {
117
+ await db.exec(`
118
+ CREATE TABLE IF NOT EXISTS project_identities (
119
+ key TEXT PRIMARY KEY,
120
+ name TEXT NOT NULL,
121
+ primary_remote TEXT,
122
+ created_at INTEGER NOT NULL,
123
+ updated_at INTEGER NOT NULL
124
+ );
125
+ `);
126
+
127
+ await db.exec(`
128
+ CREATE TABLE IF NOT EXISTS project_aliases (
129
+ alias TEXT PRIMARY KEY,
130
+ identity_key TEXT NOT NULL REFERENCES project_identities(key) ON DELETE CASCADE,
131
+ kind TEXT NOT NULL,
132
+ created_at INTEGER NOT NULL
133
+ );
134
+ `);
135
+
136
+ await db.exec(`
137
+ CREATE INDEX IF NOT EXISTS idx_project_aliases_identity ON project_aliases(identity_key);
138
+ `);
139
+ },
140
+ },
113
141
  ];
114
142
 
115
143
  export async function runMigrations(db) {