@lotargo/memory_plugin 1.4.620 → 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 (35) hide show
  1. package/README.md +352 -334
  2. package/mcp-server/admin/auth.js +293 -42
  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 -1945
  12. package/mcp-server/config/auth_store.js +178 -19
  13. package/mcp-server/config/config_manager.js +1 -0
  14. package/mcp-server/db/database.js +18 -3
  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/setup.js +41 -0
  22. package/mcp-server/tools/helpers.js +39 -0
  23. package/mcp-server/tools/identity_tools.js +277 -0
  24. package/mcp-server/tools/index.js +9 -0
  25. package/mcp-server/tools/memory_tools.js +506 -0
  26. package/mcp-server/tools/rag_tools.js +235 -0
  27. package/opencode-plugin/index.js +460 -48
  28. package/package.json +7 -3
  29. package/skills/using-memory/SKILL.md +31 -14
  30. package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
  31. package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
  32. package/mcp-server/benchmarks/quality_evaluator.js +0 -600
  33. package/mcp-server/benchmarks/run_benchmarks.js +0 -347
  34. package/mcp-server/benchmarks/stress_ingestion.js +0 -195
  35. 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
@@ -95,39 +118,175 @@ export function decryptData(encryptedStr) {
95
118
  return decrypted;
96
119
  }
97
120
 
98
- // Save secrets securely
99
- export function saveSecrets(secrets) {
100
- ensureDirSync();
101
- const plainText = JSON.stringify(secrets);
102
- const encrypted = encryptData(plainText);
103
- fs.writeFileSync(SECRETS_FILE, encrypted, "utf-8");
121
+ // Load a KEY=VALUE .env file from MEMORY_DIR (global environment override for
122
+ // headless / Docker / CI deployments). Values may optionally be quoted.
123
+ function loadEnvFile() {
124
+ const envFile = path.join(MEMORY_DIR, ".env");
125
+ if (!fs.existsSync(envFile)) return {};
126
+ try {
127
+ const out = {};
128
+ const raw = fs.readFileSync(envFile, "utf-8");
129
+ for (const line of raw.split(/\r?\n/)) {
130
+ const trimmed = line.trim();
131
+ if (!trimmed || trimmed.startsWith("#")) continue;
132
+ const eq = trimmed.indexOf("=");
133
+ if (eq <= 0) continue;
134
+ const key = trimmed.slice(0, eq).trim();
135
+ let value = trimmed.slice(eq + 1).trim();
136
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
137
+ value = value.slice(1, -1);
138
+ }
139
+ if (key) out[key] = value;
140
+ }
141
+ return out;
142
+ } catch {
143
+ return {};
144
+ }
104
145
  }
105
146
 
106
- // Load secrets securely
107
- export function loadSecrets() {
108
- if (!fs.existsSync(SECRETS_FILE)) {
109
- return null;
147
+ const ENV_DB_URL_KEYS = ["TURSO_DB_URL", "TURSO_URL"];
148
+ const ENV_DB_TOKEN_KEYS = ["TURSO_DB_TOKEN", "TURSO_TOKEN"];
149
+
150
+ function firstDefined(source, keys) {
151
+ for (const k of keys) {
152
+ const v = source[k];
153
+ if (v && String(v).trim()) return String(v).trim();
110
154
  }
155
+ return null;
156
+ }
157
+
158
+ // Resolve cloud credentials from the environment (process.env) or a
159
+ // MEMORY_DIR/.env file — the headless alternative to browser OAuth login.
160
+ // Supported vars: TURSO_DB_URL / TURSO_URL, TURSO_DB_TOKEN / TURSO_TOKEN,
161
+ // TURSO_API_TOKEN, TURSO_ORG, TURSO_DATABASE / TURSO_DB_NAME, TURSO_USERNAME.
162
+ // Returns null when no cloud secrets are present at all.
163
+ export function resolveEnvSecrets() {
164
+ const fileVars = loadEnvFile();
165
+ const merged = { ...fileVars, ...process.env };
166
+ const dbUrl = firstDefined(merged, ENV_DB_URL_KEYS);
167
+ const token = firstDefined(merged, ENV_DB_TOKEN_KEYS);
168
+ const apiToken = firstDefined(merged, ["TURSO_API_TOKEN"]);
169
+ const org = firstDefined(merged, ["TURSO_ORG"]);
170
+ const database = firstDefined(merged, ["TURSO_DATABASE", "TURSO_DB_NAME"]);
171
+ const username = firstDefined(merged, ["TURSO_USERNAME"]);
172
+ if (!dbUrl && !token && !apiToken) return null;
173
+ return { dbUrl, token, apiToken, org, database, username, source: "env" };
174
+ }
175
+
176
+ // Read the encrypted store file ONLY (no environment merge). Returns the raw
177
+ // parsed record, or null when the file is missing / undecryptable.
178
+ function readStoredSecrets() {
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 {}
111
194
  try {
112
195
  const encrypted = fs.readFileSync(SECRETS_FILE, "utf-8").trim();
113
- if (!encrypted) return null;
114
- const decrypted = decryptData(encrypted);
115
- return JSON.parse(decrypted);
196
+ if (!encrypted) { _cachedSecrets = null; return null; }
197
+ _cachedSecrets = JSON.parse(decryptData(encrypted));
198
+ return _cachedSecrets;
116
199
  } catch (err) {
117
200
  console.error(
118
201
  "Failed to decrypt or load cloud secrets:",
119
202
  err.message,
120
203
  "— the file was encrypted with a different machine key. Re-run login to recreate it."
121
204
  );
205
+ _cachedSecrets = null;
122
206
  return null;
123
207
  }
124
208
  }
125
209
 
210
+ // Where are cloud credentials coming from right now?
211
+ // "env" — TURSO_API_TOKEN / TURSO_DB_URL + TURSO_DB_TOKEN from env or .env
212
+ // "api-key" — a stored Turso account API-token session (takes priority over browser)
213
+ // "store" — a stored browser OAuth / database-token session
214
+ // null — nothing configured
215
+ export function getSecretsSource() {
216
+ const envSecrets = resolveEnvSecrets();
217
+ if (envSecrets && (envSecrets.apiToken || envSecrets.dbUrl)) return "env";
218
+ const stored = readStoredSecrets();
219
+ if (stored) return stored.apiToken ? "api-key" : "store";
220
+ return null;
221
+ }
222
+
223
+ // Save secrets securely
224
+ export function saveSecrets(secrets) {
225
+ ensureDirSync();
226
+ const plainText = JSON.stringify(secrets);
227
+ const encrypted = encryptData(plainText);
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();
232
+ }
233
+
234
+ let _onSecretsChanged = null;
235
+ export function onSecretsChanged(cb) { _onSecretsChanged = cb; }
236
+
237
+ // Load secrets securely. Priority (highest first):
238
+ // 1. Env account API token (TURSO_API_TOKEN) — reused from the store when a
239
+ // session was already minted for this exact token, else returned with
240
+ // needsResolution: true so callers can mint a DB JWT asynchronously.
241
+ // 2. Env database URL + token (TURSO_DB_URL + TURSO_DB_TOKEN).
242
+ // 3. Encrypted store: an API-key session beats a browser/database-token session.
243
+ // The env sources let Docker, Google Jules and VPS deployments work without
244
+ // any interactive login step.
245
+ export function loadSecrets() {
246
+ const envSecrets = resolveEnvSecrets();
247
+ if (envSecrets && envSecrets.apiToken) {
248
+ const stored = readStoredSecrets();
249
+ if (stored && stored.apiToken === envSecrets.apiToken && stored.dbUrl) {
250
+ return { ...stored, source: "api-key" };
251
+ }
252
+ return {
253
+ token: envSecrets.apiToken,
254
+ apiToken: envSecrets.apiToken,
255
+ dbUrl: envSecrets.dbUrl || "",
256
+ org: envSecrets.org || "",
257
+ db: envSecrets.database || "",
258
+ username: envSecrets.username || "",
259
+ authorized: true,
260
+ source: "env",
261
+ needsResolution: true,
262
+ };
263
+ }
264
+ if (envSecrets && envSecrets.dbUrl && envSecrets.token) {
265
+ return {
266
+ token: envSecrets.token,
267
+ dbUrl: envSecrets.dbUrl,
268
+ org: envSecrets.org || "",
269
+ db: envSecrets.database || "",
270
+ username: envSecrets.username || "",
271
+ authorized: true,
272
+ source: "env",
273
+ };
274
+ }
275
+ const stored = readStoredSecrets();
276
+ if (stored) {
277
+ if (stored.apiToken) return { ...stored, source: "api-key" };
278
+ return stored;
279
+ }
280
+ return null;
281
+ }
282
+
126
283
  // Delete secrets from disk
127
284
  export function deleteSecrets() {
128
285
  if (fs.existsSync(SECRETS_FILE)) {
129
286
  try {
130
287
  fs.unlinkSync(SECRETS_FILE);
288
+ _cachedSecrets = null;
289
+ _cachedSecretsMtime = 0;
131
290
  return true;
132
291
  } catch (err) {
133
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)
@@ -4,11 +4,13 @@ import { existsSync, mkdirSync } from "fs";
4
4
  import { MEMORY_DIR } from "../memory.js";
5
5
  import { runMigrations } from "./migrations.js";
6
6
  import { getConfig } from "../config/config_manager.js";
7
- import { loadSecrets } from "../config/auth_store.js";
7
+ import { resolveCloudSecrets } from "../admin/auth.js";
8
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");
@@ -159,7 +161,9 @@ async function openDatabase(customPath, mode) {
159
161
  let cloudClient = null;
160
162
  let failoverClient = null;
161
163
  if (mode === "only-cloud" || mode === "hybrid-sync") {
162
- const secrets = loadSecrets();
164
+ // Resolve working cloud credentials. An env TURSO_API_TOKEN (which can only
165
+ // call the Platform API) is lazily minted into a per-database JWT here.
166
+ const secrets = await resolveCloudSecrets();
163
167
  const tursoUrl = customPath && customPath.startsWith("libsql:") ? customPath : (secrets?.dbUrl || config.tursoUrl);
164
168
  const failoverUrl = config.failoverUrl || "";
165
169
  const token = secrets?.token;
@@ -205,10 +209,20 @@ export async function getDatabase(customPath = null, forceMode = null) {
205
209
  if (dbInstance && dbInstance.mode === mode) {
206
210
  return dbInstance;
207
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
+ }
208
216
  // Deduplicate concurrent default-DB initialization so migrations never run
209
217
  // on multiple connections at once (avoids "database is locked" crashes).
210
218
  if (!dbInitPromise) {
211
- 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(() => {
212
226
  dbInitPromise = null;
213
227
  });
214
228
  }
@@ -220,6 +234,7 @@ export async function getDatabase(customPath = null, forceMode = null) {
220
234
 
221
235
  export function closeDatabase() {
222
236
  dbInitPromise = null;
237
+ dbLastFailAt = 0;
223
238
  if (dbInstance) {
224
239
  dbInstance.close();
225
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) {