@lotargo/memory_plugin 1.3.1 → 1.4.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.
@@ -0,0 +1,90 @@
1
+ import http from "node:http";
2
+ import { saveSecrets, deleteSecrets } from "../config/auth_store.js";
3
+ import { updateConfig } from "../config/config_manager.js";
4
+
5
+ // Starts a temporary loopback HTTP server to listen for callback from browser login
6
+ export function startAuthLoopbackServer(port = 48900) {
7
+ return new Promise((resolve, reject) => {
8
+ const server = http.createServer((req, res) => {
9
+ const url = new URL(req.url, `http://${req.headers.host}`);
10
+
11
+ if (url.pathname === "/callback") {
12
+ const token = url.searchParams.get("token");
13
+ const dbUrl = url.searchParams.get("db_url") || url.searchParams.get("dbUrl");
14
+
15
+ if (!token || !dbUrl) {
16
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
17
+ res.end("Ошибка: Токен (token) и URL базы данных (db_url) обязательны!");
18
+ server.close();
19
+ reject(new Error("Missing token or db_url in auth callback"));
20
+ return;
21
+ }
22
+
23
+ // Save secrets securely and configure Turso Url
24
+ saveSecrets({ token, dbUrl });
25
+ updateConfig({ tursoUrl: dbUrl });
26
+
27
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
28
+ res.end(`
29
+ <html>
30
+ <body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
31
+ <h2 style="color: #2e7d32;">Авторизация успешна!</h2>
32
+ <p>Плагин успешно принял учетные данные. Вы можете закрыть эту вкладку.</p>
33
+ </body>
34
+ </html>
35
+ `);
36
+
37
+ server.close(() => {
38
+ resolve({ token, dbUrl });
39
+ });
40
+ } else {
41
+ res.writeHead(404, { "Content-Type": "text/plain" });
42
+ res.end("Not Found");
43
+ }
44
+ });
45
+
46
+ server.on("error", (err) => {
47
+ reject(err);
48
+ });
49
+
50
+ server.listen(port, "127.0.0.1", () => {
51
+ console.log(`\n [*] Ожидание авторизации на локальном порту http://localhost:${port}/callback...`);
52
+ });
53
+ });
54
+ }
55
+
56
+ // Perform Cloud login flow
57
+ export async function loginToCloud({ customPort = 48900, simulated = false, simulatedParams = null } = {}) {
58
+ // Opening the browser, etc. (we skip browser auto-open in automated test runs/simulations)
59
+ const loginUrl = `https://auth.lotargo.com/login?device_id=memory_plugin&port=${customPort}`;
60
+ console.log(`\n [CLOUD] Пожалуйста, откройте системный браузер для авторизации:`);
61
+ console.log(` \x1b[36m${loginUrl}\x1b[0m\n`);
62
+
63
+ if (simulated && simulatedParams) {
64
+ // Send local HTTP request to simulate loopback
65
+ return new Promise((resolve, reject) => {
66
+ const serverPromise = startAuthLoopbackServer(customPort);
67
+
68
+ const req = http.request(
69
+ `http://127.0.0.1:${customPort}/callback?token=${encodeURIComponent(simulatedParams.token)}&db_url=${encodeURIComponent(simulatedParams.dbUrl)}`,
70
+ { method: "GET" },
71
+ (res) => {
72
+ res.resume();
73
+ }
74
+ );
75
+ req.on("error", (e) => reject(e));
76
+ req.end();
77
+
78
+ resolve(serverPromise);
79
+ });
80
+ }
81
+
82
+ return startAuthLoopbackServer(customPort);
83
+ }
84
+
85
+ // Logout and reset configurations
86
+ export function logoutFromCloud() {
87
+ const deleted = deleteSecrets();
88
+ updateConfig({ tursoUrl: "", mode: "only-local" });
89
+ return deleted;
90
+ }
@@ -36,13 +36,13 @@ export function listAvailableSnapshots() {
36
36
  }
37
37
 
38
38
  export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, outputPath = null } = {}) {
39
- const db = customDb || getDatabase();
39
+ const db = customDb || await getDatabase();
40
40
 
41
- const documents = db.prepare("SELECT * FROM documents").all();
42
- const sections = db.prepare("SELECT * FROM sections").all();
43
- const mediumChunks = db.prepare("SELECT * FROM medium_chunks").all();
44
- const rawMicroChunks = db.prepare("SELECT * FROM micro_chunks").all();
45
- const graphEdges = db.prepare("SELECT * FROM graph_edges").all();
41
+ const documents = await db.prepare("SELECT * FROM documents").all();
42
+ const sections = await db.prepare("SELECT * FROM sections").all();
43
+ const mediumChunks = await db.prepare("SELECT * FROM medium_chunks").all();
44
+ const rawMicroChunks = await db.prepare("SELECT * FROM micro_chunks").all();
45
+ const graphEdges = await db.prepare("SELECT * FROM graph_edges").all();
46
46
 
47
47
  const microChunks = rawMicroChunks.map((mc) => {
48
48
  let vecBase64 = "";
@@ -92,7 +92,7 @@ export async function exportSnapshot({ customDb = null, customBlobDir = BLOBS_DI
92
92
  }
93
93
 
94
94
  export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DIR, snapshotPathOrData } = {}) {
95
- const db = customDb || getDatabase();
95
+ const db = customDb || await getDatabase();
96
96
  let snapshot;
97
97
 
98
98
  if (typeof snapshotPathOrData === "string") {
@@ -181,11 +181,11 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
181
181
  ON CONFLICT(source_id, target_id, relation_type) DO NOTHING
182
182
  `);
183
183
 
184
- db.exec("BEGIN IMMEDIATE;");
184
+ await db.exec("BEGIN IMMEDIATE;");
185
185
  try {
186
186
  if (Array.isArray(snapshot.documents)) {
187
187
  for (const d of snapshot.documents) {
188
- insertDoc.run(
188
+ await insertDoc.run(
189
189
  d.id,
190
190
  d.path,
191
191
  d.blob_hash,
@@ -201,13 +201,13 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
201
201
 
202
202
  if (Array.isArray(snapshot.sections)) {
203
203
  for (const s of snapshot.sections) {
204
- insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
204
+ await insertSection.run(s.id, s.doc_id, s.heading, s.breadcrumbs, s.content, s.token_count);
205
205
  }
206
206
  }
207
207
 
208
208
  if (Array.isArray(snapshot.medium_chunks)) {
209
209
  for (const m of snapshot.medium_chunks) {
210
- insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
210
+ await insertMedium.run(m.id, m.section_id, m.doc_id, m.content, m.block_type, m.token_count, m.created_at || Date.now());
211
211
  }
212
212
  }
213
213
 
@@ -217,23 +217,23 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
217
217
  if (mc.vector) {
218
218
  vecBuf = Buffer.from(mc.vector, "base64");
219
219
  }
220
- insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
220
+ await insertChunk.run(mc.id, mc.section_id, mc.doc_id, mc.content, vecBuf, mc.token_count, mc.medium_id || null);
221
221
 
222
222
  try {
223
- deleteFts.run(mc.id);
223
+ await deleteFts.run(mc.id);
224
224
  } catch {}
225
- insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
225
+ await insertFts.run(mc.id, mc.content, mc.breadcrumbs || "");
226
226
  }
227
227
  }
228
228
 
229
229
  if (Array.isArray(snapshot.graph_edges)) {
230
230
  for (const e of snapshot.graph_edges) {
231
- insertEdge.run(e.source_id, e.target_id, e.relation_type);
231
+ await insertEdge.run(e.source_id, e.target_id, e.relation_type);
232
232
  }
233
233
  }
234
- db.exec("COMMIT;");
234
+ await db.exec("COMMIT;");
235
235
  } catch (err) {
236
- db.exec("ROLLBACK;");
236
+ await db.exec("ROLLBACK;");
237
237
  throw err;
238
238
  }
239
239
 
@@ -247,30 +247,32 @@ export async function importSnapshot({ customDb = null, customBlobDir = BLOBS_DI
247
247
  };
248
248
  }
249
249
 
250
- export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
251
- const db = customDb || getDatabase();
250
+ export async function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR } = {}) {
251
+ const db = customDb || await getDatabase();
252
252
 
253
253
  let docCount = 0;
254
254
  let chunkCount = 0;
255
255
  let blobCount = 0;
256
256
 
257
257
  try {
258
- docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
259
- chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
258
+ const docRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
259
+ docCount = docRow ? docRow.cnt : 0;
260
+ const chunkRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
261
+ chunkCount = chunkRow ? chunkRow.cnt : 0;
260
262
  } catch {}
261
263
 
262
- db.exec("BEGIN IMMEDIATE;");
264
+ await db.exec("BEGIN IMMEDIATE;");
263
265
  try {
264
- try { db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
265
- try { db.exec("DELETE FROM micro_chunks;"); } catch {}
266
- try { db.exec("DELETE FROM medium_chunks;"); } catch {}
267
- try { db.exec("DELETE FROM sections;"); } catch {}
268
- try { db.exec("DELETE FROM graph_edges;"); } catch {}
269
- try { db.exec("DELETE FROM knowledge_links;"); } catch {}
270
- try { db.exec("DELETE FROM documents;"); } catch {}
271
- db.exec("COMMIT;");
266
+ try { await db.exec("DELETE FROM micro_chunks_fts;"); } catch {}
267
+ try { await db.exec("DELETE FROM micro_chunks;"); } catch {}
268
+ try { await db.exec("DELETE FROM medium_chunks;"); } catch {}
269
+ try { await db.exec("DELETE FROM sections;"); } catch {}
270
+ try { await db.exec("DELETE FROM graph_edges;"); } catch {}
271
+ try { await db.exec("DELETE FROM knowledge_links;"); } catch {}
272
+ try { await db.exec("DELETE FROM documents;"); } catch {}
273
+ await db.exec("COMMIT;");
272
274
  } catch (err) {
273
- db.exec("ROLLBACK;");
275
+ await db.exec("ROLLBACK;");
274
276
  throw err;
275
277
  }
276
278
 
@@ -292,7 +294,7 @@ export function hardResetDatabase({ customDb = null, customBlobDir = BLOBS_DIR }
292
294
  }
293
295
 
294
296
  try {
295
- db.exec("VACUUM;");
297
+ await db.exec("VACUUM;");
296
298
  } catch {}
297
299
 
298
300
  return {
package/mcp-server/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import readline from "readline";
3
3
  import { join } from "node:path";
4
4
  import { getConfig, updateConfig, resetConfig } from "./config/config_manager.js";
@@ -91,9 +91,11 @@ async function getQuickStats() {
91
91
  let docCount = 0;
92
92
  let chunkCount = 0;
93
93
  try {
94
- const db = getDatabase();
95
- docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
96
- chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
94
+ const db = await getDatabase();
95
+ const docRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
96
+ docCount = docRow ? docRow.cnt : 0;
97
+ const chunkRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
98
+ chunkCount = chunkRow ? chunkRow.cnt : 0;
97
99
  } catch (e) {}
98
100
 
99
101
  let factCount = 0;
@@ -658,6 +660,32 @@ export async function runCli() {
658
660
  return;
659
661
  }
660
662
 
663
+ const cliArgs = process.argv.slice(2);
664
+ if (cliArgs.includes("login")) {
665
+ console.log("\n [CLOUD] Запуск процесса авторизации в облаке Turso...");
666
+ const { loginToCloud } = await import("./admin/auth.js");
667
+ try {
668
+ const secrets = await loginToCloud();
669
+ console.log(`\n \x1b[32m[OK] Успешный вход в облако! Подключен к endpoint: ${secrets.dbUrl}\x1b[0m\n`);
670
+ } catch (e) {
671
+ console.error(`\n \x1b[31m[ERROR] Ошибка авторизации: ${e.message}\x1b[0m\n`);
672
+ process.exit(1);
673
+ }
674
+ return;
675
+ }
676
+
677
+ if (cliArgs.includes("logout")) {
678
+ console.log("\n [CLOUD] Выход из облака...");
679
+ const { logoutFromCloud } = await import("./admin/auth.js");
680
+ const deleted = logoutFromCloud();
681
+ if (deleted) {
682
+ console.log(" \x1b[32m[OK] Вы вышли из облака. Секретные ключи удалены. Режим изменен на only-local.\x1b[0m\n");
683
+ } else {
684
+ console.log(" [*] Режим изменен на only-local. Сессионных токенов не было обнаружено.\x1b[0m\n");
685
+ }
686
+ return;
687
+ }
688
+
661
689
  let running = true;
662
690
  let selectedIndex = 0;
663
691
 
@@ -760,6 +788,27 @@ export async function runCli() {
760
788
  },
761
789
  ],
762
790
  },
791
+ {
792
+ title: "Cloud Synchronization & Turso",
793
+ items: [
794
+ {
795
+ label: "[CLOUD] Login to Turso Cloud",
796
+ value: "cloud_login",
797
+ info: "Perform secure OAuth/Device login flow with loopback listener and local AES-256 key encryption",
798
+ },
799
+ {
800
+ label: "[CLOUD] Logout",
801
+ value: "cloud_logout",
802
+ info: "Sign out, purge encrypted secrets, and revert mode to only-local",
803
+ },
804
+ {
805
+ label: "Operational Mode",
806
+ badge: config.mode.toUpperCase(),
807
+ value: "cloud_mode",
808
+ info: "Choose Operational Mode: only-local | only-cloud | hybrid-sync",
809
+ },
810
+ ],
811
+ },
763
812
  {
764
813
  title: "Global Prompt & Integration Management",
765
814
  items: [
@@ -1103,10 +1152,11 @@ export async function runCli() {
1103
1152
  await writeMemory(key, updated);
1104
1153
  let links = 0;
1105
1154
  try {
1106
- const db = getDatabase();
1107
- links = db
1155
+ const db = await getDatabase();
1156
+ const runRes = await db
1108
1157
  .prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
1109
- .run(newText, key, factText(selectedEntry)).changes;
1158
+ .run(newText, key, factText(selectedEntry));
1159
+ links = runRes ? runRes.changes : 0;
1110
1160
  } catch (e) {}
1111
1161
  console.clear();
1112
1162
  console.log(`\n [OK] Fact updated successfully${links ? `, ${links} doc link(s) updated` : ""}.\n`);
@@ -1234,8 +1284,8 @@ export async function runCli() {
1234
1284
  case "rag_docs": {
1235
1285
  let docRunning = true;
1236
1286
  while (docRunning) {
1237
- const db = getDatabase();
1238
- const docs = db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
1287
+ const db = await getDatabase();
1288
+ const docs = await db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
1239
1289
 
1240
1290
  if (!docs || docs.length === 0) {
1241
1291
  console.clear();
@@ -1295,9 +1345,11 @@ export async function runCli() {
1295
1345
  });
1296
1346
 
1297
1347
  if (actionRes.action === "select" && actionRes.value === "info") {
1298
- const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id).cnt;
1299
- const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id).cnt;
1300
- const sampleSections = db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
1348
+ const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id);
1349
+ const secCount = secCountRow ? secCountRow.cnt : 0;
1350
+ const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id);
1351
+ const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
1352
+ const sampleSections = await db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
1301
1353
 
1302
1354
  console.clear();
1303
1355
  const line = "─".repeat(PANEL_WIDTH - 2);
@@ -1776,6 +1828,51 @@ export async function runCli() {
1776
1828
  }
1777
1829
  break;
1778
1830
  }
1831
+ case "cloud_login": {
1832
+ console.clear();
1833
+ console.log("\n [CLOUD] Запуск процесса авторизации в облаке Turso...");
1834
+ const { loginToCloud } = await import("./admin/auth.js");
1835
+ try {
1836
+ const secrets = await loginToCloud();
1837
+ console.log(`\n \x1b[32m[OK] Успешный вход в облако! Подключен к endpoint: ${secrets.dbUrl}\x1b[0m\n`);
1838
+ } catch (e) {
1839
+ console.error(`\n \x1b[31m[ERROR] Ошибка авторизации: ${e.message}\x1b[0m\n`);
1840
+ }
1841
+ await waitForEnter();
1842
+ break;
1843
+ }
1844
+ case "cloud_logout": {
1845
+ console.clear();
1846
+ console.log("\n [CLOUD] Выход из облака...");
1847
+ const { logoutFromCloud } = await import("./admin/auth.js");
1848
+ const deleted = logoutFromCloud();
1849
+ if (deleted) {
1850
+ console.log(" \x1b[32m[OK] Вы вышли из облака. Секретные ключи удалены. Режим изменен на only-local.\x1b[0m\n");
1851
+ } else {
1852
+ console.log(" [*] Режим изменен на only-local. Сессионных токенов не было обнаружено.\x1b[0m\n");
1853
+ }
1854
+ await waitForEnter();
1855
+ break;
1856
+ }
1857
+ case "cloud_mode": {
1858
+ const modeItems = [
1859
+ { label: "only-local (Только локальный)", value: "only-local", info: "Полностью приватный автономный режим (все на диске)" },
1860
+ { label: "only-cloud (Только облачный)", value: "only-cloud", info: "Полностью облачный бессерверный режим без локального кэширования" },
1861
+ { label: "hybrid-sync (Локальный с фоновой синхронизацией)", value: "hybrid-sync", info: "Локальные мгновенные операции с фоновым демоном синхронизации" },
1862
+ ];
1863
+ const initialIdx = Math.max(0, modeItems.findIndex((i) => i.value === config.mode));
1864
+ const subRes = await selectSimpleMenu({
1865
+ title: "CHOOSE OPERATIONAL MODE",
1866
+ subtitle: "Configure database storage and cloud sync behavior",
1867
+ items: modeItems,
1868
+ initialIndex: initialIdx,
1869
+ });
1870
+
1871
+ if (subRes.action === "select") {
1872
+ updateConfig({ mode: subRes.value });
1873
+ }
1874
+ break;
1875
+ }
1779
1876
  case "enable_prompt": {
1780
1877
  const { enableGlobalPrompt } = await import("./prompt_manager.js");
1781
1878
  const results = await enableGlobalPrompt();
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import os from "node:os";
5
+ import { MEMORY_DIR, ensureDirSync } from "../memory.js";
6
+
7
+ const SECRETS_FILE = path.join(MEMORY_DIR, "auth_secrets.enc");
8
+
9
+ // Generate a deterministic hardware + system fingerprint
10
+ function getSystemFingerprint() {
11
+ const parts = [
12
+ os.hostname() || "localhost",
13
+ os.userInfo()?.username || "default_user",
14
+ os.platform() || "unknown",
15
+ os.arch() || "unknown",
16
+ // Fallback if network interfaces list is empty or can't be fetched
17
+ JSON.stringify(os.networkInterfaces() || {}),
18
+ ];
19
+ return parts.join("|");
20
+ }
21
+
22
+ // Derive a 256-bit (32 bytes) key using PBKDF2 with salt derived from fingerprint
23
+ function deriveEncryptionKey() {
24
+ const fingerprint = getSystemFingerprint();
25
+ const salt = crypto.createHash("sha256").update(fingerprint).digest();
26
+ // PBKDF2 with 10,000 iterations to derive a secure 32-byte key
27
+ return crypto.pbkdf2Sync(fingerprint, salt, 10000, 32, "sha256");
28
+ }
29
+
30
+ // Encrypt data using AES-256-GCM
31
+ export function encryptData(plainText) {
32
+ const key = deriveEncryptionKey();
33
+ const iv = crypto.randomBytes(12); // 96-bit IV is standard for GCM
34
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
35
+
36
+ let encrypted = cipher.update(plainText, "utf8", "hex");
37
+ encrypted += cipher.final("hex");
38
+
39
+ const authTag = cipher.getAuthTag().toString("hex");
40
+
41
+ // Format as: iv_hex:auth_tag_hex:encrypted_hex
42
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
43
+ }
44
+
45
+ // Decrypt data using AES-256-GCM
46
+ export function decryptData(encryptedStr) {
47
+ const parts = encryptedStr.split(":");
48
+ if (parts.length !== 3) {
49
+ throw new Error("Invalid encrypted format. Expected iv:tag:ciphertext");
50
+ }
51
+
52
+ const [ivHex, authTagHex, encryptedHex] = parts;
53
+ const key = deriveEncryptionKey();
54
+ const iv = Buffer.from(ivHex, "hex");
55
+ const authTag = Buffer.from(authTagHex, "hex");
56
+
57
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
58
+ decipher.setAuthTag(authTag);
59
+
60
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
61
+ decrypted += decipher.final("utf8");
62
+
63
+ return decrypted;
64
+ }
65
+
66
+ // Save secrets securely
67
+ export function saveSecrets(secrets) {
68
+ ensureDirSync();
69
+ const plainText = JSON.stringify(secrets);
70
+ const encrypted = encryptData(plainText);
71
+ fs.writeFileSync(SECRETS_FILE, encrypted, "utf-8");
72
+ }
73
+
74
+ // Load secrets securely
75
+ export function loadSecrets() {
76
+ if (!fs.existsSync(SECRETS_FILE)) {
77
+ return null;
78
+ }
79
+ try {
80
+ const encrypted = fs.readFileSync(SECRETS_FILE, "utf-8").trim();
81
+ if (!encrypted) return null;
82
+ const decrypted = decryptData(encrypted);
83
+ return JSON.parse(decrypted);
84
+ } catch (err) {
85
+ console.error("Failed to decrypt or load cloud secrets:", err.message);
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // Delete secrets from disk
91
+ export function deleteSecrets() {
92
+ if (fs.existsSync(SECRETS_FILE)) {
93
+ try {
94
+ fs.unlinkSync(SECRETS_FILE);
95
+ return true;
96
+ } catch (err) {
97
+ console.error("Failed to delete secrets file:", err.message);
98
+ }
99
+ }
100
+ return false;
101
+ }
@@ -14,6 +14,9 @@ export const DEFAULT_CONFIG = {
14
14
  gpuAttentionBudget: 2000000, // GPU micro-batch attention budget [1M - 16M] (default 2.0M ~1.5GB VRAM)
15
15
  onnxThreads: 0, // ONNX WASM threads: 0 = auto-detect CPU cores, or 1-16
16
16
  executionDevice: "cpu", // "cpu" | "webgpu"
17
+ mode: "only-local", // "only-local" | "only-cloud" | "hybrid-sync"
18
+ tursoUrl: "", // Connection endpoint URL for Turso DB
19
+ failoverUrl: "", // Failover connection endpoint URL (Fly.io + LiteFS)
17
20
  };
18
21
 
19
22
  let cachedConfig = null;