@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
@@ -1,9 +1,12 @@
1
1
  import http from "node:http";
2
2
  import crypto from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
- import { saveSecrets, deleteSecrets, loadSecrets, resolveEnvSecrets, getSecretsSource } from "../config/auth_store.js";
4
+ import { saveSecrets, deleteSecrets, loadSecrets, resolveEnvSecrets, getSecretsSource, invalidateAuthCache, onSecretsChanged } from "../config/auth_store.js";
5
5
  import { getConfig, updateConfig } from "../config/config_manager.js";
6
6
 
7
+ // Register callback so resolveCloudSecrets() cache is cleared when secrets change
8
+ onSecretsChanged(() => { _cachedResolvedSecrets = undefined; _cachedResolvedAt = 0; });
9
+
7
10
  export const TURSO_API_BASE = () => process.env.TURSO_API_BASE || "https://api.turso.tech";
8
11
 
9
12
  function openBrowser(url) {
@@ -541,9 +544,23 @@ export async function loginFromEnv({ persist = false } = {}) {
541
544
  // Used by database.js at startup so a raw TURSO_API_TOKEN environment token
542
545
  // (which can only call the Platform API, not libsql) gets minted into a
543
546
  // per-database JWT without any interactive step.
547
+ let _cachedResolvedSecrets = undefined;
548
+ let _cachedResolvedAt = 0;
549
+ const RESOLVED_CACHE_TTL_MS = 60_000; // 60s — avoids re-minting JWT on every getDatabase()
550
+
551
+ export function invalidateResolvedCache() {
552
+ _cachedResolvedSecrets = undefined;
553
+ _cachedResolvedAt = 0;
554
+ }
555
+
544
556
  export async function resolveCloudSecrets() {
557
+ const now = Date.now();
558
+ if (_cachedResolvedSecrets !== undefined && (now - _cachedResolvedAt) < RESOLVED_CACHE_TTL_MS) {
559
+ return _cachedResolvedSecrets;
560
+ }
561
+
545
562
  const secrets = loadSecrets();
546
- if (!secrets) return null;
563
+ if (!secrets) { _cachedResolvedSecrets = null; return null; }
547
564
  if (secrets.apiToken && secrets.needsResolution) {
548
565
  const resolved = await loginWithApiToken({
549
566
  token: secrets.apiToken,
@@ -552,9 +569,13 @@ export async function resolveCloudSecrets() {
552
569
  username: secrets.username || null,
553
570
  persist: false,
554
571
  });
555
- return { ...resolved, source: "env" };
572
+ _cachedResolvedSecrets = { ...resolved, source: "env" };
573
+ _cachedResolvedAt = Date.now();
574
+ return _cachedResolvedSecrets;
556
575
  }
557
- return secrets;
576
+ _cachedResolvedSecrets = secrets;
577
+ _cachedResolvedAt = Date.now();
578
+ return _cachedResolvedSecrets;
558
579
  }
559
580
 
560
581
  // Store/replace a Turso account API token. Alias of the headless login flow:
@@ -577,9 +598,13 @@ export function clearApiKey() {
577
598
  delete rest.apiToken;
578
599
  if (rest.token && rest.dbUrl) {
579
600
  saveSecrets({ ...rest, authorized: true });
601
+ invalidateAuthCache();
602
+ invalidateResolvedCache();
580
603
  return { removed: true, keptDbSession: true };
581
604
  }
582
605
  deleteSecrets();
606
+ invalidateAuthCache();
607
+ invalidateResolvedCache();
583
608
  return { removed: true, keptDbSession: false };
584
609
  }
585
610
 
@@ -604,6 +629,8 @@ export function getAuthStatus() {
604
629
  // Logout and reset configurations
605
630
  export function logoutFromCloud() {
606
631
  const deleted = deleteSecrets();
632
+ invalidateAuthCache();
633
+ invalidateResolvedCache();
607
634
  updateConfig({ tursoUrl: "", mode: "only-local", authorized: false, username: "" });
608
635
  return deleted;
609
636
  }
@@ -0,0 +1,313 @@
1
+ import { basename } from "node:path";
2
+ import {
3
+ readMemory,
4
+ writeMemory,
5
+ storeFilePath,
6
+ canonicalPath,
7
+ listProjectStores,
8
+ migrateStoreTitles,
9
+ GLOBAL_KEY,
10
+ projectKey,
11
+ } from "../memory.js";
12
+ import { factBody } from "../fact_format.js";
13
+
14
+ export async function handleDirectCommands(cliArgs) {
15
+ if (cliArgs[0] === "link") {
16
+ const dirIdx = cliArgs.indexOf("--dir");
17
+ const dir = dirIdx >= 0 && cliArgs[dirIdx + 1] ? cliArgs[dirIdx + 1] : process.cwd();
18
+ const remIdx = cliArgs.indexOf("--remote");
19
+ const remote = remIdx >= 0 && cliArgs[remIdx + 1] ? cliArgs[remIdx + 1] : null;
20
+
21
+ try {
22
+ const { getDatabase } = await import("../db/database.js");
23
+ const { resolveProjectIdentity, upsertIdentity, registerAlias, normalizeRemoteUrl } = await import("../identity.js");
24
+ const db = await getDatabase();
25
+
26
+ const identity = await resolveProjectIdentity(dir);
27
+ if (!identity && !remote) {
28
+ console.error("Error: No Git repository detected and no remote URL specified.");
29
+ process.exit(1);
30
+ }
31
+
32
+ let key = identity ? identity.key : `git:${normalizeRemoteUrl(remote)}`;
33
+ let name = identity ? identity.name : basename(dir) || "unbound";
34
+ let primaryRemote = remote ? normalizeRemoteUrl(remote) : (identity ? identity.primaryRemote : null);
35
+
36
+ await upsertIdentity(db, { key, name, primaryRemote });
37
+
38
+ const aliases = [];
39
+ if (primaryRemote) {
40
+ aliases.push({ alias: `remote:${primaryRemote}`, kind: "remote" });
41
+ }
42
+ aliases.push({ alias: `path:${canonicalPath(dir)}`, kind: "path" });
43
+ aliases.push({ alias: `basename:${name}`, kind: "basename" });
44
+
45
+ for (const a of aliases) {
46
+ await registerAlias(db, { alias: a.alias, identityKey: key, kind: a.kind });
47
+ }
48
+
49
+ console.log(`\n [OK] Linked directory "${dir}" successfully to identity key: ${key}\n`);
50
+ } catch (err) {
51
+ console.error(`Error: ${err.message}`);
52
+ process.exit(1);
53
+ }
54
+ return true;
55
+ }
56
+
57
+ if (cliArgs[0] === "unlink") {
58
+ const dirIdx = cliArgs.indexOf("--dir");
59
+ const dir = dirIdx >= 0 && cliArgs[dirIdx + 1] ? cliArgs[dirIdx + 1] : process.cwd();
60
+ const purge = cliArgs.includes("--purge");
61
+
62
+ try {
63
+ const { getDatabase } = await import("../db/database.js");
64
+ const { unregisterAlias, removeIdentity, resolveProjectIdentity } = await import("../identity.js");
65
+ const db = await getDatabase();
66
+
67
+ const alias = `path:${canonicalPath(dir)}`;
68
+ await unregisterAlias(db, alias);
69
+
70
+ if (purge) {
71
+ const identity = await resolveProjectIdentity(dir);
72
+ if (identity) {
73
+ await removeIdentity(db, identity.key);
74
+ }
75
+ }
76
+
77
+ console.log(`\n [OK] Unlinked directory "${dir}" successfully.\n`);
78
+ } catch (err) {
79
+ console.error(`Error: ${err.message}`);
80
+ process.exit(1);
81
+ }
82
+ return true;
83
+ }
84
+
85
+ if (cliArgs[0] === "relink") {
86
+ const dirIdx = cliArgs.indexOf("--dir");
87
+ const dir = dirIdx >= 0 && cliArgs[dirIdx + 1] ? cliArgs[dirIdx + 1] : process.cwd();
88
+ const remIdx = cliArgs.indexOf("--remote");
89
+ const remote = remIdx >= 0 && cliArgs[remIdx + 1] ? cliArgs[remIdx + 1] : null;
90
+
91
+ if (!remote) {
92
+ console.error("Error: --remote parameter is required for relink.");
93
+ process.exit(1);
94
+ }
95
+
96
+ try {
97
+ const { getDatabase } = await import("../db/database.js");
98
+ const { resolveProjectIdentity, upsertIdentity, removeIdentity, normalizeRemoteUrl } = await import("../identity.js");
99
+ const db = await getDatabase();
100
+
101
+ const sourceIdentity = await resolveProjectIdentity(dir);
102
+ if (!sourceIdentity) {
103
+ console.error("Error: Source project identity not detected.");
104
+ process.exit(1);
105
+ }
106
+
107
+ const targetKey = `git:${normalizeRemoteUrl(remote)}`;
108
+ const sourceKey = sourceIdentity.key;
109
+
110
+ if (sourceKey === targetKey) {
111
+ console.log("Source and target identities are already identical.");
112
+ return true;
113
+ }
114
+
115
+ const sourceFacts = await readMemory(sourceKey);
116
+ const targetFacts = await readMemory(targetKey);
117
+ const seen = new Set(targetFacts.map((e) => factBody(e).toLowerCase().trim()));
118
+
119
+ let mergedCount = 0;
120
+ for (const f of sourceFacts) {
121
+ const body = factBody(f).toLowerCase().trim();
122
+ if (!seen.has(body)) {
123
+ seen.add(body);
124
+ targetFacts.push(f);
125
+ mergedCount++;
126
+ }
127
+ }
128
+
129
+ await writeMemory(targetKey, targetFacts);
130
+ await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
131
+ await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
132
+ await removeIdentity(db, sourceKey);
133
+
134
+ try {
135
+ const sourceFp = storeFilePath(sourceKey);
136
+ const { existsSync } = await import("node:fs");
137
+ if (existsSync(sourceFp)) {
138
+ const { unlink } = await import("fs/promises");
139
+ await unlink(sourceFp);
140
+ }
141
+ } catch (e) {}
142
+
143
+ console.log(`\n [OK] Relinked and merged ${mergedCount} facts successfully!\n`);
144
+ } catch (err) {
145
+ console.error(`Error: ${err.message}`);
146
+ process.exit(1);
147
+ }
148
+ return true;
149
+ }
150
+
151
+ if (cliArgs[0] === "identity") {
152
+ const dirIdx = cliArgs.indexOf("--dir");
153
+ const dir = dirIdx >= 0 && cliArgs[dirIdx + 1] ? cliArgs[dirIdx + 1] : process.cwd();
154
+
155
+ try {
156
+ const { resolveProjectIdentity } = await import("../identity.js");
157
+ const identity = await resolveProjectIdentity(dir);
158
+ console.log(`\n PROJECT IDENTITY`);
159
+ if (identity) {
160
+ console.log(` - Key: ${identity.key}`);
161
+ console.log(` - Name: ${identity.name}`);
162
+ console.log(` - Primary Remote: ${identity.primaryRemote || "none"}`);
163
+ console.log(` - Toplevel Directory: ${identity.toplevel}`);
164
+ } else {
165
+ console.log(" No Git repository detected.");
166
+ }
167
+ } catch (err) {
168
+ console.error(`Error: ${err.message}`);
169
+ process.exit(1);
170
+ }
171
+ return true;
172
+ }
173
+
174
+ if (cliArgs[0] === "migrate_titles") {
175
+ const keyIdx = cliArgs.indexOf("--key");
176
+ const key = keyIdx >= 0 && cliArgs[keyIdx + 1] ? cliArgs[keyIdx + 1] : null;
177
+
178
+ try {
179
+ const targets = [];
180
+ if (key) {
181
+ targets.push(key);
182
+ } else {
183
+ const gitKey = await projectKey(process.cwd(), null);
184
+ if (gitKey) targets.push(gitKey);
185
+ targets.push(GLOBAL_KEY);
186
+ const stores = await listProjectStores();
187
+ for (const s of stores) {
188
+ if (!targets.includes(s.key)) targets.push(s.key);
189
+ }
190
+ }
191
+
192
+ let total = 0;
193
+ for (const k of targets) {
194
+ const res = await migrateStoreTitles(k);
195
+ if (res.ok) {
196
+ total += res.changed;
197
+ console.log(` [OK] ${k}: ${res.changed} fact(s) titled`);
198
+ } else {
199
+ console.log(` [SKIP] ${k}: ${res.reason}`);
200
+ }
201
+ }
202
+ console.log(`\n Done. ${total} fact(s) updated across ${targets.length} store(s).\n`);
203
+ } catch (err) {
204
+ console.error(`Error: ${err.message}`);
205
+ process.exit(1);
206
+ }
207
+ return true;
208
+ }
209
+
210
+ if (cliArgs.includes("--enable-prompt") || cliArgs.includes("enable-prompt")) {
211
+ const { enableGlobalPrompt } = await import("../prompt_manager.js");
212
+ const results = await enableGlobalPrompt();
213
+ console.log("\n [OK] Global prompt enabled across client configurations:\n");
214
+ results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
215
+ console.log("");
216
+ return true;
217
+ }
218
+
219
+ if (cliArgs.includes("--disable-prompt") || cliArgs.includes("disable-prompt")) {
220
+ const { disableGlobalPrompt } = await import("../prompt_manager.js");
221
+ const results = await disableGlobalPrompt();
222
+ console.log("\n [OK] Global prompt disabled across client configurations:\n");
223
+ results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
224
+ console.log("");
225
+ return true;
226
+ }
227
+
228
+ if (cliArgs.includes("login")) {
229
+ console.log("\n [CLOUD] Starting Turso cloud authorization...");
230
+ const loginIdx = cliArgs.indexOf("login");
231
+ const loginArgs = cliArgs.slice(loginIdx + 1);
232
+ const flagValue = (name) => {
233
+ const i = loginArgs.indexOf(name);
234
+ return i >= 0 && loginArgs[i + 1] ? loginArgs[i + 1] : null;
235
+ };
236
+ const { loginToCloud, loginWithApiToken, loginWithDatabaseToken, loginFromEnv } = await import("../admin/auth.js");
237
+ try {
238
+ let secrets;
239
+ if (loginArgs.includes("--from-env")) {
240
+ const res = await loginFromEnv({ persist: false });
241
+ if (!res.ok) throw new Error(res.reason);
242
+ secrets = res.secrets;
243
+ } else if (loginArgs.includes("--db-url") && loginArgs.includes("--db-token")) {
244
+ secrets = await loginWithDatabaseToken({
245
+ dbUrl: flagValue("--db-url"),
246
+ token: flagValue("--db-token"),
247
+ username: flagValue("--username") || "",
248
+ org: flagValue("--org") || "",
249
+ db: flagValue("--database") || "",
250
+ validate: !loginArgs.includes("--no-validate"),
251
+ });
252
+ } else if (loginArgs.includes("--token") || loginArgs.includes("--api-token") || loginArgs.includes("--api-key")) {
253
+ const token = flagValue("--token") || flagValue("--api-token") || flagValue("--api-key");
254
+ if (!token) throw new Error("Missing token value. Usage: memory_plugin login --api-token <TOKEN> [--org <ORG>] [--database <DB>]");
255
+ secrets = await loginWithApiToken({
256
+ token,
257
+ org: flagValue("--org") || null,
258
+ databaseName: flagValue("--database") || null,
259
+ });
260
+ } else {
261
+ secrets = await loginToCloud();
262
+ }
263
+ console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
264
+ } catch (e) {
265
+ console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
266
+ process.exit(1);
267
+ }
268
+ return true;
269
+ }
270
+
271
+ if (cliArgs.includes("logout")) {
272
+ const { logoutFromCloud, clearApiKey } = await import("../admin/auth.js");
273
+ if (cliArgs.includes("--api-key")) {
274
+ const res = clearApiKey();
275
+ if (res.removed) {
276
+ console.log(
277
+ res.keptDbSession
278
+ ? " \x1b[32m[OK] API token removed. The resolved database session is kept and stays authorized.\x1b[0m\n"
279
+ : " \x1b[32m[OK] API token removed. Encrypted secrets purged.\x1b[0m\n"
280
+ );
281
+ } else {
282
+ console.log(" [*] No stored API token to remove.\x1b[0m\n");
283
+ }
284
+ return true;
285
+ }
286
+ console.log("\n [CLOUD] Signing out of the cloud...");
287
+ const deleted = logoutFromCloud();
288
+ if (deleted) {
289
+ console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
290
+ } else {
291
+ console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
292
+ }
293
+ return true;
294
+ }
295
+
296
+ if (cliArgs.includes("auth-status") || cliArgs.includes("auth_status") || cliArgs.includes("auth")) {
297
+ const { getAuthStatus } = await import("../admin/auth.js");
298
+ const st = getAuthStatus();
299
+ console.log("\n [CLOUD] Authentication status:");
300
+ console.log(` Source: ${st.source}`);
301
+ console.log(` Authorized: ${st.authorized ? "YES" : "no"}`);
302
+ console.log(` API Key: ${st.hasApiKey ? "SET" : "not set"}`);
303
+ console.log(` Endpoint: ${st.dbUrl || "(none)"}`);
304
+ console.log(` Username: ${st.username || "(unknown)"}`);
305
+ console.log(` Organization: ${st.org || "(unknown)"}`);
306
+ console.log(` Database: ${st.database || "(unknown)"}`);
307
+ console.log(` Mode: ${st.mode}`);
308
+ console.log("");
309
+ return true;
310
+ }
311
+
312
+ return false;
313
+ }
@@ -0,0 +1,138 @@
1
+ import { updateConfig } from "../../config/config_manager.js";
2
+ import { selectSimpleMenu, promptText, waitForEnter } from "../ui.js";
3
+
4
+ export async function handleCloudAction(value, config) {
5
+ switch (value) {
6
+ case "cloud_login": {
7
+ console.clear();
8
+ console.log("\n [CLOUD] Turso cloud authorization\n");
9
+ const methodItems = [
10
+ { label: "Browser OAuth (GUI)", value: "browser", info: "Opens the system browser for the loopback OAuth flow (requires a desktop session)" },
11
+ { label: "Account API Token", value: "api_token", info: "Paste a Turso account API token — works headless (Docker, Google Jules, VPS/VDS)" },
12
+ { label: "Database URL + Token", value: "db_token", info: "Paste a libsql:// endpoint and its database auth token — no Platform API needed" },
13
+ { label: "Import From Environment", value: "env", info: "Pick up TURSO_DB_URL / TURSO_DB_TOKEN / TURSO_API_TOKEN from env vars or MEMORY_DIR/.env" },
14
+ { label: "< Cancel", value: "cancel", info: "Return to the main menu" },
15
+ ];
16
+ const methodRes = await selectSimpleMenu({
17
+ title: "CHOOSE LOGIN METHOD",
18
+ subtitle: "Browser login needs a GUI. Token / env methods work in Docker, Google Jules and on VPS/VDS.",
19
+ items: methodItems,
20
+ });
21
+ if (methodRes.action !== "select" || methodRes.value === "cancel") break;
22
+
23
+ const { loginToCloud, loginWithApiToken, loginWithDatabaseToken, loginFromEnv } = await import("../../admin/auth.js");
24
+ try {
25
+ let secrets;
26
+ if (methodRes.value === "browser") {
27
+ secrets = await loginToCloud();
28
+ } else if (methodRes.value === "api_token") {
29
+ const token = await promptText("Paste your Turso account API token\n (create one at https://console.turso.tech or via `turso auth api-tokens create`)");
30
+ if (!token) throw new Error("Empty API token.");
31
+ secrets = await loginWithApiToken({ token });
32
+ } else if (methodRes.value === "db_token") {
33
+ const dbUrl = await promptText("Paste your database URL (libsql://<database>-<org>.turso.io)");
34
+ const token = await promptText("Paste your database auth token");
35
+ if (!dbUrl || !token) throw new Error("Empty URL or token.");
36
+ secrets = await loginWithDatabaseToken({ dbUrl, token, validate: false });
37
+ } else if (methodRes.value === "env") {
38
+ const res = await loginFromEnv({ persist: true });
39
+ if (!res.ok) throw new Error(res.reason);
40
+ secrets = res.secrets;
41
+ }
42
+ console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
43
+ } catch (e) {
44
+ console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
45
+ }
46
+ await waitForEnter();
47
+ break;
48
+ }
49
+ case "cloud_logout": {
50
+ console.clear();
51
+ console.log("\n [CLOUD] Signing out of the cloud...");
52
+ const { logoutFromCloud } = await import("../../admin/auth.js");
53
+ const deleted = logoutFromCloud();
54
+ if (deleted) {
55
+ console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
56
+ } else {
57
+ console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
58
+ }
59
+ await waitForEnter();
60
+ break;
61
+ }
62
+ case "cloud_api_set": {
63
+ console.clear();
64
+ console.log("\n [API KEY] Set / replace the Turso account API token\n");
65
+ const { setApiKey } = await import("../../admin/auth.js");
66
+ try {
67
+ const token = await promptText(
68
+ "Paste your Turso account API token\n (create one at https://console.turso.tech or via `turso auth api-tokens create`)"
69
+ );
70
+ if (!token) throw new Error("Empty API token.");
71
+ const res = await setApiKey(token);
72
+ console.log(
73
+ `\n \x1b[32m[OK] API token stored. Authorized as "${res.secrets.username}" — endpoint: ${res.secrets.dbUrl}\x1b[0m\n`
74
+ );
75
+ } catch (e) {
76
+ console.error(`\n \x1b[31m[ERROR] Failed to set API key: ${e.message}\x1b[0m\n`);
77
+ }
78
+ await waitForEnter();
79
+ break;
80
+ }
81
+ case "cloud_api_clear": {
82
+ console.clear();
83
+ console.log("\n [API KEY] Removing the stored account API token...");
84
+ const { clearApiKey } = await import("../../admin/auth.js");
85
+ const res = clearApiKey();
86
+ if (res.removed) {
87
+ console.log(
88
+ res.keptDbSession
89
+ ? " \x1b[32m[OK] API token removed. The resolved database session is kept and stays authorized.\x1b[0m\n"
90
+ : " \x1b[32m[OK] API token removed. Encrypted secrets purged.\x1b[0m\n"
91
+ );
92
+ } else {
93
+ console.log(" [*] No stored API token to remove.\x1b[0m\n");
94
+ }
95
+ await waitForEnter();
96
+ break;
97
+ }
98
+ case "cloud_mode": {
99
+ const modeItems = [
100
+ { label: "only-local (Local only)", value: "only-local", info: "Fully private, offline-first mode (everything stored on disk)" },
101
+ { label: "only-cloud (Cloud only)", value: "only-cloud", info: "Fully serverless cloud mode with no local caching" },
102
+ { label: "hybrid-sync (Local with background sync)", value: "hybrid-sync", info: "Instant local operations with a background sync daemon" },
103
+ ];
104
+ const initialIdx = Math.max(0, modeItems.findIndex((i) => i.value === config.mode));
105
+ const subRes = await selectSimpleMenu({
106
+ title: "CHOOSE OPERATIONAL MODE",
107
+ subtitle: "Configure database storage and cloud sync behavior",
108
+ items: modeItems,
109
+ initialIndex: initialIdx,
110
+ });
111
+
112
+ if (subRes.action === "select") {
113
+ updateConfig({ mode: subRes.value });
114
+ }
115
+ break;
116
+ }
117
+ case "conflict_strategy": {
118
+ const strategyItems = [
119
+ { label: "merge (Union local + cloud)", value: "merge", info: "Facts from both sides are merged and deduplicated — no data loss (recommended)" },
120
+ { label: "cloud-wins (Cloud overwrites local)", value: "cloud-wins", info: "On conflict, the cloud copy replaces the local store" },
121
+ { label: "local-wins (Local overwrites cloud)", value: "local-wins", info: "On conflict, the local copy replaces the cloud store" },
122
+ ];
123
+ const initialIdx = Math.max(0, strategyItems.findIndex((i) => i.value === (config.conflictStrategy || "merge")));
124
+ const subRes = await selectSimpleMenu({
125
+ title: "CHOOSE CONFLICT STRATEGY",
126
+ subtitle: "How hybrid-sync resolves differing local vs cloud stores",
127
+ items: strategyItems,
128
+ initialIndex: initialIdx,
129
+ });
130
+
131
+ if (subRes.action === "select") {
132
+ updateConfig({ conflictStrategy: subRes.value });
133
+ console.log(`\n [OK] Conflict strategy set to: ${subRes.value}`);
134
+ }
135
+ break;
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,107 @@
1
+ import { getConfig, updateConfig, resetConfig } from "../../config/config_manager.js";
2
+ import { hybridQuery } from "../../retrieval/retriever.js";
3
+ import {
4
+ selectSimpleMenu,
5
+ readTextInput,
6
+ waitForEnter,
7
+ } from "../ui.js";
8
+
9
+ export async function handleDiagnosticsAction(value, config, stats) {
10
+ switch (value) {
11
+ case "test": {
12
+ const queryRes = await readTextInput("Enter Test Verification Query", "sqlite compact database");
13
+ if (queryRes.action === "submit" && queryRes.value) {
14
+ console.clear();
15
+ console.log(`\n \x1b[1m\x1b[37mSEARCH QUERY EXECUTION\x1b[0m`);
16
+ console.log(`\n [SEARCH] Executing query: "\x1b[36m${queryRes.value}\x1b[0m"...\n`);
17
+ try {
18
+ const results = await hybridQuery({ query: queryRes.value, limit: 3 });
19
+ if (!results || results.length === 0) {
20
+ console.log(" [*] No matching results found in knowledge base.");
21
+ } else {
22
+ results.forEach((r, i) => {
23
+ console.log(`\n \x1b[1m[Hit #${i + 1}] ${r.doc_title || "Doc"} > ${r.breadcrumbs || ""}\x1b[0m`);
24
+ console.log(` Score: \x1b[33m${r.score}\x1b[0m (RSF: ${r.rsf_score}, RRF: ${r.rrf_score}, CosSim: ${r.cosine_sim})`);
25
+ console.log(` Snippet: \x1b[90m${r.snippet ? r.snippet.substring(0, 100).replace(/\n/g, " ") : ""}...\x1b[0m`);
26
+ });
27
+ }
28
+ } catch (err) {
29
+ console.error(" [ERROR] Query execution failed:", err.message);
30
+ }
31
+ await waitForEnter();
32
+ }
33
+ break;
34
+ }
35
+
36
+ case "graph_test": {
37
+ console.clear();
38
+ console.log(`\n \x1b[1m\x1b[37mGRAPH & NOTEBOOK LINKING VERIFICATION\x1b[0m\n`);
39
+
40
+ const sampleDoc = `# Ода о единороге (Секретный проект Unicorn)
41
+
42
+ ## Раздел 1: Введение
43
+ Разработка нового высоконагруженного сервиса Unicorn.
44
+
45
+ ## Раздел 2: Стандарты
46
+ Строка 7: Бэкенд пишется исключительно на Go.
47
+ Строка 8: Хранилище транзакций — PostgreSQL 16.
48
+ `;
49
+
50
+ const { ingestDocument } = await import("../../ingest/pipeline.js");
51
+ const { linkFactToDocument, getLinksForFact } = await import("../../graph/knowledge_linker.js");
52
+ const { readMemoryRaw, writeMemory, scopeKey } = await import("../../memory.js");
53
+
54
+ console.log(" 1. Ingesting test document 'Ода о единороге'...");
55
+ const ingRes = await ingestDocument({
56
+ content: sampleDoc,
57
+ type: "text",
58
+ title: "Ода о единороге",
59
+ path: "virtual://oda_unicorna.md",
60
+ generateEmbeddings: false,
61
+ });
62
+ console.log(` [OK] Document ingested. Doc ID: ${ingRes.docId}`);
63
+
64
+ console.log("\n 2. Saving Notebook fact & linking to lines L7-L8...");
65
+ const factText = "Project Unicorn backend services must use Go with PostgreSQL 16";
66
+ const factKey = scopeKey("project", "cli_test_repo", null);
67
+
68
+ const entries = await readMemoryRaw(factKey);
69
+ entries.push(`[2026-07-30] ${factText}`);
70
+ await writeMemory(factKey, entries);
71
+
72
+ const linkRes = linkFactToDocument({
73
+ factKey,
74
+ factText,
75
+ docId: ingRes.docId,
76
+ startLine: 7,
77
+ endLine: 8,
78
+ relationType: "RULES_FOR",
79
+ });
80
+ console.log(` [OK] Graph Edge created. Link ID: ${linkRes.linkId} -> L7-L8`);
81
+
82
+ console.log("\n 3. Recalling memory (Verifying Graph Document Tag)...");
83
+ const rawFacts = await readMemoryRaw(factKey);
84
+ rawFacts.forEach((f, i) => {
85
+ const links = getLinksForFact(factKey, f);
86
+ let lStr = ` ${i + 1}. ${f}`;
87
+ if (links && links.length > 0) {
88
+ const docStr = links.map(l => `${l.doc_title || l.doc_path}:L${l.start_line}-${l.end_line}`).join(", ");
89
+ lStr += ` \x1b[36m🔗 [Linked Docs: ${docStr}]\x1b[0m`;
90
+ }
91
+ console.log(lStr);
92
+ });
93
+
94
+ console.log("\n \x1b[32m[OK] AGENT-DRIVEN GRAPH LINKING VERIFIED SUCCESSFULLY!\x1b[0m\n");
95
+ await waitForEnter();
96
+ break;
97
+ }
98
+
99
+ case "reset": {
100
+ resetConfig();
101
+ console.clear();
102
+ console.log("\n [OK] Configuration reset to factory defaults (RSF 50/50, e5-small, no reranker).\n");
103
+ await waitForEnter();
104
+ break;
105
+ }
106
+ }
107
+ }