@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
@@ -0,0 +1,749 @@
1
+ import { join, basename } from "node:path";
2
+ import { getDatabase } from "../../db/database.js";
3
+ import {
4
+ readMemory,
5
+ readMemoryRaw,
6
+ writeMemory,
7
+ storeFilePath,
8
+ GLOBAL_KEY,
9
+ projectName,
10
+ projectKey,
11
+ listProjectStores,
12
+ migrateLegacyStore,
13
+ migrateStoreTitles,
14
+ memoryFileName,
15
+ canonicalPath,
16
+ MEMORY_DIR,
17
+ } from "../../memory.js";
18
+ import {
19
+ parseFactEntry,
20
+ factText,
21
+ factBody,
22
+ withMeta,
23
+ displayFact,
24
+ isKeepFact,
25
+ metaBadges,
26
+ formatFactEntry,
27
+ } from "../../fact_format.js";
28
+ import { deleteDocument } from "../../ingest/pipeline.js";
29
+ import { getModelStorageInfo, deleteModelCache, listAllCachedModels } from "../../ml/model_manager.js";
30
+ import {
31
+ EMBEDDING_PRESETS,
32
+ RERANKER_PRESETS,
33
+ selectSimpleMenu,
34
+ readTextInput,
35
+ promptText,
36
+ waitForEnter,
37
+ } from "../ui.js";
38
+
39
+ export async function handleStorageAction(value, config, stats) {
40
+ switch (value) {
41
+ case "notebook": {
42
+ let nbRunning = true;
43
+ while (nbRunning) {
44
+ const projKey = await projectKey(null, null);
45
+ const projLabel = await projectName(null, null);
46
+
47
+ async function browseFacts(key, title) {
48
+ let factRunning = true;
49
+ while (factRunning) {
50
+ const rawEntries = await readMemory(key);
51
+ const factList = await readMemoryRaw(key);
52
+
53
+ if (!factList || factList.length === 0) {
54
+ console.clear();
55
+ console.log(`\n \x1b[1m\x1b[37mNOTEBOOK FACTS: STORE EMPTY\x1b[0m`);
56
+ console.log(` [*] Notebook store [${key}] has no saved facts.\n`);
57
+ await waitForEnter();
58
+ return;
59
+ }
60
+
61
+ const file = memoryFileName(key);
62
+ const factItems = factList.map((fact, idx) => {
63
+ const badges = metaBadges(fact);
64
+ return {
65
+ label: `${idx + 1}. ${factText(fact)}`,
66
+ value: idx,
67
+ badge: badges.length ? badges.join(" ") : undefined,
68
+ info: `Select to manage this fact from ${file}`,
69
+ };
70
+ });
71
+ factItems.push({ label: "< Back", value: "back" });
72
+
73
+ const factRes = await selectSimpleMenu({
74
+ title: `NOTEBOOK FACTS [${title}]`,
75
+ subtitle: `Total facts: ${factList.length}`,
76
+ items: factItems,
77
+ });
78
+
79
+ if (factRes.action === "back" || factRes.value === "back") {
80
+ return;
81
+ }
82
+
83
+ const selectedIdx = factRes.value;
84
+ const selectedEntry = rawEntries[selectedIdx];
85
+ const selDisplay = displayFact(selectedEntry);
86
+ const selBadges = metaBadges(selectedEntry);
87
+
88
+ const actionItems = [
89
+ { label: "[UPDATE] Edit fact text", value: "update", info: "Rewrite the fact, keeping its date and metadata" },
90
+ ];
91
+ if (isKeepFact(selectedEntry)) {
92
+ actionItems.push({ label: "[UNPROTECT] Remove keep protection", value: "unprotect", info: "Allow forget to delete it without force" });
93
+ } else {
94
+ actionItems.push({ label: "[PROTECT] Mark as important (keep)", value: "protect", info: "forget will skip it unless force=true" });
95
+ }
96
+ actionItems.push({ label: "[DELETE] Delete this fact from store", value: "delete", info: "Remove fact permanently" });
97
+ actionItems.push({ label: "< Cancel / Back", value: "cancel" });
98
+
99
+ const actionRes = await selectSimpleMenu({
100
+ title: "FACT ACTION",
101
+ subtitle: `Fact: "${selDisplay}"${selBadges.length ? " [" + selBadges.join("] [") + "]" : ""}`,
102
+ items: actionItems,
103
+ });
104
+
105
+ if (actionRes.action === "back" || actionRes.value === "cancel") {
106
+ return;
107
+ }
108
+
109
+ if (actionRes.action === "select" && actionRes.value === "update") {
110
+ const p = parseFactEntry(selectedEntry);
111
+ const newText = await promptText(`New text for fact #${selectedIdx + 1}:`);
112
+ if (!newText) continue;
113
+ const newLine = formatFactEntry({ date: p.date, time: p.time, text: newText, meta: p.meta });
114
+ const updated = [...rawEntries];
115
+ updated[selectedIdx] = newLine;
116
+ await writeMemory(key, updated);
117
+ let links = 0;
118
+ try {
119
+ const db = await getDatabase();
120
+ const runRes = await db
121
+ .prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
122
+ .run(newText, key, factText(selectedEntry));
123
+ links = runRes ? runRes.changes : 0;
124
+ } catch (e) {}
125
+ console.clear();
126
+ console.log(`\n [OK] Fact updated successfully${links ? `, ${links} doc link(s) updated` : ""}.\n`);
127
+ await waitForEnter();
128
+ } else if (actionRes.action === "select" && (actionRes.value === "protect" || actionRes.value === "unprotect")) {
129
+ const updated = [...rawEntries];
130
+ updated[selectedIdx] =
131
+ actionRes.value === "protect" ? withMeta(selectedEntry, { keep: "1" }) : withMeta(selectedEntry, { keep: null });
132
+ await writeMemory(key, updated);
133
+ console.clear();
134
+ console.log(`\n [OK] Fact ${actionRes.value === "protect" ? "protected" : "unprotected"} successfully.\n`);
135
+ await waitForEnter();
136
+ } else if (actionRes.action === "select" && actionRes.value === "delete") {
137
+ const updated = [...rawEntries];
138
+ updated.splice(selectedIdx, 1);
139
+ await writeMemory(key, updated);
140
+ console.clear();
141
+ console.log("\n [OK] Fact deleted successfully.\n");
142
+ await waitForEnter();
143
+ }
144
+ }
145
+ }
146
+
147
+ const scopeItems = [
148
+ { label: "Global Memory", value: "global", badge: "global.md", info: "User facts stored across all projects" },
149
+ {
150
+ label: projKey ? `Project Memory (${projLabel})` : `Project Memory (${projLabel} - Not in git)`,
151
+ value: "project",
152
+ badge: projKey ? memoryFileName(projKey) : "none",
153
+ info: projKey ? `Facts bound to ${projKey}` : "Current directory is not a Git repository",
154
+ },
155
+ { label: "Project Stores (All Projects)", value: "projects", info: "List & browse every project memory store; bind legacy stores" },
156
+ { label: "< Back to Main Menu", value: "back" },
157
+ ];
158
+ const scopeRes = await selectSimpleMenu({
159
+ title: "NOTEBOOK FACTS MANAGEMENT",
160
+ subtitle: "Inspect & delete persistent user facts (Layer 1)",
161
+ items: scopeItems,
162
+ });
163
+
164
+ if (scopeRes.action === "back" || scopeRes.value === "back") {
165
+ nbRunning = false;
166
+ break;
167
+ }
168
+
169
+ if (scopeRes.value === "projects") {
170
+ let stores = await listProjectStores();
171
+ let storeRunning = true;
172
+ while (storeRunning) {
173
+ if (!stores.length) {
174
+ console.clear();
175
+ console.log(`\n \x1b[1m\x1b[37mPROJECT STORES: NONE FOUND\x1b[0m`);
176
+ console.log(" [*] No project memory stores found.\n");
177
+ await waitForEnter();
178
+ storeRunning = false;
179
+ break;
180
+ }
181
+ const storeItems = stores.map((s) => ({
182
+ label: `${s.basename} (${s.count})`,
183
+ badge: s.file,
184
+ hint: s.legacy ? "LEGACY" : "BOUND",
185
+ info: s.path ? `Bound to: ${s.path}` : `Unbound legacy store. View facts or bind to current dir: ${projKey}`,
186
+ value: s,
187
+ }));
188
+ storeItems.push({ label: "< Back", value: "back" });
189
+
190
+ const storeRes = await selectSimpleMenu({
191
+ title: "PROJECT MEMORY STORES",
192
+ subtitle: `Total stores: ${stores.length}`,
193
+ items: storeItems,
194
+ });
195
+
196
+ if (storeRes.action === "back" || storeRes.value === "back") {
197
+ storeRunning = false;
198
+ break;
199
+ }
200
+
201
+ const store = storeRes.value;
202
+ let actionRunning = true;
203
+ while (actionRunning) {
204
+ const actionItems = [
205
+ { label: "View facts", value: "view", info: `Browse ${store.count} fact(s) in ${store.file}` },
206
+ ];
207
+ if (store.legacy) {
208
+ actionItems.push({
209
+ label: "[MIGRATE] Bind to current directory",
210
+ value: "migrate",
211
+ info: `Rebind '${store.basename}' store from unbound legacy to ${projKey}`,
212
+ });
213
+ }
214
+ actionItems.push({ label: "< Cancel / Back", value: "cancel" });
215
+
216
+ const actRes = await selectSimpleMenu({
217
+ title: `STORE: ${store.basename}`,
218
+ subtitle: store.path || "Unbound legacy store",
219
+ items: actionItems,
220
+ });
221
+
222
+ if (actRes.action === "back" || actRes.value === "cancel") {
223
+ actionRunning = false;
224
+ break;
225
+ }
226
+ if (actRes.value === "view") {
227
+ await browseFacts(store.key, store.basename);
228
+ } else if (actRes.value === "migrate") {
229
+ const mig = await migrateLegacyStore(store.key, projKey);
230
+ console.clear();
231
+ if (mig.ok) {
232
+ console.log(`\n [OK] Legacy store '${store.basename}' bound to ${mig.key} (${mig.facts} fact(s)) [${mig.file}]\n`);
233
+ } else {
234
+ console.log(`\n [*] Could not migrate: ${mig.reason}\n`);
235
+ }
236
+ await waitForEnter();
237
+ stores = await listProjectStores();
238
+ actionRunning = false;
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ continue;
244
+ }
245
+
246
+ await browseFacts(scopeRes.value === "global" ? GLOBAL_KEY : projKey, scopeRes.value === "global" ? "GLOBAL" : projLabel);
247
+ }
248
+ break;
249
+ }
250
+
251
+ case "git_identity": {
252
+ let giRunning = true;
253
+ while (giRunning) {
254
+ const { resolveProjectIdentity, listIdentities } = await import("../../identity.js");
255
+ const identity = await resolveProjectIdentity(process.cwd());
256
+
257
+ const giItems = [
258
+ { label: "Show current identity & aliases", value: "show" },
259
+ { label: "Link current directory to Git project", value: "link" },
260
+ { label: "Unlink current directory from project", value: "unlink" },
261
+ { label: "Relink directory to another remote/identity", value: "relink" },
262
+ { label: "< Back to Main Menu", value: "back" }
263
+ ];
264
+
265
+ const giRes = await selectSimpleMenu({
266
+ title: "PROJECT IDENTITY CONTROL",
267
+ subtitle: identity ? `Current Identity: ${identity.key}` : "No Git repository / project identity linked.",
268
+ items: giItems,
269
+ });
270
+
271
+ if (giRes.action === "back" || giRes.value === "back") {
272
+ giRunning = false;
273
+ break;
274
+ }
275
+
276
+ const action = giRes.value;
277
+ if (action === "show") {
278
+ console.clear();
279
+ console.log(`\n \x1b[1m\x1b[37mCURRENT PROJECT IDENTITY\x1b[0m\n`);
280
+ if (identity) {
281
+ console.log(` - \x1b[1mKey:\x1b[0m ${identity.key}`);
282
+ console.log(` - \x1b[1mName:\x1b[0m ${identity.name}`);
283
+ console.log(` - \x1b[1mPrimary Remote:\x1b[0m ${identity.primaryRemote || "none"}`);
284
+ console.log(` - \x1b[1mToplevel Directory:\x1b[0m ${identity.toplevel}`);
285
+
286
+ const db = await getDatabase();
287
+ const aliases = await db.prepare("SELECT alias, kind FROM project_aliases WHERE identity_key = ?;").all(identity.key);
288
+ console.log(`\n \x1b[1mActive Aliases:\x1b[0m`);
289
+ for (const a of aliases) {
290
+ console.log(` - [${a.kind}] ${a.alias}`);
291
+ }
292
+ } else {
293
+ console.log(" No Git repository detected in the current directory.");
294
+ }
295
+ console.log(`\n \x1b[1mAll Known Project Identities in SQLite Registry:\x1b[0m`);
296
+ const db = await getDatabase();
297
+ const ids = await listIdentities(db);
298
+ if (ids.length > 0) {
299
+ for (const id of ids) {
300
+ console.log(` - \x1b[1m${id.key}\x1b[0m (${id.name})`);
301
+ for (const a of id.aliases) {
302
+ console.log(` - [${a.kind}] ${a.alias}`);
303
+ }
304
+ }
305
+ } else {
306
+ console.log(" None registered yet.");
307
+ }
308
+ await waitForEnter();
309
+ } else if (action === "link") {
310
+ console.clear();
311
+ console.log(`\n \x1b[1m\x1b[37mLINK PROJECT TO IDENTITY\x1b[0m\n`);
312
+ const remoteUrl = await promptText("Enter optional explicit remote URL (or press ENTER to auto-detect):");
313
+
314
+ try {
315
+ const { resolveProjectIdentity, upsertIdentity, registerAlias, normalizeRemoteUrl } = await import("../../identity.js");
316
+ const db = await getDatabase();
317
+
318
+ const dir = process.cwd();
319
+ const identity = await resolveProjectIdentity(dir);
320
+ if (!identity && !remoteUrl) {
321
+ console.log("\n \x1b[31m[ERROR] No Git repository detected and no remote URL specified.\x1b[0m");
322
+ await waitForEnter();
323
+ continue;
324
+ }
325
+
326
+ let key = identity ? identity.key : `git:${normalizeRemoteUrl(remoteUrl)}`;
327
+ let name = identity ? identity.name : basename(dir) || "unbound";
328
+ let primaryRemote = remoteUrl ? normalizeRemoteUrl(remoteUrl) : (identity ? identity.primaryRemote : null);
329
+
330
+ await upsertIdentity(db, { key, name, primaryRemote });
331
+
332
+ const aliases = [];
333
+ if (primaryRemote) {
334
+ aliases.push({ alias: `remote:${primaryRemote}`, kind: "remote" });
335
+ }
336
+ aliases.push({ alias: `path:${canonicalPath(dir)}`, kind: "path" });
337
+ aliases.push({ alias: `basename:${name}`, kind: "basename" });
338
+
339
+ for (const a of aliases) {
340
+ await registerAlias(db, { alias: a.alias, identityKey: key, kind: a.kind });
341
+ }
342
+
343
+ console.log(`\n \x1b[32m[SUCCESS] Successfully linked project!\x1b[0m`);
344
+ console.log(` Identity Key: ${key}`);
345
+ } catch (err) {
346
+ console.log(`\n \x1b[31m[ERROR] Link failed: ${err.message}\x1b[0m`);
347
+ }
348
+ await waitForEnter();
349
+ } else if (action === "unlink") {
350
+ console.clear();
351
+ console.log(`\n \x1b[1m\x1b[37mUNLINK PROJECT IDENTITY\x1b[0m\n`);
352
+ const confirm = await promptText("Are you sure you want to unlink the current path alias? (y/N):");
353
+ if (confirm.toLowerCase() === "y" || confirm.toLowerCase() === "yes") {
354
+ try {
355
+ const { unregisterAlias } = await import("../../identity.js");
356
+ const db = await getDatabase();
357
+ const alias = `path:${canonicalPath(process.cwd())}`;
358
+ await unregisterAlias(db, alias);
359
+ console.log(`\n \x1b[32m[SUCCESS] Unlinked path alias: ${alias}\x1b[0m`);
360
+ } catch (err) {
361
+ console.log(`\n \x1b[31m[ERROR] Unlink failed: ${err.message}\x1b[0m`);
362
+ }
363
+ }
364
+ await waitForEnter();
365
+ } else if (action === "relink") {
366
+ console.clear();
367
+ console.log(`\n \x1b[1m\x1b[37mRELINK PROJECT IDENTITY\x1b[0m\n`);
368
+ if (!identity) {
369
+ console.log(" No Git repository detected in the current directory.");
370
+ await waitForEnter();
371
+ continue;
372
+ }
373
+ const targetRemote = await promptText("Enter new target remote URL:");
374
+ if (!targetRemote) {
375
+ console.log(" Target remote URL cannot be empty.");
376
+ await waitForEnter();
377
+ continue;
378
+ }
379
+ try {
380
+ const { upsertIdentity, removeIdentity, normalizeRemoteUrl } = await import("../../identity.js");
381
+ const db = await getDatabase();
382
+
383
+ const targetKey = `git:${normalizeRemoteUrl(targetRemote)}`;
384
+ const sourceKey = identity.key;
385
+
386
+ if (sourceKey === targetKey) {
387
+ console.log(" Source and target identities are already identical.");
388
+ await waitForEnter();
389
+ continue;
390
+ }
391
+
392
+ const sourceFacts = await readMemory(sourceKey);
393
+ const targetFacts = await readMemory(targetKey);
394
+ const seen = new Set(targetFacts.map((e) => factBody(e).toLowerCase().trim()));
395
+
396
+ let mergedCount = 0;
397
+ for (const f of sourceFacts) {
398
+ const body = factBody(f).toLowerCase().trim();
399
+ if (!seen.has(body)) {
400
+ seen.add(body);
401
+ targetFacts.push(f);
402
+ mergedCount++;
403
+ }
404
+ }
405
+
406
+ await writeMemory(targetKey, targetFacts);
407
+ await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
408
+ await upsertIdentity(db, { key: targetKey, name: identity.name, primaryRemote: normalizeRemoteUrl(targetRemote) });
409
+ await removeIdentity(db, sourceKey);
410
+
411
+ try {
412
+ const sourceFp = storeFilePath(sourceKey);
413
+ const { existsSync } = await import("node:fs");
414
+ if (existsSync(sourceFp)) {
415
+ const { unlink } = await import("fs/promises");
416
+ await unlink(sourceFp);
417
+ }
418
+ } catch (e) {}
419
+
420
+ console.log(`\n \x1b[32m[SUCCESS] Relinked and merged ${mergedCount} facts successfully!\x1b[0m`);
421
+ } catch (err) {
422
+ console.log(`\n \x1b[31m[ERROR] Relink failed: ${err.message}\x1b[0m`);
423
+ }
424
+ await waitForEnter();
425
+ }
426
+ }
427
+ break;
428
+ }
429
+
430
+ case "migrate_titles": {
431
+ console.log(`\n \x1b[1m\x1b[37mMIGRATE TITLES TO LEGACY FACTS\x1b[0m\n`);
432
+ console.log(" Scans every store and stamps an auto-generated **Title** onto");
433
+ console.log(" facts that lack one (Part A1 legacy migration).\n");
434
+ try {
435
+ const targets = [];
436
+ const gitKey = await projectKey(process.cwd(), null);
437
+ if (gitKey) targets.push(gitKey);
438
+ targets.push(GLOBAL_KEY);
439
+ const stores = await listProjectStores();
440
+ for (const s of stores) {
441
+ if (!targets.includes(s.key)) targets.push(s.key);
442
+ }
443
+
444
+ let total = 0;
445
+ for (const k of targets) {
446
+ const res = await migrateStoreTitles(k);
447
+ if (res.ok) {
448
+ total += res.changed;
449
+ console.log(` [OK] ${k}: ${res.changed} fact(s) titled`);
450
+ } else {
451
+ console.log(` [SKIP] ${k}: ${res.reason}`);
452
+ }
453
+ }
454
+ console.log(`\n \x1b[32m[DONE] ${total} fact(s) updated across ${targets.length} store(s).\x1b[0m`);
455
+ } catch (err) {
456
+ console.log(`\n \x1b[31m[ERROR] ${err.message}\x1b[0m`);
457
+ }
458
+ await waitForEnter();
459
+ break;
460
+ }
461
+
462
+ case "rag_docs": {
463
+ let docRunning = true;
464
+ while (docRunning) {
465
+ const db = await getDatabase();
466
+ const docs = await db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
467
+
468
+ if (!docs || docs.length === 0) {
469
+ console.clear();
470
+ console.log(`\n \x1b[1m\x1b[37mRAG DOCUMENTS: BASE EMPTY\x1b[0m`);
471
+ console.log("\n [*] RAG Knowledge Base is empty. No documents ingested.\n");
472
+ await waitForEnter();
473
+ docRunning = false;
474
+ break;
475
+ }
476
+
477
+ const docItems = docs.map((doc) => {
478
+ const rawDate = doc.created_at || doc.updated_at || "";
479
+ let formattedDate = "";
480
+ if (rawDate) {
481
+ try {
482
+ const d = typeof rawDate === "number" ? new Date(rawDate) : new Date(String(rawDate));
483
+ formattedDate = isNaN(d.getTime()) ? String(rawDate).substring(0, 16) : d.toISOString().replace("T", " ").substring(0, 16);
484
+ } catch (e) {
485
+ formattedDate = String(rawDate).substring(0, 16);
486
+ }
487
+ }
488
+ const docIdStr = doc.id != null ? String(doc.id) : "";
489
+ return {
490
+ label: doc.title || doc.path || "Untitled Document",
491
+ badge: formattedDate,
492
+ hint: docIdStr ? `ID: ${docIdStr.substring(0, 8)}...` : "",
493
+ info: `Path: ${doc.path || "N/A"}`,
494
+ value: doc,
495
+ };
496
+ });
497
+ docItems.push({ label: "< Back to Main Menu", value: "back" });
498
+
499
+ const docRes = await selectSimpleMenu({
500
+ title: "RAG KNOWLEDGE BASE DOCUMENTS",
501
+ subtitle: `Total ingested documents: ${docs.length}`,
502
+ items: docItems,
503
+ });
504
+
505
+ if (docRes.action === "back" || docRes.value === "back") {
506
+ docRunning = false;
507
+ break;
508
+ }
509
+
510
+ const targetDoc = docRes.value;
511
+ const actionRes = await selectSimpleMenu({
512
+ title: "DOCUMENT ACTION",
513
+ subtitle: targetDoc.title || targetDoc.path,
514
+ items: [
515
+ { label: "[INFO] View Details & Sections", value: "info", info: "Inspect micro-chunks and sections count" },
516
+ { label: "[EXPORT JSON] Export Full Hierarchy to Pretty JSON", value: "export_json", info: "Export multiline JSON with doc metadata & all 3 hierarchy levels" },
517
+ { label: "[DELETE] Delete Document from RAG Base", value: "delete", info: "Purge document, FTS5 index & vectors" },
518
+ { label: "< Cancel / Back", value: "cancel" },
519
+ ],
520
+ });
521
+
522
+ if (actionRes.action === "select" && actionRes.value === "info") {
523
+ const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id);
524
+ const secCount = secCountRow ? secCountRow.cnt : 0;
525
+ const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id);
526
+ const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
527
+ const sampleSections = await db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
528
+
529
+ console.clear();
530
+ console.log(`\n \x1b[1m\x1b[37mDOCUMENT DETAILS\x1b[0m\n`);
531
+ console.log(` Title: ${targetDoc.title || "Untitled"}`);
532
+ console.log(` ID: ${targetDoc.id}`);
533
+ console.log(` Path: ${targetDoc.path || "N/A"}`);
534
+ console.log(` Created: ${targetDoc.created_at}`);
535
+ console.log(` Sections Count: ${secCount}`);
536
+ console.log(` Micro-Chunks: ${chunkCount}`);
537
+ if (sampleSections.length > 0) {
538
+ console.log("\n Sample Section Headings:");
539
+ sampleSections.forEach((s, idx) => console.log(` ${idx + 1}. ${s.heading || "Untitled Section"}`));
540
+ }
541
+ console.log("\n");
542
+ await waitForEnter();
543
+ } else if (actionRes.action === "select" && actionRes.value === "export_json") {
544
+ const { exportDocumentToFile } = await import("../../ingest/exporter.js");
545
+ const outFile = exportDocumentToFile(targetDoc.id, null, db);
546
+ console.clear();
547
+ console.log(`\n \x1b[32m[OK] Full document JSON exported to:\x1b[0m`);
548
+ console.log(` \x1b[36m${outFile}\x1b[0m\n`);
549
+ await waitForEnter();
550
+ } else if (actionRes.action === "select" && actionRes.value === "delete") {
551
+ await deleteDocument(targetDoc.id, db);
552
+ console.clear();
553
+ console.log(`\n [OK] Document "${targetDoc.title || targetDoc.path}" deleted from RAG base.\n`);
554
+ await waitForEnter();
555
+ }
556
+ }
557
+ break;
558
+ }
559
+ case "export_snapshot": {
560
+ const { exportSnapshot } = await import("../../admin/snapshot.js");
561
+ const defaultPath = join(MEMORY_DIR, "exports", `rag_snapshot_${Date.now()}.json.gz`);
562
+ const pathRes = await readTextInput("Enter Output Snapshot Path (.json or .json.gz)", defaultPath);
563
+ if (pathRes.action === "submit" && pathRes.value) {
564
+ console.clear();
565
+ console.log(`\n [EXPORT] Exporting full snapshot to: \x1b[36m${pathRes.value}\x1b[0m...\n`);
566
+ try {
567
+ const res = await exportSnapshot({ outputPath: pathRes.value });
568
+ console.log(` \x1b[32m[OK] Snapshot exported successfully!\x1b[0m`);
569
+ console.log(` Documents: ${res.snapshot.documents ? res.snapshot.documents.length : 0}`);
570
+ console.log(` Micro-Chunks: ${res.snapshot.micro_chunks ? res.snapshot.micro_chunks.length : 0}`);
571
+ console.log(` Blobs: ${res.snapshot.blobs ? res.snapshot.blobs.length : 0}`);
572
+ console.log(` Output: ${res.outputPath}\n`);
573
+ } catch (err) {
574
+ console.error(` \x1b[31m[ERROR] Snapshot export failed: ${err.message}\x1b[0m\n`);
575
+ }
576
+ await waitForEnter();
577
+ }
578
+ break;
579
+ }
580
+ case "import_snapshot": {
581
+ const { importSnapshot, listAvailableSnapshots } = await import("../../admin/snapshot.js");
582
+ const availableSnapshots = listAvailableSnapshots();
583
+
584
+ let chosenPath = null;
585
+
586
+ if (availableSnapshots.length > 0) {
587
+ const menuItems = availableSnapshots.map((s) => ({
588
+ label: s.name,
589
+ badge: `${s.sizeMB} MB`,
590
+ hint: s.dateStr,
591
+ info: `Path: ${s.path}`,
592
+ value: s.path,
593
+ }));
594
+
595
+ menuItems.push({
596
+ label: "[MANUAL ENTRY] Enter Custom Snapshot File Path...",
597
+ value: "manual",
598
+ info: "Type or paste an absolute file path to a .json or .json.gz snapshot file",
599
+ });
600
+ menuItems.push({ label: "< Cancel / Back", value: "back" });
601
+
602
+ const subRes = await selectSimpleMenu({
603
+ title: "SELECT SNAPSHOT FOR IMPORT",
604
+ subtitle: `Found ${availableSnapshots.length} snapshot files in exports directory`,
605
+ items: menuItems,
606
+ });
607
+
608
+ if (subRes.action === "back" || subRes.value === "back") {
609
+ break;
610
+ }
611
+
612
+ if (subRes.value === "manual") {
613
+ const inputRes = await readTextInput("Enter Input Snapshot Path (.json or .json.gz)");
614
+ if (inputRes.action === "submit" && inputRes.value) {
615
+ chosenPath = inputRes.value;
616
+ } else {
617
+ break;
618
+ }
619
+ } else {
620
+ chosenPath = subRes.value;
621
+ }
622
+ } else {
623
+ const inputRes = await readTextInput("Enter Input Snapshot Path (.json or .json.gz)");
624
+ if (inputRes.action === "submit" && inputRes.value) {
625
+ chosenPath = inputRes.value;
626
+ } else {
627
+ break;
628
+ }
629
+ }
630
+
631
+ if (chosenPath) {
632
+ console.clear();
633
+ console.log(`\n [IMPORT] Importing snapshot from: \x1b[36m${chosenPath}\x1b[0m...\n`);
634
+ try {
635
+ const res = await importSnapshot({ snapshotPathOrData: chosenPath });
636
+ console.log(` \x1b[32m[OK] Snapshot imported successfully!\x1b[0m`);
637
+ console.log(` Documents: ${res.documents}`);
638
+ console.log(` Sections: ${res.sections}`);
639
+ console.log(` Medium-Chunks:${res.medium_chunks}`);
640
+ console.log(` Micro-Chunks: ${res.micro_chunks}`);
641
+ console.log(` Blobs: ${res.blobs}\n`);
642
+ } catch (err) {
643
+ console.error(` \x1b[31m[ERROR] Snapshot import failed: ${err.message}\x1b[0m\n`);
644
+ }
645
+ await waitForEnter();
646
+ }
647
+ break;
648
+ }
649
+ case "hard_reset": {
650
+ const confirmRes = await selectSimpleMenu({
651
+ title: "HARD RESET DATABASE & BLOB STORAGE",
652
+ subtitle: `Permanently purge all ${stats.docCount} docs, ${stats.chunkCount} chunks & blobs`,
653
+ items: [
654
+ {
655
+ label: "[CONFIRM HARD RESET] Purge All Documents, Vectors & Blobs",
656
+ value: "confirm",
657
+ info: "WARNING: Irreversible deletion of all SQLite documents, micro-chunks, and CAS blobs!",
658
+ },
659
+ { label: "< Cancel / Back", value: "cancel" },
660
+ ],
661
+ });
662
+
663
+ if (confirmRes.action === "select" && confirmRes.value === "confirm") {
664
+ const { hardResetDatabase } = await import("../../admin/snapshot.js");
665
+ const res = hardResetDatabase();
666
+ console.clear();
667
+ console.log(`\n \x1b[32m[OK] HARD RESET COMPLETED SUCCESSFULLY!\x1b[0m`);
668
+ console.log(` Purged Documents: ${res.purgedDocuments}`);
669
+ console.log(` Purged Chunks: ${res.purgedChunks}`);
670
+ console.log(` Purged Blobs: ${res.purgedBlobs}\n`);
671
+ await waitForEnter();
672
+ }
673
+ break;
674
+ }
675
+ case "manage_models": {
676
+ let modelMgmtRunning = true;
677
+ while (modelMgmtRunning) {
678
+ const allPresets = [...new Set([...EMBEDDING_PRESETS, ...RERANKER_PRESETS.filter((r) => r !== "none")])];
679
+ const cachedOnDisk = listAllCachedModels();
680
+ const diskModelNames = cachedOnDisk.map((m) => m.modelName);
681
+
682
+ const combinedModels = [...new Set([...allPresets, ...diskModelNames])];
683
+
684
+ let totalDiskBytes = 0;
685
+ const modelItems = combinedModels.map((m) => {
686
+ const info = getModelStorageInfo(m);
687
+ totalDiskBytes += info.bytes;
688
+ let badge = "NOT DOWNLOADED";
689
+ if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
690
+ else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
691
+
692
+ return {
693
+ label: m,
694
+ badge,
695
+ value: m,
696
+ info: info.status !== "not_downloaded"
697
+ ? `Size: ${info.sizeMB} MB | Select to inspect or delete from disk`
698
+ : "Model weights not present on local disk",
699
+ };
700
+ });
701
+
702
+ modelItems.push({ label: "< Back to Main Menu", value: "back" });
703
+
704
+ const totalDiskMB = (totalDiskBytes / (1024 * 1024)).toFixed(2);
705
+ const subRes = await selectSimpleMenu({
706
+ title: "ML MODEL CACHE MANAGEMENT",
707
+ subtitle: `Total ML Storage Used: ${totalDiskMB} MB | Models Tracked: ${combinedModels.length}`,
708
+ items: modelItems,
709
+ });
710
+
711
+ if (subRes.action === "back" || subRes.value === "back") {
712
+ modelMgmtRunning = false;
713
+ break;
714
+ }
715
+
716
+ const selectedModel = subRes.value;
717
+ const selectedInfo = getModelStorageInfo(selectedModel);
718
+
719
+ if (selectedInfo.status === "not_downloaded") {
720
+ console.clear();
721
+ console.log(`\n [*] Model "${selectedModel}" is not downloaded on local disk.\n`);
722
+ await waitForEnter();
723
+ continue;
724
+ }
725
+
726
+ const actionRes = await selectSimpleMenu({
727
+ title: `MODEL ACTION: ${selectedModel}`,
728
+ subtitle: `Status: ${selectedInfo.status.toUpperCase()} | Size: ${selectedInfo.sizeMB} MB`,
729
+ items: [
730
+ { label: `[PURGE] Delete model weights from disk (${selectedInfo.sizeMB} MB)`, value: "delete", info: `Delete ${selectedInfo.dir} permanently` },
731
+ { label: "< Cancel / Back", value: "cancel" },
732
+ ],
733
+ });
734
+
735
+ if (actionRes.action === "select" && actionRes.value === "delete") {
736
+ const delRes = deleteModelCache(selectedModel);
737
+ console.clear();
738
+ if (delRes.deleted) {
739
+ console.log(`\n \x1b[32m[OK] Model "${selectedModel}" deleted successfully (${delRes.freedMB} MB freed).\x1b[0m\n`);
740
+ } else {
741
+ console.error(`\n \x1b[31m[ERROR] Failed to delete model: ${delRes.reason}\x1b[0m\n`);
742
+ }
743
+ await waitForEnter();
744
+ }
745
+ }
746
+ break;
747
+ }
748
+ }
749
+ }