@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.
- package/README.md +352 -366
- package/mcp-server/admin/auth.js +31 -4
- package/mcp-server/cli/direct_commands.js +313 -0
- package/mcp-server/cli/handlers/cloud_actions.js +138 -0
- package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
- package/mcp-server/cli/handlers/engine_actions.js +214 -0
- package/mcp-server/cli/handlers/prompt_actions.js +24 -0
- package/mcp-server/cli/handlers/storage_actions.js +749 -0
- package/mcp-server/cli/quick_stats.js +39 -0
- package/mcp-server/cli/ui.js +565 -0
- package/mcp-server/cli.js +324 -2085
- package/mcp-server/config/auth_store.js +56 -9
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +14 -1
- package/mcp-server/db/migrations.js +28 -0
- package/mcp-server/fact_format.js +244 -177
- package/mcp-server/identity.js +152 -0
- package/mcp-server/index.js +42 -679
- package/mcp-server/memory.js +50 -63
- package/mcp-server/prompt_manager.js +1 -1
- package/mcp-server/tools/helpers.js +39 -0
- package/mcp-server/tools/identity_tools.js +277 -0
- package/mcp-server/tools/index.js +9 -0
- package/mcp-server/tools/memory_tools.js +506 -0
- package/mcp-server/tools/rag_tools.js +235 -0
- package/opencode-plugin/index.js +460 -48
- package/package.json +7 -3
- package/skills/using-memory/SKILL.md +31 -14
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/quality_evaluator.js +0 -600
- package/mcp-server/benchmarks/run_benchmarks.js +0 -347
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- 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)
|
|
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)
|
|
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)
|
|
53
|
+
if (m) id = m[1];
|
|
36
54
|
}
|
|
37
55
|
} catch {}
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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) {
|