@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.
- package/README.md +352 -334
- package/mcp-server/admin/auth.js +293 -42
- 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 -1945
- package/mcp-server/config/auth_store.js +178 -19
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +18 -3
- 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/setup.js +41 -0
- 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
|
@@ -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
|
+
}
|