@lotargo/memory_plugin 1.4.621 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +352 -366
- package/mcp-server/admin/auth.js +31 -4
- package/mcp-server/cli/direct_commands.js +313 -0
- package/mcp-server/cli/handlers/cloud_actions.js +138 -0
- package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
- package/mcp-server/cli/handlers/engine_actions.js +214 -0
- package/mcp-server/cli/handlers/prompt_actions.js +24 -0
- package/mcp-server/cli/handlers/storage_actions.js +749 -0
- package/mcp-server/cli/quick_stats.js +39 -0
- package/mcp-server/cli/ui.js +565 -0
- package/mcp-server/cli.js +324 -2085
- package/mcp-server/config/auth_store.js +56 -9
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +14 -1
- package/mcp-server/db/migrations.js +28 -0
- package/mcp-server/fact_format.js +244 -177
- package/mcp-server/identity.js +152 -0
- package/mcp-server/index.js +42 -679
- package/mcp-server/memory.js +50 -63
- package/mcp-server/prompt_manager.js +1 -1
- package/mcp-server/tools/helpers.js +39 -0
- package/mcp-server/tools/identity_tools.js +277 -0
- package/mcp-server/tools/index.js +9 -0
- package/mcp-server/tools/memory_tools.js +506 -0
- package/mcp-server/tools/rag_tools.js +235 -0
- package/opencode-plugin/index.js +460 -48
- package/package.json +7 -3
- package/skills/using-memory/SKILL.md +31 -14
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/quality_evaluator.js +0 -600
- package/mcp-server/benchmarks/run_benchmarks.js +0 -347
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- package/mcp-server/benchmarks/test_dual_layer.js +0 -140
package/mcp-server/cli.js
CHANGED
|
@@ -1,2085 +1,324 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import readline from "readline";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const line = "─".repeat(Math.max(2, PANEL_WIDTH - titleStr.length - 7));
|
|
326
|
-
console.log(`\x1b[36m ┌── ${titleStr} ${line}┐\x1b[0m`);
|
|
327
|
-
|
|
328
|
-
block.items.forEach((item) => {
|
|
329
|
-
const isSelected = currentItemGlobalIndex === activeIndex;
|
|
330
|
-
const pointer = isSelected ? "\x1b[36m > " : " ";
|
|
331
|
-
|
|
332
|
-
const nameStr = item.label;
|
|
333
|
-
const dotsCount = Math.max(2, 32 - nameStr.length);
|
|
334
|
-
const dots = "\x1b[90m" + ".".repeat(dotsCount) + "\x1b[0m";
|
|
335
|
-
const badgeStr = item.badge ? `\x1b[33m[${item.badge}]\x1b[0m` : "";
|
|
336
|
-
|
|
337
|
-
let lineContent = item.badge ? `${nameStr} ${dots} ${badgeStr}` : nameStr;
|
|
338
|
-
|
|
339
|
-
if (isSelected) {
|
|
340
|
-
console.log(` \x1b[36m│\x1b[0m${pointer}\x1b[1m\x1b[36m${lineContent}\x1b[0m`);
|
|
341
|
-
} else {
|
|
342
|
-
console.log(` \x1b[36m│\x1b[0m${pointer}${lineContent}`);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
currentItemGlobalIndex++;
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
console.log(`\x1b[36m └──${"─".repeat(PANEL_WIDTH - 4)}┘\x1b[0m\n`);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const activeItem = allItems[activeIndex];
|
|
352
|
-
if (activeItem && activeItem.info) {
|
|
353
|
-
printQuickInfoBox(activeItem.info);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
render();
|
|
358
|
-
|
|
359
|
-
function onKeypress(str, key) {
|
|
360
|
-
if (!key) return;
|
|
361
|
-
if (key.ctrl && key.name === "c") {
|
|
362
|
-
cleanup();
|
|
363
|
-
process.exit(0);
|
|
364
|
-
}
|
|
365
|
-
if (key.name === "up") {
|
|
366
|
-
activeIndex = (activeIndex - 1 + allItems.length) % allItems.length;
|
|
367
|
-
render();
|
|
368
|
-
} else if (key.name === "down") {
|
|
369
|
-
activeIndex = (activeIndex + 1) % allItems.length;
|
|
370
|
-
render();
|
|
371
|
-
} else if (key.name === "return") {
|
|
372
|
-
cleanup();
|
|
373
|
-
resolve({ action: "select", index: activeIndex, value: allItems[activeIndex].value });
|
|
374
|
-
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
375
|
-
cleanup();
|
|
376
|
-
resolve({ action: "back" });
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function cleanup() {
|
|
381
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
382
|
-
if (process.stdin.isTTY) {
|
|
383
|
-
process.stdin.setRawMode(false);
|
|
384
|
-
}
|
|
385
|
-
process.stdin.pause();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
process.stdin.on("keypress", onKeypress);
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function selectSimpleMenu({ title, subtitle = "", items, initialIndex = 0 }) {
|
|
393
|
-
return new Promise((resolve) => {
|
|
394
|
-
let index = Math.min(Math.max(0, initialIndex), items.length - 1);
|
|
395
|
-
|
|
396
|
-
readline.emitKeypressEvents(process.stdin);
|
|
397
|
-
if (process.stdin.isTTY) {
|
|
398
|
-
process.stdin.setRawMode(true);
|
|
399
|
-
}
|
|
400
|
-
process.stdin.resume();
|
|
401
|
-
|
|
402
|
-
function render() {
|
|
403
|
-
console.clear();
|
|
404
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
405
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
406
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37m${title.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
407
|
-
if (subtitle) {
|
|
408
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90m${subtitle.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
409
|
-
}
|
|
410
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
411
|
-
console.log(" \x1b[90mControls: ↑ / ↓ - Navigate [ENTER] - Select [BACKSPACE] - Back\x1b[0m\n");
|
|
412
|
-
|
|
413
|
-
items.forEach((item, idx) => {
|
|
414
|
-
const isSelected = idx === index;
|
|
415
|
-
const pointer = isSelected ? "\x1b[36m > " : " ";
|
|
416
|
-
const label = isSelected ? `\x1b[1m\x1b[36m${item.label}\x1b[0m` : item.label;
|
|
417
|
-
const badge = item.badge ? ` \x1b[33m[${item.badge}]\x1b[0m` : "";
|
|
418
|
-
const hint = item.hint ? ` \x1b[90m(${item.hint})\x1b[0m` : "";
|
|
419
|
-
console.log(`${pointer}${label}${badge}${hint}`);
|
|
420
|
-
});
|
|
421
|
-
console.log("\n");
|
|
422
|
-
|
|
423
|
-
const activeItem = items[index];
|
|
424
|
-
if (activeItem && activeItem.info) {
|
|
425
|
-
printQuickInfoBox(activeItem.info);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
render();
|
|
430
|
-
|
|
431
|
-
function onKeypress(str, key) {
|
|
432
|
-
if (!key) return;
|
|
433
|
-
if (key.ctrl && key.name === "c") {
|
|
434
|
-
cleanup();
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
|
-
if (key.name === "up") {
|
|
438
|
-
index = (index - 1 + items.length) % items.length;
|
|
439
|
-
render();
|
|
440
|
-
} else if (key.name === "down") {
|
|
441
|
-
index = (index + 1) % items.length;
|
|
442
|
-
render();
|
|
443
|
-
} else if (key.name === "return") {
|
|
444
|
-
cleanup();
|
|
445
|
-
resolve({ action: "select", index, value: items[index].value });
|
|
446
|
-
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
447
|
-
cleanup();
|
|
448
|
-
resolve({ action: "back" });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function cleanup() {
|
|
453
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
454
|
-
if (process.stdin.isTTY) {
|
|
455
|
-
process.stdin.setRawMode(false);
|
|
456
|
-
}
|
|
457
|
-
process.stdin.pause();
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
process.stdin.on("keypress", onKeypress);
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function adjustAlphaMenu(initialAlpha) {
|
|
465
|
-
return new Promise((resolve) => {
|
|
466
|
-
let alpha = initialAlpha;
|
|
467
|
-
|
|
468
|
-
readline.emitKeypressEvents(process.stdin);
|
|
469
|
-
if (process.stdin.isTTY) {
|
|
470
|
-
process.stdin.setRawMode(true);
|
|
471
|
-
}
|
|
472
|
-
process.stdin.resume();
|
|
473
|
-
|
|
474
|
-
function render() {
|
|
475
|
-
console.clear();
|
|
476
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
477
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
478
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mRSF ALPHA WEIGHT BALANCER\x1b[0m${" ".repeat(PANEL_WIDTH - 30)}\x1b[36m│\x1b[0m`);
|
|
479
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90mAdjust Vector Similarity vs BM25 Score Weight\x1b[0m${" ".repeat(PANEL_WIDTH - 49)}\x1b[36m│\x1b[0m`);
|
|
480
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
481
|
-
console.log(" \x1b[90mControls: ← / → or ↑ / ↓ - Adjust (5% step) [ENTER] - Save [BACKSPACE] - Cancel\x1b[0m\n");
|
|
482
|
-
|
|
483
|
-
const semPct = Math.round(alpha * 100);
|
|
484
|
-
const lexPct = 100 - semPct;
|
|
485
|
-
|
|
486
|
-
const totalBlocks = 20;
|
|
487
|
-
const semBlocks = Math.round(alpha * totalBlocks);
|
|
488
|
-
const lexBlocks = totalBlocks - semBlocks;
|
|
489
|
-
|
|
490
|
-
const bar = "━".repeat(semBlocks) + "─".repeat(lexBlocks);
|
|
491
|
-
|
|
492
|
-
console.log(` Balance: \x1b[36m${semPct}% Semantic (Vector)\x1b[0m / \x1b[33m${lexPct}% Lexical (BM25)\x1b[0m`);
|
|
493
|
-
console.log(` [ \x1b[36m${bar}\x1b[0m ] Alpha: \x1b[1m\x1b[32m${alpha.toFixed(2)}\x1b[0m\n`);
|
|
494
|
-
|
|
495
|
-
if (alpha === 0.5) {
|
|
496
|
-
console.log(" [*] \x1b[32mMode: 50 / 50 Balanced Hybrid Fusion (Recommended)\x1b[0m\n");
|
|
497
|
-
} else if (alpha > 0.5) {
|
|
498
|
-
console.log(` [*] Mode: Semantic Vector Priority (${semPct}%)\n`);
|
|
499
|
-
} else {
|
|
500
|
-
console.log(` [*] Mode: Exact Keyword BM25 Priority (${lexPct}%)\n`);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
printQuickInfoBox(`RSF Formula: Score = ${alpha.toFixed(2)} * NormVector + ${(1 - alpha).toFixed(2)} * NormBM25`);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
render();
|
|
507
|
-
|
|
508
|
-
function onKeypress(str, key) {
|
|
509
|
-
if (!key) return;
|
|
510
|
-
if (key.ctrl && key.name === "c") {
|
|
511
|
-
cleanup();
|
|
512
|
-
process.exit(0);
|
|
513
|
-
}
|
|
514
|
-
if (key.name === "left" || key.name === "down") {
|
|
515
|
-
alpha = Math.max(0.0, Math.round((alpha - 0.05) * 100) / 100);
|
|
516
|
-
render();
|
|
517
|
-
} else if (key.name === "right" || key.name === "up") {
|
|
518
|
-
alpha = Math.min(1.0, Math.round((alpha + 0.05) * 100) / 100);
|
|
519
|
-
render();
|
|
520
|
-
} else if (key.name === "return") {
|
|
521
|
-
cleanup();
|
|
522
|
-
resolve({ action: "save", value: alpha });
|
|
523
|
-
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
524
|
-
cleanup();
|
|
525
|
-
resolve({ action: "cancel" });
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function cleanup() {
|
|
530
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
531
|
-
if (process.stdin.isTTY) {
|
|
532
|
-
process.stdin.setRawMode(false);
|
|
533
|
-
}
|
|
534
|
-
process.stdin.pause();
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
process.stdin.on("keypress", onKeypress);
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function readTextInput(promptText, defaultValue = "") {
|
|
542
|
-
return new Promise((resolve) => {
|
|
543
|
-
let text = defaultValue;
|
|
544
|
-
|
|
545
|
-
readline.emitKeypressEvents(process.stdin);
|
|
546
|
-
if (process.stdin.isTTY) {
|
|
547
|
-
process.stdin.setRawMode(true);
|
|
548
|
-
}
|
|
549
|
-
process.stdin.resume();
|
|
550
|
-
|
|
551
|
-
function render() {
|
|
552
|
-
console.clear();
|
|
553
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
554
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
555
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mINPUT: ${promptText.toUpperCase()}\x1b[0m${" ".repeat(Math.max(0, PANEL_WIDTH - 11 - promptText.length))}\x1b[36m│\x1b[0m`);
|
|
556
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
557
|
-
console.log(" \x1b[90mControls: Type text [ENTER] - Submit [BACKSPACE] - Delete / Cancel\x1b[0m\n");
|
|
558
|
-
console.log(` > \x1b[36m${text}\x1b[0m_\n`);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
render();
|
|
562
|
-
|
|
563
|
-
function onKeypress(str, key) {
|
|
564
|
-
if (!key) return;
|
|
565
|
-
if (key.ctrl && key.name === "c") {
|
|
566
|
-
cleanup();
|
|
567
|
-
process.exit(0);
|
|
568
|
-
}
|
|
569
|
-
if (key.name === "return") {
|
|
570
|
-
cleanup();
|
|
571
|
-
resolve({ action: "submit", value: text.trim() });
|
|
572
|
-
} else if (key.name === "backspace" || key.name === "delete") {
|
|
573
|
-
if (text.length > 0) {
|
|
574
|
-
text = text.slice(0, -1);
|
|
575
|
-
render();
|
|
576
|
-
} else {
|
|
577
|
-
cleanup();
|
|
578
|
-
resolve({ action: "cancel" });
|
|
579
|
-
}
|
|
580
|
-
} else if (key.name === "escape") {
|
|
581
|
-
cleanup();
|
|
582
|
-
resolve({ action: "cancel" });
|
|
583
|
-
} else if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
584
|
-
text += str;
|
|
585
|
-
render();
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function cleanup() {
|
|
590
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
591
|
-
if (process.stdin.isTTY) {
|
|
592
|
-
process.stdin.setRawMode(false);
|
|
593
|
-
}
|
|
594
|
-
process.stdin.pause();
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
process.stdin.on("keypress", onKeypress);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function waitForEnter() {
|
|
602
|
-
return new Promise((resolve) => {
|
|
603
|
-
console.log("\n \x1b[90mPress [ENTER] or [BACKSPACE] to return to menu...\x1b[0m");
|
|
604
|
-
readline.emitKeypressEvents(process.stdin);
|
|
605
|
-
if (process.stdin.isTTY) {
|
|
606
|
-
process.stdin.setRawMode(true);
|
|
607
|
-
}
|
|
608
|
-
process.stdin.resume();
|
|
609
|
-
|
|
610
|
-
function onKeypress(str, key) {
|
|
611
|
-
if (!key) return;
|
|
612
|
-
if (key.ctrl && key.name === "c") {
|
|
613
|
-
cleanup();
|
|
614
|
-
process.exit(0);
|
|
615
|
-
}
|
|
616
|
-
if (key.name === "return" || key.name === "backspace" || key.name === "escape" || key.name === "delete" || key.name === "space") {
|
|
617
|
-
cleanup();
|
|
618
|
-
resolve();
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function cleanup() {
|
|
623
|
-
process.stdin.removeListener("keypress", onKeypress);
|
|
624
|
-
if (process.stdin.isTTY) {
|
|
625
|
-
process.stdin.setRawMode(false);
|
|
626
|
-
}
|
|
627
|
-
process.stdin.pause();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
process.stdin.on("keypress", onKeypress);
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function promptText(question) {
|
|
635
|
-
return new Promise((resolve) => {
|
|
636
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
637
|
-
rl.question(`\n ${question}\n > `, (answer) => {
|
|
638
|
-
rl.close();
|
|
639
|
-
resolve(answer.trim());
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
export async function runCli() {
|
|
645
|
-
const cliArgs = process.argv.slice(2);
|
|
646
|
-
if (cliArgs.includes("--enable-prompt") || cliArgs.includes("enable-prompt")) {
|
|
647
|
-
const { enableGlobalPrompt } = await import("./prompt_manager.js");
|
|
648
|
-
const results = await enableGlobalPrompt();
|
|
649
|
-
console.log("\n [OK] Global prompt enabled across client configurations:\n");
|
|
650
|
-
results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
|
|
651
|
-
console.log("");
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (cliArgs.includes("--disable-prompt") || cliArgs.includes("disable-prompt")) {
|
|
655
|
-
const { disableGlobalPrompt } = await import("./prompt_manager.js");
|
|
656
|
-
const results = await disableGlobalPrompt();
|
|
657
|
-
console.log("\n [OK] Global prompt disabled across client configurations:\n");
|
|
658
|
-
results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
|
|
659
|
-
console.log("");
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
if (cliArgs.includes("login")) {
|
|
664
|
-
console.log("\n [CLOUD] Starting Turso cloud authorization...");
|
|
665
|
-
const loginIdx = cliArgs.indexOf("login");
|
|
666
|
-
const loginArgs = cliArgs.slice(loginIdx + 1);
|
|
667
|
-
const flagValue = (name) => {
|
|
668
|
-
const i = loginArgs.indexOf(name);
|
|
669
|
-
return i >= 0 && loginArgs[i + 1] ? loginArgs[i + 1] : null;
|
|
670
|
-
};
|
|
671
|
-
const { loginToCloud, loginWithApiToken, loginWithDatabaseToken, loginFromEnv } = await import("./admin/auth.js");
|
|
672
|
-
try {
|
|
673
|
-
let secrets;
|
|
674
|
-
if (loginArgs.includes("--from-env")) {
|
|
675
|
-
// Headless: pick up TURSO_DB_URL / TURSO_DB_TOKEN / TURSO_API_TOKEN from env or .env
|
|
676
|
-
const res = await loginFromEnv({ persist: false });
|
|
677
|
-
if (!res.ok) throw new Error(res.reason);
|
|
678
|
-
secrets = res.secrets;
|
|
679
|
-
} else if (loginArgs.includes("--db-url") && loginArgs.includes("--db-token")) {
|
|
680
|
-
// Headless: direct database URL + token (no Platform API calls)
|
|
681
|
-
secrets = await loginWithDatabaseToken({
|
|
682
|
-
dbUrl: flagValue("--db-url"),
|
|
683
|
-
token: flagValue("--db-token"),
|
|
684
|
-
username: flagValue("--username") || "",
|
|
685
|
-
org: flagValue("--org") || "",
|
|
686
|
-
db: flagValue("--database") || "",
|
|
687
|
-
validate: !loginArgs.includes("--no-validate"),
|
|
688
|
-
});
|
|
689
|
-
} else if (loginArgs.includes("--token") || loginArgs.includes("--api-token") || loginArgs.includes("--api-key")) {
|
|
690
|
-
// Headless: account API token resolved via the Turso Platform API
|
|
691
|
-
const token = flagValue("--token") || flagValue("--api-token") || flagValue("--api-key");
|
|
692
|
-
if (!token) throw new Error("Missing token value. Usage: memory_plugin login --api-token <TOKEN> [--org <ORG>] [--database <DB>]");
|
|
693
|
-
secrets = await loginWithApiToken({
|
|
694
|
-
token,
|
|
695
|
-
org: flagValue("--org") || null,
|
|
696
|
-
databaseName: flagValue("--database") || null,
|
|
697
|
-
});
|
|
698
|
-
} else {
|
|
699
|
-
// Default: interactive browser OAuth flow
|
|
700
|
-
secrets = await loginToCloud();
|
|
701
|
-
}
|
|
702
|
-
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
|
|
703
|
-
} catch (e) {
|
|
704
|
-
console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (cliArgs.includes("logout")) {
|
|
711
|
-
const { logoutFromCloud, clearApiKey } = await import("./admin/auth.js");
|
|
712
|
-
if (cliArgs.includes("--api-key")) {
|
|
713
|
-
// Headless: remove only the stored account API token
|
|
714
|
-
const res = clearApiKey();
|
|
715
|
-
if (res.removed) {
|
|
716
|
-
console.log(
|
|
717
|
-
res.keptDbSession
|
|
718
|
-
? " \x1b[32m[OK] API token removed. The resolved database session is kept and stays authorized.\x1b[0m\n"
|
|
719
|
-
: " \x1b[32m[OK] API token removed. Encrypted secrets purged.\x1b[0m\n"
|
|
720
|
-
);
|
|
721
|
-
} else {
|
|
722
|
-
console.log(" [*] No stored API token to remove.\x1b[0m\n");
|
|
723
|
-
}
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
console.log("\n [CLOUD] Signing out of the cloud...");
|
|
727
|
-
const deleted = logoutFromCloud();
|
|
728
|
-
if (deleted) {
|
|
729
|
-
console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
|
|
730
|
-
} else {
|
|
731
|
-
console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
|
|
732
|
-
}
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (cliArgs.includes("auth-status") || cliArgs.includes("auth_status") || cliArgs.includes("auth")) {
|
|
737
|
-
const { getAuthStatus } = await import("./admin/auth.js");
|
|
738
|
-
const st = getAuthStatus();
|
|
739
|
-
console.log("\n [CLOUD] Authentication status:");
|
|
740
|
-
console.log(` Source: ${st.source}`);
|
|
741
|
-
console.log(` Authorized: ${st.authorized ? "YES" : "no"}`);
|
|
742
|
-
console.log(` API Key: ${st.hasApiKey ? "SET" : "not set"}`);
|
|
743
|
-
console.log(` Endpoint: ${st.dbUrl || "(none)"}`);
|
|
744
|
-
console.log(` Username: ${st.username || "(unknown)"}`);
|
|
745
|
-
console.log(` Organization: ${st.org || "(unknown)"}`);
|
|
746
|
-
console.log(` Database: ${st.database || "(unknown)"}`);
|
|
747
|
-
console.log(` Mode: ${st.mode}`);
|
|
748
|
-
console.log("");
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
let running = true;
|
|
753
|
-
let selectedIndex = 0;
|
|
754
|
-
|
|
755
|
-
while (running) {
|
|
756
|
-
const config = getConfig();
|
|
757
|
-
const stats = await getQuickStats();
|
|
758
|
-
const semPct = Math.round(config.alpha * 100);
|
|
759
|
-
const lexPct = 100 - semPct;
|
|
760
|
-
|
|
761
|
-
const mainBlocks = [
|
|
762
|
-
{
|
|
763
|
-
title: "Engine & Hybrid Search Settings",
|
|
764
|
-
items: [
|
|
765
|
-
{
|
|
766
|
-
label: "Fusion Algorithm",
|
|
767
|
-
badge: config.fusionAlgorithm.toUpperCase(),
|
|
768
|
-
value: "algo",
|
|
769
|
-
info: "Choose how vector similarity and BM25 text ranks are fused",
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
label: "RSF Alpha Balance",
|
|
773
|
-
badge: `${semPct}% Sem / ${lexPct}% Lex`,
|
|
774
|
-
value: "alpha",
|
|
775
|
-
info: `Current Alpha: ${config.alpha.toFixed(2)}. Adjust ratio of Vector vs BM25 Keyword score`,
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
label: "Embedding Model",
|
|
779
|
-
badge: config.embeddingModel.split("/").pop(),
|
|
780
|
-
value: "embedding",
|
|
781
|
-
info: `Model: ${config.embeddingModel}. ONNX Feature Extraction via @huggingface/transformers`,
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
label: "Reranker Model",
|
|
785
|
-
badge: config.rerankerEnabled ? config.rerankerModel.split("/").pop() : "DISABLED",
|
|
786
|
-
value: "reranker",
|
|
787
|
-
info: config.rerankerEnabled ? `Reranker active: ${config.rerankerModel}` : "Optional Cross-Encoder re-ranking pass",
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
label: "Vector Batch Size",
|
|
791
|
-
badge: `${config.batchSize || 12} Chunks`,
|
|
792
|
-
value: "batch_size",
|
|
793
|
-
info: `Ingestion batch size: ${config.batchSize || 12} micro-chunks per ONNX pass`,
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
label: "GPU Attention Budget",
|
|
797
|
-
badge: `${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M Units`,
|
|
798
|
-
value: "gpu_budget",
|
|
799
|
-
info: `Micro-batch tensor budget: ${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M quadratic units (controls max peak VRAM usage on GPU)`,
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
label: "CPU WASM Threads",
|
|
803
|
-
badge: config.onnxThreads > 0 ? `${config.onnxThreads} Threads` : "AUTO (CPU Cores)",
|
|
804
|
-
value: "onnx_threads",
|
|
805
|
-
info: config.onnxThreads > 0 ? `ONNX execution threads manually set to ${config.onnxThreads}` : "Auto-detect optimal physical CPU threads",
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
label: "Execution Hardware",
|
|
809
|
-
badge: (config.executionDevice || "cpu").toUpperCase() === "WEBGPU" || (config.executionDevice || "cpu").toUpperCase() === "GPU" ? "\x1b[31mGPU (EXPERIMENTAL)\x1b[0m" : "CPU (AVX2)",
|
|
810
|
-
value: "execution_device",
|
|
811
|
-
info: config.executionDevice === "webgpu" || config.executionDevice === "gpu"
|
|
812
|
-
? "⚠️ EXPERIMENTAL: ONNX DirectML GPU execution (high VRAM/padding overhead, CPU AVX2 recommended)"
|
|
813
|
-
: "CPU inference via AVX2 / WASM SIMD (Recommended for stability & speed)",
|
|
814
|
-
},
|
|
815
|
-
],
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
title: "Knowledge Base & Storage Management",
|
|
819
|
-
items: [
|
|
820
|
-
{
|
|
821
|
-
label: "[NOTEBOOK] Layer 1 Facts",
|
|
822
|
-
badge: `${stats.factCount} Facts Saved`,
|
|
823
|
-
value: "notebook",
|
|
824
|
-
info: "Inspect & delete durable user identity facts (global & project)",
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
label: "[RAG DOCS] Layer 2 RAG Base",
|
|
828
|
-
badge: `${stats.docCount} Docs / ${stats.chunkCount} Chunks`,
|
|
829
|
-
value: "rag_docs",
|
|
830
|
-
info: "Inspect ingested Markdown/code docs & delete chunks from SQLite",
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
label: "[SNAPSHOT EXPORT] Export RAG Base Snapshot",
|
|
834
|
-
value: "export_snapshot",
|
|
835
|
-
info: "Export full RAG database, vectors & blobs into a snapshot file (.json or .json.gz)",
|
|
836
|
-
},
|
|
837
|
-
{
|
|
838
|
-
label: "[SNAPSHOT IMPORT] Import RAG Base Snapshot",
|
|
839
|
-
value: "import_snapshot",
|
|
840
|
-
info: "Import RAG database, vectors & blobs from a snapshot file (.json or .json.gz)",
|
|
841
|
-
},
|
|
842
|
-
{
|
|
843
|
-
label: "[MODELS] Manage & Purge ML Model Cache",
|
|
844
|
-
value: "manage_models",
|
|
845
|
-
info: "Inspect cached ONNX models on disk, check status (Ready / Partial / Not Downloaded) & delete models to free disk space",
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
label: "[HARD RESET] Purge RAG Base & Blob Storage",
|
|
849
|
-
value: "hard_reset",
|
|
850
|
-
info: "Permanently delete all documents, sections, vectors, FTS indexes, and blobs",
|
|
851
|
-
},
|
|
852
|
-
],
|
|
853
|
-
},
|
|
854
|
-
{
|
|
855
|
-
title: "Cloud Synchronization & Turso",
|
|
856
|
-
items: [
|
|
857
|
-
{
|
|
858
|
-
label: "[CLOUD] Login to Turso Cloud",
|
|
859
|
-
value: "cloud_login",
|
|
860
|
-
info: "Browser OAuth, account API token, database URL+token, or import from environment (.env) — token/env methods work headless in Docker, Google Jules and VPS",
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
label: "[CLOUD] Logout",
|
|
864
|
-
value: "cloud_logout",
|
|
865
|
-
info: "Sign out, purge encrypted secrets, and revert mode to only-local",
|
|
866
|
-
},
|
|
867
|
-
{
|
|
868
|
-
label: "[API KEY] Set / Replace Account API Token",
|
|
869
|
-
value: "cloud_api_set",
|
|
870
|
-
info: "Paste a Turso account API token to authorize headless (Docker, Google Jules, VPS) — validated and persisted",
|
|
871
|
-
},
|
|
872
|
-
{
|
|
873
|
-
label: "[API KEY] Remove Account API Token",
|
|
874
|
-
value: "cloud_api_clear",
|
|
875
|
-
info: "Delete the stored API token; the resolved database session is kept",
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
label: "Operational Mode",
|
|
879
|
-
badge: config.mode.toUpperCase(),
|
|
880
|
-
value: "cloud_mode",
|
|
881
|
-
info: "Choose Operational Mode: only-local | only-cloud | hybrid-sync",
|
|
882
|
-
},
|
|
883
|
-
{
|
|
884
|
-
label: "Conflict Strategy",
|
|
885
|
-
badge: (config.conflictStrategy || "merge").toUpperCase(),
|
|
886
|
-
value: "conflict_strategy",
|
|
887
|
-
info: "How hybrid-sync resolves differing local vs cloud stores: merge | cloud-wins | local-wins",
|
|
888
|
-
},
|
|
889
|
-
],
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
title: "Global Prompt & Integration Management",
|
|
893
|
-
items: [
|
|
894
|
-
{
|
|
895
|
-
label: "[PROMPT ENABLE] Enable Global Prompt (Antigravity / Codex / Claude)",
|
|
896
|
-
value: "enable_prompt",
|
|
897
|
-
info: "Inject memory instructions into ~/.gemini/config/AGENTS.md, ~/.codex/AGENTS.md, ~/.claude/CLAUDE.md",
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
label: "[PROMPT DISABLE] Disable Global Prompt",
|
|
901
|
-
value: "disable_prompt",
|
|
902
|
-
info: "Remove memory instructions from global AGENTS.md / CLAUDE.md files",
|
|
903
|
-
},
|
|
904
|
-
],
|
|
905
|
-
},
|
|
906
|
-
{
|
|
907
|
-
title: "Diagnostics & System Actions",
|
|
908
|
-
items: [
|
|
909
|
-
{
|
|
910
|
-
label: "[BENCHMARK] Run Search Quality Benchmark",
|
|
911
|
-
value: "benchmark",
|
|
912
|
-
info: "Choose Quick Smoke (9 queries, ~7s) or Full (21 queries + stats, ~32s)",
|
|
913
|
-
},
|
|
914
|
-
{
|
|
915
|
-
label: "[SEARCH] Run Search Verification Query",
|
|
916
|
-
value: "test",
|
|
917
|
-
info: "Execute hybrid search query and display result hit scores",
|
|
918
|
-
},
|
|
919
|
-
{
|
|
920
|
-
label: "[CACHE] Clear Benchmark Corpus Cache",
|
|
921
|
-
value: "clear_cache",
|
|
922
|
-
info: "Delete cached GitHub README files used by benchmarks",
|
|
923
|
-
},
|
|
924
|
-
{
|
|
925
|
-
label: "[RESET] Reset Config to Factory Defaults",
|
|
926
|
-
value: "reset",
|
|
927
|
-
info: "Reset RSF alpha to 50/50 and restore factory default config",
|
|
928
|
-
},
|
|
929
|
-
{
|
|
930
|
-
label: "[EXIT] Exit CLI Menu",
|
|
931
|
-
value: "exit",
|
|
932
|
-
info: "Save configuration and exit to terminal",
|
|
933
|
-
},
|
|
934
|
-
],
|
|
935
|
-
},
|
|
936
|
-
];
|
|
937
|
-
|
|
938
|
-
const res = await selectBlockMenu({
|
|
939
|
-
title: "MEMORY PLUGIN RAG ENGINE CONTROL PANEL",
|
|
940
|
-
stats,
|
|
941
|
-
blocks: mainBlocks,
|
|
942
|
-
initialIndex: selectedIndex,
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
if (res.action === "back") {
|
|
946
|
-
running = false;
|
|
947
|
-
console.clear();
|
|
948
|
-
console.log("Exiting CLI. Configuration saved.");
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
selectedIndex = res.index;
|
|
953
|
-
|
|
954
|
-
switch (res.value) {
|
|
955
|
-
case "algo": {
|
|
956
|
-
const algoItems = [
|
|
957
|
-
{ label: "RSF (Relative Score Fusion)", value: "rsf", info: "Normalized Score Scaling (Recommended)" },
|
|
958
|
-
{ label: "RRF (Reciprocal Rank Fusion)", value: "rrf", info: "Rank-based Fusion (1/(k + rank))" },
|
|
959
|
-
{ label: "Pure Semantic Search", value: "semantic_only", info: "Vector Search Only (Cosine Similarity)" },
|
|
960
|
-
{ label: "Pure Lexical Search", value: "lexical_only", info: "BM25 Text Search Only (SQLite FTS5)" },
|
|
961
|
-
];
|
|
962
|
-
const initialAlgoIdx = Math.max(0, algoItems.findIndex((i) => i.value === config.fusionAlgorithm));
|
|
963
|
-
const subRes = await selectSimpleMenu({
|
|
964
|
-
title: "SELECT FUSION ALGORITHM",
|
|
965
|
-
subtitle: "Choose how vector and keyword search scores are combined",
|
|
966
|
-
items: algoItems,
|
|
967
|
-
initialIndex: initialAlgoIdx,
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
if (subRes.action === "select") {
|
|
971
|
-
updateConfig({ fusionAlgorithm: subRes.value });
|
|
972
|
-
}
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
case "alpha": {
|
|
976
|
-
const alphaRes = await adjustAlphaMenu(config.alpha);
|
|
977
|
-
if (alphaRes.action === "save") {
|
|
978
|
-
updateConfig({ alpha: alphaRes.value });
|
|
979
|
-
}
|
|
980
|
-
break;
|
|
981
|
-
}
|
|
982
|
-
case "embedding": {
|
|
983
|
-
const embItems = EMBEDDING_PRESETS.map((m) => {
|
|
984
|
-
const info = getModelStorageInfo(m);
|
|
985
|
-
let badge = "NOT DOWNLOADED";
|
|
986
|
-
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
987
|
-
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
988
|
-
return { label: m, badge, value: m, info: `Model: ${m} [${badge}]` };
|
|
989
|
-
});
|
|
990
|
-
embItems.push({ label: "Custom HuggingFace Model...", value: "custom", info: "Specify custom HF model string" });
|
|
991
|
-
const initialEmbIdx = Math.max(0, embItems.findIndex((i) => i.value === config.embeddingModel));
|
|
992
|
-
|
|
993
|
-
const subRes = await selectSimpleMenu({
|
|
994
|
-
title: "SELECT EMBEDDING MODEL",
|
|
995
|
-
subtitle: "Dense vector extraction model via @huggingface/transformers",
|
|
996
|
-
items: embItems,
|
|
997
|
-
initialIndex: initialEmbIdx,
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
if (subRes.action === "select") {
|
|
1001
|
-
let chosenModel = subRes.value;
|
|
1002
|
-
if (subRes.value === "custom") {
|
|
1003
|
-
const inputRes = await readTextInput("Enter HuggingFace Model ID", "Xenova/all-MiniLM-L6-v2");
|
|
1004
|
-
if (inputRes.action === "submit" && inputRes.value) {
|
|
1005
|
-
chosenModel = inputRes.value;
|
|
1006
|
-
} else {
|
|
1007
|
-
break;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
await downloadModelWithProgress(chosenModel, "embedding");
|
|
1011
|
-
updateConfig({ embeddingModel: chosenModel });
|
|
1012
|
-
await waitForEnter();
|
|
1013
|
-
}
|
|
1014
|
-
break;
|
|
1015
|
-
}
|
|
1016
|
-
case "reranker": {
|
|
1017
|
-
const rkItems = [
|
|
1018
|
-
{ label: "Disable Reranker", value: "none", info: "No cross-encoder re-ranking" },
|
|
1019
|
-
...RERANKER_PRESETS.filter((r) => r !== "none").map((r) => {
|
|
1020
|
-
const info = getModelStorageInfo(r);
|
|
1021
|
-
let badge = "NOT DOWNLOADED";
|
|
1022
|
-
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
1023
|
-
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
1024
|
-
return { label: r, badge, value: r, info: `Reranker: ${r} [${badge}]` };
|
|
1025
|
-
}),
|
|
1026
|
-
{ label: "Custom Reranker Model...", value: "custom", info: "Specify custom HuggingFace cross-encoder model" },
|
|
1027
|
-
];
|
|
1028
|
-
const currentRk = config.rerankerEnabled ? config.rerankerModel : "none";
|
|
1029
|
-
const initialRkIdx = Math.max(0, rkItems.findIndex((i) => i.value === currentRk));
|
|
1030
|
-
|
|
1031
|
-
const subRes = await selectSimpleMenu({
|
|
1032
|
-
title: "CONFIGURE RERANKER MODEL",
|
|
1033
|
-
subtitle: "Cross-Encoder candidate re-ranking pass",
|
|
1034
|
-
items: rkItems,
|
|
1035
|
-
initialIndex: initialRkIdx,
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
if (subRes.action === "select") {
|
|
1039
|
-
if (subRes.value === "none") {
|
|
1040
|
-
updateConfig({ rerankerEnabled: false, rerankerModel: "none" });
|
|
1041
|
-
} else {
|
|
1042
|
-
let chosenRk = subRes.value;
|
|
1043
|
-
if (subRes.value === "custom") {
|
|
1044
|
-
const inputRes = await readTextInput("Enter HuggingFace Reranker Model ID", "Xenova/bge-reranker-base");
|
|
1045
|
-
if (inputRes.action === "submit" && inputRes.value) {
|
|
1046
|
-
chosenRk = inputRes.value;
|
|
1047
|
-
} else {
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
await downloadModelWithProgress(chosenRk, "reranker");
|
|
1052
|
-
updateConfig({ rerankerEnabled: true, rerankerModel: chosenRk });
|
|
1053
|
-
await waitForEnter();
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
break;
|
|
1057
|
-
}
|
|
1058
|
-
case "batch_size": {
|
|
1059
|
-
const batchItems = [
|
|
1060
|
-
{ label: "Batch Size 1 (Single Item)", value: 1, info: "Process micro-chunks strictly 1 by 1" },
|
|
1061
|
-
{ label: "Batch Size 4", value: 4, info: "Small CPU batch size" },
|
|
1062
|
-
{ label: "Batch Size 8 (CPU Sweet Spot)", value: 8, info: "Optimal for CPU L3 cache" },
|
|
1063
|
-
{ label: "Batch Size 12 (Default)", value: 12, info: "Balanced CPU throughput" },
|
|
1064
|
-
{ label: "Batch Size 16", value: 16, info: "High throughput batch size" },
|
|
1065
|
-
{ label: "Batch Size 32 (Standard GPU)", value: 32, info: "Standard GPU batching" },
|
|
1066
|
-
{ label: "Batch Size 48 (High GPU)", value: 48, info: "High throughput GPU batching" },
|
|
1067
|
-
{ label: "Batch Size 64 (Ultra GPU)", value: 64, info: "Ultra-fast GPU parallel tensor execution" },
|
|
1068
|
-
{ label: "Batch Size 128 (Extreme GPU)", value: 128, info: "Massive GPU parallelism" },
|
|
1069
|
-
{ label: "Batch Size 256 (Max GPU)", value: 256, info: "Maximum batch capacity for dedicated VRAM" },
|
|
1070
|
-
];
|
|
1071
|
-
const currentBatch = config.batchSize || 12;
|
|
1072
|
-
const initialBatchIdx = Math.max(0, batchItems.findIndex((i) => i.value === currentBatch));
|
|
1073
|
-
const subRes = await selectSimpleMenu({
|
|
1074
|
-
title: "SELECT VECTOR BATCH SIZE",
|
|
1075
|
-
subtitle: "Number of micro-chunks vectorized per ONNX inference pass",
|
|
1076
|
-
items: batchItems,
|
|
1077
|
-
initialIndex: initialBatchIdx,
|
|
1078
|
-
});
|
|
1079
|
-
if (subRes.action === "select") {
|
|
1080
|
-
updateConfig({ batchSize: subRes.value });
|
|
1081
|
-
}
|
|
1082
|
-
break;
|
|
1083
|
-
}
|
|
1084
|
-
case "gpu_budget": {
|
|
1085
|
-
const budgetItems = [
|
|
1086
|
-
{ label: "1.0M Units (Conservative ~0.8 GB VRAM)", value: 1000000, info: "Ultra-safe for 4GB-6GB GPUs or heavy background multitasking" },
|
|
1087
|
-
{ label: "2.0M Units (Balanced ~1.5 GB VRAM - Default)", value: 2000000, info: "Optimal balance between GPU throughput & safe VRAM ceiling" },
|
|
1088
|
-
{ label: "4.0M Units (Aggressive ~2.5 GB VRAM)", value: 4000000, info: "Higher GPU parallel compute for dedicated 8GB+ GPUs" },
|
|
1089
|
-
{ label: "8.0M Units (High Parallelism ~4.5 GB VRAM)", value: 8000000, info: "Maximum batching throughput for 12GB-16GB VRAM GPUs" },
|
|
1090
|
-
{ label: "16.0M Units (Extreme ~8.0 GB VRAM)", value: 16000000, info: "Uncapped micro-batching for 24GB+ VRAM workstation GPUs" },
|
|
1091
|
-
];
|
|
1092
|
-
const currentBudget = config.gpuAttentionBudget || 2000000;
|
|
1093
|
-
const initialIdx = Math.max(0, budgetItems.findIndex((i) => i.value === currentBudget));
|
|
1094
|
-
const subRes = await selectSimpleMenu({
|
|
1095
|
-
title: "SELECT GPU MICRO-BATCH ATTENTION BUDGET",
|
|
1096
|
-
subtitle: "Controls dynamic O(seq_len^2) sub-batching to prevent VRAM overflow",
|
|
1097
|
-
items: budgetItems,
|
|
1098
|
-
initialIndex: initialIdx,
|
|
1099
|
-
});
|
|
1100
|
-
if (subRes.action === "select") {
|
|
1101
|
-
updateConfig({ gpuAttentionBudget: subRes.value });
|
|
1102
|
-
}
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
case "onnx_threads": {
|
|
1106
|
-
const threadItems = [
|
|
1107
|
-
{ label: "0 - Auto (Detect CPU Cores)", value: 0, info: "Automatically match physical CPU cores (up to 8)" },
|
|
1108
|
-
{ label: "1 Thread (Single-Threaded)", value: 1, info: "Restrict ONNX WASM to 1 thread" },
|
|
1109
|
-
{ label: "2 Threads", value: 2, info: "Use 2 WASM threads" },
|
|
1110
|
-
{ label: "4 Threads", value: 4, info: "Use 4 WASM threads" },
|
|
1111
|
-
{ label: "8 Threads", value: 8, info: "Use 8 WASM threads" },
|
|
1112
|
-
{ label: "16 Threads", value: 16, info: "Use 16 WASM threads" },
|
|
1113
|
-
];
|
|
1114
|
-
const currentThreads = config.onnxThreads || 0;
|
|
1115
|
-
const initialThreadIdx = Math.max(0, threadItems.findIndex((i) => i.value === currentThreads));
|
|
1116
|
-
const subRes = await selectSimpleMenu({
|
|
1117
|
-
title: "SELECT CPU ONNX WASM THREADS",
|
|
1118
|
-
subtitle: "Number of WASM worker threads for ONNX Runtime",
|
|
1119
|
-
items: threadItems,
|
|
1120
|
-
initialIndex: initialThreadIdx,
|
|
1121
|
-
});
|
|
1122
|
-
if (subRes.action === "select") {
|
|
1123
|
-
updateConfig({ onnxThreads: subRes.value });
|
|
1124
|
-
}
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
case "execution_device": {
|
|
1128
|
-
const devItems = [
|
|
1129
|
-
{
|
|
1130
|
-
label: "CPU (AVX2 / WASM SIMD - RECOMMENDED)",
|
|
1131
|
-
value: "cpu",
|
|
1132
|
-
info: "Standard multi-threaded CPU execution via ONNX native AVX2 (Optimal speed, stability & zero VRAM overhead)",
|
|
1133
|
-
},
|
|
1134
|
-
{
|
|
1135
|
-
label: "\x1b[31m[EXPERIMENTAL]\x1b[0m GPU (DirectML / WebGPU)",
|
|
1136
|
-
value: "webgpu",
|
|
1137
|
-
info: "⚠️ EXPERIMENTAL: DirectML GPU tensor execution. High JS FFI & zero-padding overhead; CPU AVX2 is recommended for local Node.js.",
|
|
1138
|
-
},
|
|
1139
|
-
];
|
|
1140
|
-
const currentDev = config.executionDevice || "cpu";
|
|
1141
|
-
const initialDevIdx = Math.max(0, devItems.findIndex((i) => i.value === currentDev));
|
|
1142
|
-
const subRes = await selectSimpleMenu({
|
|
1143
|
-
title: "SELECT EXECUTION HARDWARE DEVICE",
|
|
1144
|
-
subtitle: "CPU AVX2 (Recommended) vs Experimental DirectML GPU Hardware Mode",
|
|
1145
|
-
items: devItems,
|
|
1146
|
-
initialIndex: initialDevIdx,
|
|
1147
|
-
});
|
|
1148
|
-
if (subRes.action === "select") {
|
|
1149
|
-
updateConfig({ executionDevice: subRes.value });
|
|
1150
|
-
}
|
|
1151
|
-
break;
|
|
1152
|
-
}
|
|
1153
|
-
case "notebook": {
|
|
1154
|
-
let nbRunning = true;
|
|
1155
|
-
while (nbRunning) {
|
|
1156
|
-
const projKey = projectKey(null, null);
|
|
1157
|
-
const projLabel = projectName(null, null);
|
|
1158
|
-
|
|
1159
|
-
async function browseFacts(key, title) {
|
|
1160
|
-
let factRunning = true;
|
|
1161
|
-
while (factRunning) {
|
|
1162
|
-
const rawEntries = await readMemory(key);
|
|
1163
|
-
const factList = await readMemoryRaw(key);
|
|
1164
|
-
|
|
1165
|
-
if (!factList || factList.length === 0) {
|
|
1166
|
-
console.clear();
|
|
1167
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1168
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1169
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mNOTEBOOK FACTS: STORE EMPTY\x1b[0m${" ".repeat(PANEL_WIDTH - 30)}\x1b[36m│\x1b[0m`);
|
|
1170
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
1171
|
-
console.log(`\n [*] Notebook store [${key}] has no saved facts.\n`);
|
|
1172
|
-
await waitForEnter();
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const file = memoryFileName(key);
|
|
1177
|
-
const factItems = factList.map((fact, idx) => {
|
|
1178
|
-
const badges = metaBadges(fact);
|
|
1179
|
-
return {
|
|
1180
|
-
label: `${idx + 1}. ${factText(fact)}`,
|
|
1181
|
-
value: idx,
|
|
1182
|
-
badge: badges.length ? badges.join(" ") : undefined,
|
|
1183
|
-
info: `Select to manage this fact from ${file}`,
|
|
1184
|
-
};
|
|
1185
|
-
});
|
|
1186
|
-
factItems.push({ label: "< Back", value: "back" });
|
|
1187
|
-
|
|
1188
|
-
const factRes = await selectSimpleMenu({
|
|
1189
|
-
title: `NOTEBOOK FACTS [${title}]`,
|
|
1190
|
-
subtitle: `Total facts: ${factList.length}`,
|
|
1191
|
-
items: factItems,
|
|
1192
|
-
});
|
|
1193
|
-
|
|
1194
|
-
if (factRes.action === "back" || factRes.value === "back") {
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const selectedIdx = factRes.value;
|
|
1199
|
-
const selectedEntry = rawEntries[selectedIdx];
|
|
1200
|
-
const selDisplay = displayFact(selectedEntry);
|
|
1201
|
-
const selBadges = metaBadges(selectedEntry);
|
|
1202
|
-
|
|
1203
|
-
const actionItems = [
|
|
1204
|
-
{ label: "[UPDATE] Edit fact text", value: "update", info: "Rewrite the fact, keeping its date and metadata" },
|
|
1205
|
-
];
|
|
1206
|
-
if (isKeepFact(selectedEntry)) {
|
|
1207
|
-
actionItems.push({ label: "[UNPROTECT] Remove keep protection", value: "unprotect", info: "Allow forget to delete it without force" });
|
|
1208
|
-
} else {
|
|
1209
|
-
actionItems.push({ label: "[PROTECT] Mark as important (keep)", value: "protect", info: "forget will skip it unless force=true" });
|
|
1210
|
-
}
|
|
1211
|
-
actionItems.push({ label: "[DELETE] Delete this fact from store", value: "delete", info: "Remove fact permanently" });
|
|
1212
|
-
actionItems.push({ label: "< Cancel / Back", value: "cancel" });
|
|
1213
|
-
|
|
1214
|
-
const actionRes = await selectSimpleMenu({
|
|
1215
|
-
title: "FACT ACTION",
|
|
1216
|
-
subtitle: `Fact: "${selDisplay}"${selBadges.length ? " [" + selBadges.join("] [") + "]" : ""}`,
|
|
1217
|
-
items: actionItems,
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
if (actionRes.action === "back" || actionRes.value === "cancel") {
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
if (actionRes.action === "select" && actionRes.value === "update") {
|
|
1225
|
-
const p = parseFactEntry(selectedEntry);
|
|
1226
|
-
const newText = await promptText(`New text for fact #${selectedIdx + 1}:`);
|
|
1227
|
-
if (!newText) continue;
|
|
1228
|
-
const newLine = formatFactEntry({ date: p.date, time: p.time, text: newText, meta: p.meta });
|
|
1229
|
-
const updated = [...rawEntries];
|
|
1230
|
-
updated[selectedIdx] = newLine;
|
|
1231
|
-
await writeMemory(key, updated);
|
|
1232
|
-
let links = 0;
|
|
1233
|
-
try {
|
|
1234
|
-
const db = await getDatabase();
|
|
1235
|
-
const runRes = await db
|
|
1236
|
-
.prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
|
|
1237
|
-
.run(newText, key, factText(selectedEntry));
|
|
1238
|
-
links = runRes ? runRes.changes : 0;
|
|
1239
|
-
} catch (e) {}
|
|
1240
|
-
console.clear();
|
|
1241
|
-
console.log(`\n [OK] Fact updated successfully${links ? `, ${links} doc link(s) updated` : ""}.\n`);
|
|
1242
|
-
await waitForEnter();
|
|
1243
|
-
} else if (actionRes.action === "select" && (actionRes.value === "protect" || actionRes.value === "unprotect")) {
|
|
1244
|
-
const updated = [...rawEntries];
|
|
1245
|
-
updated[selectedIdx] =
|
|
1246
|
-
actionRes.value === "protect" ? withMeta(selectedEntry, { keep: "1" }) : withMeta(selectedEntry, { keep: null });
|
|
1247
|
-
await writeMemory(key, updated);
|
|
1248
|
-
console.clear();
|
|
1249
|
-
console.log(`\n [OK] Fact ${actionRes.value === "protect" ? "protected" : "unprotected"} successfully.\n`);
|
|
1250
|
-
await waitForEnter();
|
|
1251
|
-
} else if (actionRes.action === "select" && actionRes.value === "delete") {
|
|
1252
|
-
const updated = [...rawEntries];
|
|
1253
|
-
updated.splice(selectedIdx, 1);
|
|
1254
|
-
await writeMemory(key, updated);
|
|
1255
|
-
console.clear();
|
|
1256
|
-
console.log("\n [OK] Fact deleted successfully.\n");
|
|
1257
|
-
await waitForEnter();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
const scopeItems = [
|
|
1263
|
-
{ label: "Global Memory", value: "global", badge: "global.md", info: "User facts stored across all projects" },
|
|
1264
|
-
{ label: `Project Memory (${projLabel})`, value: "project", badge: memoryFileName(projKey), info: `Facts bound to ${projKey}` },
|
|
1265
|
-
{ label: "Project Stores (All Projects)", value: "projects", info: "List & browse every project memory store; bind legacy stores" },
|
|
1266
|
-
{ label: "< Back to Main Menu", value: "back" },
|
|
1267
|
-
];
|
|
1268
|
-
const scopeRes = await selectSimpleMenu({
|
|
1269
|
-
title: "NOTEBOOK FACTS MANAGEMENT",
|
|
1270
|
-
subtitle: "Inspect & delete persistent user facts (Layer 1)",
|
|
1271
|
-
items: scopeItems,
|
|
1272
|
-
});
|
|
1273
|
-
|
|
1274
|
-
if (scopeRes.action === "back" || scopeRes.value === "back") {
|
|
1275
|
-
nbRunning = false;
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
if (scopeRes.value === "projects") {
|
|
1280
|
-
let stores = await listProjectStores();
|
|
1281
|
-
let storeRunning = true;
|
|
1282
|
-
while (storeRunning) {
|
|
1283
|
-
if (!stores.length) {
|
|
1284
|
-
console.clear();
|
|
1285
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1286
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1287
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mPROJECT STORES: NONE FOUND\x1b[0m${" ".repeat(PANEL_WIDTH - 32)}\x1b[36m│\x1b[0m`);
|
|
1288
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
1289
|
-
console.log("\n [*] No project memory stores found.\n");
|
|
1290
|
-
await waitForEnter();
|
|
1291
|
-
storeRunning = false;
|
|
1292
|
-
break;
|
|
1293
|
-
}
|
|
1294
|
-
const storeItems = stores.map((s) => ({
|
|
1295
|
-
label: `${s.basename} (${s.count})`,
|
|
1296
|
-
badge: s.file,
|
|
1297
|
-
hint: s.legacy ? "LEGACY" : "BOUND",
|
|
1298
|
-
info: s.path ? `Bound to: ${s.path}` : `Unbound legacy store. View facts or bind to current dir: ${projKey}`,
|
|
1299
|
-
value: s,
|
|
1300
|
-
}));
|
|
1301
|
-
storeItems.push({ label: "< Back", value: "back" });
|
|
1302
|
-
|
|
1303
|
-
const storeRes = await selectSimpleMenu({
|
|
1304
|
-
title: "PROJECT MEMORY STORES",
|
|
1305
|
-
subtitle: `Total stores: ${stores.length}`,
|
|
1306
|
-
items: storeItems,
|
|
1307
|
-
});
|
|
1308
|
-
|
|
1309
|
-
if (storeRes.action === "back" || storeRes.value === "back") {
|
|
1310
|
-
storeRunning = false;
|
|
1311
|
-
break;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const store = storeRes.value;
|
|
1315
|
-
let actionRunning = true;
|
|
1316
|
-
while (actionRunning) {
|
|
1317
|
-
const actionItems = [
|
|
1318
|
-
{ label: "View facts", value: "view", info: `Browse ${store.count} fact(s) in ${store.file}` },
|
|
1319
|
-
];
|
|
1320
|
-
if (store.legacy) {
|
|
1321
|
-
actionItems.push({
|
|
1322
|
-
label: "[MIGRATE] Bind to current directory",
|
|
1323
|
-
value: "migrate",
|
|
1324
|
-
info: `Rebind '${store.basename}' store from unbound legacy to ${projKey}`,
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
actionItems.push({ label: "< Cancel / Back", value: "cancel" });
|
|
1328
|
-
|
|
1329
|
-
const actRes = await selectSimpleMenu({
|
|
1330
|
-
title: `STORE: ${store.basename}`,
|
|
1331
|
-
subtitle: store.path || "Unbound legacy store",
|
|
1332
|
-
items: actionItems,
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
if (actRes.action === "back" || actRes.value === "cancel") {
|
|
1336
|
-
actionRunning = false;
|
|
1337
|
-
break;
|
|
1338
|
-
}
|
|
1339
|
-
if (actRes.value === "view") {
|
|
1340
|
-
await browseFacts(store.key, store.basename);
|
|
1341
|
-
} else if (actRes.value === "migrate") {
|
|
1342
|
-
const mig = await migrateLegacyStore(store.key, projKey);
|
|
1343
|
-
console.clear();
|
|
1344
|
-
if (mig.ok) {
|
|
1345
|
-
console.log(`\n [OK] Legacy store '${store.basename}' bound to ${mig.key} (${mig.facts} fact(s)) [${mig.file}]\n`);
|
|
1346
|
-
} else {
|
|
1347
|
-
console.log(`\n [*] Could not migrate: ${mig.reason}\n`);
|
|
1348
|
-
}
|
|
1349
|
-
await waitForEnter();
|
|
1350
|
-
stores = await listProjectStores();
|
|
1351
|
-
actionRunning = false;
|
|
1352
|
-
break;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
continue;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
await browseFacts(scopeRes.value === "global" ? GLOBAL_KEY : projKey, scopeRes.value === "global" ? "GLOBAL" : projLabel);
|
|
1360
|
-
}
|
|
1361
|
-
break;
|
|
1362
|
-
}
|
|
1363
|
-
case "rag_docs": {
|
|
1364
|
-
let docRunning = true;
|
|
1365
|
-
while (docRunning) {
|
|
1366
|
-
const db = await getDatabase();
|
|
1367
|
-
const docs = await db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
|
|
1368
|
-
|
|
1369
|
-
if (!docs || docs.length === 0) {
|
|
1370
|
-
console.clear();
|
|
1371
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1372
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1373
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mRAG DOCUMENTS: BASE EMPTY\x1b[0m${" ".repeat(PANEL_WIDTH - 28)}\x1b[36m│\x1b[0m`);
|
|
1374
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
1375
|
-
console.log("\n [*] RAG Knowledge Base is empty. No documents ingested.\n");
|
|
1376
|
-
await waitForEnter();
|
|
1377
|
-
docRunning = false;
|
|
1378
|
-
break;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
const docItems = docs.map((doc) => {
|
|
1382
|
-
const rawDate = doc.created_at || doc.updated_at || "";
|
|
1383
|
-
let formattedDate = "";
|
|
1384
|
-
if (rawDate) {
|
|
1385
|
-
try {
|
|
1386
|
-
const d = typeof rawDate === "number" ? new Date(rawDate) : new Date(String(rawDate));
|
|
1387
|
-
formattedDate = isNaN(d.getTime()) ? String(rawDate).substring(0, 16) : d.toISOString().replace("T", " ").substring(0, 16);
|
|
1388
|
-
} catch (e) {
|
|
1389
|
-
formattedDate = String(rawDate).substring(0, 16);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
const docIdStr = doc.id != null ? String(doc.id) : "";
|
|
1393
|
-
return {
|
|
1394
|
-
label: doc.title || doc.path || "Untitled Document",
|
|
1395
|
-
badge: formattedDate,
|
|
1396
|
-
hint: docIdStr ? `ID: ${docIdStr.substring(0, 8)}...` : "",
|
|
1397
|
-
info: `Path: ${doc.path || "N/A"}`,
|
|
1398
|
-
value: doc,
|
|
1399
|
-
};
|
|
1400
|
-
});
|
|
1401
|
-
docItems.push({ label: "< Back to Main Menu", value: "back" });
|
|
1402
|
-
|
|
1403
|
-
const docRes = await selectSimpleMenu({
|
|
1404
|
-
title: "RAG KNOWLEDGE BASE DOCUMENTS",
|
|
1405
|
-
subtitle: `Total ingested documents: ${docs.length}`,
|
|
1406
|
-
items: docItems,
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
if (docRes.action === "back" || docRes.value === "back") {
|
|
1410
|
-
docRunning = false;
|
|
1411
|
-
break;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
const targetDoc = docRes.value;
|
|
1415
|
-
const actionRes = await selectSimpleMenu({
|
|
1416
|
-
title: "DOCUMENT ACTION",
|
|
1417
|
-
subtitle: targetDoc.title || targetDoc.path,
|
|
1418
|
-
items: [
|
|
1419
|
-
{ label: "[INFO] View Details & Sections", value: "info", info: "Inspect micro-chunks and sections count" },
|
|
1420
|
-
{ label: "[EXPORT JSON] Export Full Hierarchy to Pretty JSON", value: "export_json", info: "Export multiline JSON with doc metadata & all 3 hierarchy levels" },
|
|
1421
|
-
{ label: "[DELETE] Delete Document from RAG Base", value: "delete", info: "Purge document, FTS5 index & vectors" },
|
|
1422
|
-
{ label: "< Cancel / Back", value: "cancel" },
|
|
1423
|
-
],
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
if (actionRes.action === "select" && actionRes.value === "info") {
|
|
1427
|
-
const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id);
|
|
1428
|
-
const secCount = secCountRow ? secCountRow.cnt : 0;
|
|
1429
|
-
const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id);
|
|
1430
|
-
const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
|
|
1431
|
-
const sampleSections = await db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
|
|
1432
|
-
|
|
1433
|
-
console.clear();
|
|
1434
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1435
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1436
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mDOCUMENT DETAILS\x1b[0m${" ".repeat(PANEL_WIDTH - 20)}\x1b[36m│\x1b[0m`);
|
|
1437
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
1438
|
-
console.log(` Title: ${targetDoc.title || "Untitled"}`);
|
|
1439
|
-
console.log(` ID: ${targetDoc.id}`);
|
|
1440
|
-
console.log(` Path: ${targetDoc.path || "N/A"}`);
|
|
1441
|
-
console.log(` Created: ${targetDoc.created_at}`);
|
|
1442
|
-
console.log(` Sections Count: ${secCount}`);
|
|
1443
|
-
console.log(` Micro-Chunks: ${chunkCount}`);
|
|
1444
|
-
if (sampleSections.length > 0) {
|
|
1445
|
-
console.log("\n Sample Section Headings:");
|
|
1446
|
-
sampleSections.forEach((s, idx) => console.log(` ${idx + 1}. ${s.heading || "Untitled Section"}`));
|
|
1447
|
-
}
|
|
1448
|
-
console.log("\n");
|
|
1449
|
-
await waitForEnter();
|
|
1450
|
-
} else if (actionRes.action === "select" && actionRes.value === "export_json") {
|
|
1451
|
-
const { exportDocumentToFile } = await import("./ingest/exporter.js");
|
|
1452
|
-
const outFile = exportDocumentToFile(targetDoc.id, null, db);
|
|
1453
|
-
console.clear();
|
|
1454
|
-
console.log(`\n \x1b[32m[OK] Full document JSON exported to:\x1b[0m`);
|
|
1455
|
-
console.log(` \x1b[36m${outFile}\x1b[0m\n`);
|
|
1456
|
-
await waitForEnter();
|
|
1457
|
-
} else if (actionRes.action === "select" && actionRes.value === "delete") {
|
|
1458
|
-
await deleteDocument(targetDoc.id, db);
|
|
1459
|
-
console.clear();
|
|
1460
|
-
console.log(`\n [OK] Document "${targetDoc.title || targetDoc.path}" deleted from RAG base.\n`);
|
|
1461
|
-
await waitForEnter();
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
case "export_snapshot": {
|
|
1467
|
-
const { exportSnapshot } = await import("./admin/snapshot.js");
|
|
1468
|
-
const { MEMORY_DIR } = await import("./memory.js");
|
|
1469
|
-
const defaultPath = join(MEMORY_DIR, "exports", `rag_snapshot_${Date.now()}.json.gz`);
|
|
1470
|
-
const pathRes = await readTextInput("Enter Output Snapshot Path (.json or .json.gz)", defaultPath);
|
|
1471
|
-
if (pathRes.action === "submit" && pathRes.value) {
|
|
1472
|
-
console.clear();
|
|
1473
|
-
console.log(`\n [EXPORT] Exporting full snapshot to: \x1b[36m${pathRes.value}\x1b[0m...\n`);
|
|
1474
|
-
try {
|
|
1475
|
-
const res = await exportSnapshot({ outputPath: pathRes.value });
|
|
1476
|
-
console.log(` \x1b[32m[OK] Snapshot exported successfully!\x1b[0m`);
|
|
1477
|
-
console.log(` Documents: ${res.snapshot.documents ? res.snapshot.documents.length : 0}`);
|
|
1478
|
-
console.log(` Micro-Chunks: ${res.snapshot.micro_chunks ? res.snapshot.micro_chunks.length : 0}`);
|
|
1479
|
-
console.log(` Blobs: ${res.snapshot.blobs ? res.snapshot.blobs.length : 0}`);
|
|
1480
|
-
console.log(` Output: ${res.outputPath}\n`);
|
|
1481
|
-
} catch (err) {
|
|
1482
|
-
console.error(` \x1b[31m[ERROR] Snapshot export failed: ${err.message}\x1b[0m\n`);
|
|
1483
|
-
}
|
|
1484
|
-
await waitForEnter();
|
|
1485
|
-
}
|
|
1486
|
-
break;
|
|
1487
|
-
}
|
|
1488
|
-
case "import_snapshot": {
|
|
1489
|
-
const { importSnapshot, listAvailableSnapshots } = await import("./admin/snapshot.js");
|
|
1490
|
-
const availableSnapshots = listAvailableSnapshots();
|
|
1491
|
-
|
|
1492
|
-
let chosenPath = null;
|
|
1493
|
-
|
|
1494
|
-
if (availableSnapshots.length > 0) {
|
|
1495
|
-
const menuItems = availableSnapshots.map((s) => ({
|
|
1496
|
-
label: s.name,
|
|
1497
|
-
badge: `${s.sizeMB} MB`,
|
|
1498
|
-
hint: s.dateStr,
|
|
1499
|
-
info: `Path: ${s.path}`,
|
|
1500
|
-
value: s.path,
|
|
1501
|
-
}));
|
|
1502
|
-
|
|
1503
|
-
menuItems.push({
|
|
1504
|
-
label: "[MANUAL ENTRY] Enter Custom Snapshot File Path...",
|
|
1505
|
-
value: "manual",
|
|
1506
|
-
info: "Type or paste an absolute file path to a .json or .json.gz snapshot file",
|
|
1507
|
-
});
|
|
1508
|
-
menuItems.push({ label: "< Cancel / Back", value: "back" });
|
|
1509
|
-
|
|
1510
|
-
const subRes = await selectSimpleMenu({
|
|
1511
|
-
title: "SELECT SNAPSHOT FOR IMPORT",
|
|
1512
|
-
subtitle: `Found ${availableSnapshots.length} snapshot files in exports directory`,
|
|
1513
|
-
items: menuItems,
|
|
1514
|
-
});
|
|
1515
|
-
|
|
1516
|
-
if (subRes.action === "back" || subRes.value === "back") {
|
|
1517
|
-
break;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
if (subRes.value === "manual") {
|
|
1521
|
-
const inputRes = await readTextInput("Enter Input Snapshot Path (.json or .json.gz)");
|
|
1522
|
-
if (inputRes.action === "submit" && inputRes.value) {
|
|
1523
|
-
chosenPath = inputRes.value;
|
|
1524
|
-
} else {
|
|
1525
|
-
break;
|
|
1526
|
-
}
|
|
1527
|
-
} else {
|
|
1528
|
-
chosenPath = subRes.value;
|
|
1529
|
-
}
|
|
1530
|
-
} else {
|
|
1531
|
-
const inputRes = await readTextInput("Enter Input Snapshot Path (.json or .json.gz)");
|
|
1532
|
-
if (inputRes.action === "submit" && inputRes.value) {
|
|
1533
|
-
chosenPath = inputRes.value;
|
|
1534
|
-
} else {
|
|
1535
|
-
break;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
if (chosenPath) {
|
|
1540
|
-
console.clear();
|
|
1541
|
-
console.log(`\n [IMPORT] Importing snapshot from: \x1b[36m${chosenPath}\x1b[0m...\n`);
|
|
1542
|
-
try {
|
|
1543
|
-
const res = await importSnapshot({ snapshotPathOrData: chosenPath });
|
|
1544
|
-
console.log(` \x1b[32m[OK] Snapshot imported successfully!\x1b[0m`);
|
|
1545
|
-
console.log(` Documents: ${res.documents}`);
|
|
1546
|
-
console.log(` Sections: ${res.sections}`);
|
|
1547
|
-
console.log(` Medium-Chunks:${res.medium_chunks}`);
|
|
1548
|
-
console.log(` Micro-Chunks: ${res.micro_chunks}`);
|
|
1549
|
-
console.log(` Blobs: ${res.blobs}\n`);
|
|
1550
|
-
} catch (err) {
|
|
1551
|
-
console.error(` \x1b[31m[ERROR] Snapshot import failed: ${err.message}\x1b[0m\n`);
|
|
1552
|
-
}
|
|
1553
|
-
await waitForEnter();
|
|
1554
|
-
}
|
|
1555
|
-
break;
|
|
1556
|
-
}
|
|
1557
|
-
case "hard_reset": {
|
|
1558
|
-
const confirmRes = await selectSimpleMenu({
|
|
1559
|
-
title: "HARD RESET DATABASE & BLOB STORAGE",
|
|
1560
|
-
subtitle: `Permanently purge all ${stats.docCount} docs, ${stats.chunkCount} chunks & blobs`,
|
|
1561
|
-
items: [
|
|
1562
|
-
{
|
|
1563
|
-
label: "[CONFIRM HARD RESET] Purge All Documents, Vectors & Blobs",
|
|
1564
|
-
value: "confirm",
|
|
1565
|
-
info: "WARNING: Irreversible deletion of all SQLite documents, micro-chunks, and CAS blobs!",
|
|
1566
|
-
},
|
|
1567
|
-
{ label: "< Cancel / Back", value: "cancel" },
|
|
1568
|
-
],
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
if (confirmRes.action === "select" && confirmRes.value === "confirm") {
|
|
1572
|
-
const { hardResetDatabase } = await import("./admin/snapshot.js");
|
|
1573
|
-
const res = hardResetDatabase();
|
|
1574
|
-
console.clear();
|
|
1575
|
-
console.log(`\n \x1b[32m[OK] HARD RESET COMPLETED SUCCESSFULLY!\x1b[0m`);
|
|
1576
|
-
console.log(` Purged Documents: ${res.purgedDocuments}`);
|
|
1577
|
-
console.log(` Purged Chunks: ${res.purgedChunks}`);
|
|
1578
|
-
console.log(` Purged Blobs: ${res.purgedBlobs}\n`);
|
|
1579
|
-
await waitForEnter();
|
|
1580
|
-
}
|
|
1581
|
-
break;
|
|
1582
|
-
}
|
|
1583
|
-
case "manage_models": {
|
|
1584
|
-
let modelMgmtRunning = true;
|
|
1585
|
-
while (modelMgmtRunning) {
|
|
1586
|
-
const allPresets = [...new Set([...EMBEDDING_PRESETS, ...RERANKER_PRESETS.filter((r) => r !== "none")])];
|
|
1587
|
-
const cachedOnDisk = listAllCachedModels();
|
|
1588
|
-
const diskModelNames = cachedOnDisk.map((m) => m.modelName);
|
|
1589
|
-
|
|
1590
|
-
const combinedModels = [...new Set([...allPresets, ...diskModelNames])];
|
|
1591
|
-
|
|
1592
|
-
let totalDiskBytes = 0;
|
|
1593
|
-
const modelItems = combinedModels.map((m) => {
|
|
1594
|
-
const info = getModelStorageInfo(m);
|
|
1595
|
-
totalDiskBytes += info.bytes;
|
|
1596
|
-
let badge = "NOT DOWNLOADED";
|
|
1597
|
-
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
1598
|
-
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
1599
|
-
|
|
1600
|
-
return {
|
|
1601
|
-
label: m,
|
|
1602
|
-
badge,
|
|
1603
|
-
value: m,
|
|
1604
|
-
info: info.status !== "not_downloaded"
|
|
1605
|
-
? `Size: ${info.sizeMB} MB | Select to inspect or delete from disk`
|
|
1606
|
-
: "Model weights not present on local disk",
|
|
1607
|
-
};
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
modelItems.push({ label: "< Back to Main Menu", value: "back" });
|
|
1611
|
-
|
|
1612
|
-
const totalDiskMB = (totalDiskBytes / (1024 * 1024)).toFixed(2);
|
|
1613
|
-
const subRes = await selectSimpleMenu({
|
|
1614
|
-
title: "ML MODEL CACHE MANAGEMENT",
|
|
1615
|
-
subtitle: `Total ML Storage Used: ${totalDiskMB} MB | Models Tracked: ${combinedModels.length}`,
|
|
1616
|
-
items: modelItems,
|
|
1617
|
-
});
|
|
1618
|
-
|
|
1619
|
-
if (subRes.action === "back" || subRes.value === "back") {
|
|
1620
|
-
modelMgmtRunning = false;
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
const selectedModel = subRes.value;
|
|
1625
|
-
const selectedInfo = getModelStorageInfo(selectedModel);
|
|
1626
|
-
|
|
1627
|
-
if (selectedInfo.status === "not_downloaded") {
|
|
1628
|
-
console.clear();
|
|
1629
|
-
console.log(`\n [*] Model "${selectedModel}" is not downloaded on local disk.\n`);
|
|
1630
|
-
await waitForEnter();
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const actionRes = await selectSimpleMenu({
|
|
1635
|
-
title: `MODEL ACTION: ${selectedModel}`,
|
|
1636
|
-
subtitle: `Status: ${selectedInfo.status.toUpperCase()} | Size: ${selectedInfo.sizeMB} MB`,
|
|
1637
|
-
items: [
|
|
1638
|
-
{ label: `[PURGE] Delete model weights from disk (${selectedInfo.sizeMB} MB)`, value: "delete", info: `Delete ${selectedInfo.dir} permanently` },
|
|
1639
|
-
{ label: "< Cancel / Back", value: "cancel" },
|
|
1640
|
-
],
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
if (actionRes.action === "select" && actionRes.value === "delete") {
|
|
1644
|
-
const delRes = deleteModelCache(selectedModel);
|
|
1645
|
-
console.clear();
|
|
1646
|
-
if (delRes.deleted) {
|
|
1647
|
-
console.log(`\n \x1b[32m[OK] Model "${selectedModel}" deleted successfully (${delRes.freedMB} MB freed).\x1b[0m\n`);
|
|
1648
|
-
} else {
|
|
1649
|
-
console.error(`\n \x1b[31m[ERROR] Failed to delete model: ${delRes.reason}\x1b[0m\n`);
|
|
1650
|
-
}
|
|
1651
|
-
await waitForEnter();
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
case "benchmark": {
|
|
1657
|
-
const modeRes = await selectSimpleMenu({
|
|
1658
|
-
title: "BENCHMARK MODE",
|
|
1659
|
-
subtitle: "Choose smoke (fast iteration) vs full (statistical rigor)",
|
|
1660
|
-
items: [
|
|
1661
|
-
{
|
|
1662
|
-
label: "Quick Smoke (~7s, 9 queries on 6 docs)",
|
|
1663
|
-
value: "smoke",
|
|
1664
|
-
info: `Subset: ${SMOKE_DOC_IDS.join(", ")}. Skips bootstrap/grid/t-tests for fast dev iteration loop.`,
|
|
1665
|
-
},
|
|
1666
|
-
{
|
|
1667
|
-
label: "Full Benchmark (~32s, 21 queries on all 28 docs)",
|
|
1668
|
-
value: "full",
|
|
1669
|
-
info: "Full 28-doc corpus, per-query answer token metrics, bootstrap CIs, grid sweep. Writes dev_docs/benchmark_results.md.",
|
|
1670
|
-
},
|
|
1671
|
-
{
|
|
1672
|
-
label: "[GPU PROFILER] GPU Inference Bottleneck Trace",
|
|
1673
|
-
value: "gpu_profile",
|
|
1674
|
-
info: "Profile GPU DirectML tensor execution stages, kernel launch overhead & VRAM throughput.",
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
label: "[CPU vs GPU] Dual-Run Comparison Benchmark",
|
|
1678
|
-
value: "cpu_vs_gpu",
|
|
1679
|
-
info: "Run identical workload on CPU then GPU and compare throughput, latency & speedup.",
|
|
1680
|
-
},
|
|
1681
|
-
{
|
|
1682
|
-
label: "Graph & Notebook Linking Verification (Layer 1+3 Agent Graph Links)",
|
|
1683
|
-
value: "graph_test",
|
|
1684
|
-
info: "Ingest sample doc + save Notebook fact linked to line range + verify recall & raw document reader.",
|
|
1685
|
-
},
|
|
1686
|
-
{ label: "< Back to Main Menu", value: "back" },
|
|
1687
|
-
],
|
|
1688
|
-
});
|
|
1689
|
-
|
|
1690
|
-
if (modeRes.action === "back" || modeRes.value === "back") {
|
|
1691
|
-
break;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
if (modeRes.value === "graph_test") {
|
|
1695
|
-
console.clear();
|
|
1696
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1697
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1698
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mGRAPH & NOTEBOOK LINKING VERIFICATION\x1b[0m${" ".repeat(PANEL_WIDTH - 42)}\x1b[36m│\x1b[0m`);
|
|
1699
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1700
|
-
|
|
1701
|
-
const sampleDoc = `# Ода о единороге (Секретный проект Unicorn)
|
|
1702
|
-
|
|
1703
|
-
## Раздел 1: Введение
|
|
1704
|
-
Разработка нового высоконагруженного сервиса Unicorn.
|
|
1705
|
-
|
|
1706
|
-
## Раздел 2: Стандарты
|
|
1707
|
-
Строка 7: Бэкенд пишется исключительно на Go.
|
|
1708
|
-
Строка 8: Хранилище транзакций — PostgreSQL 16.
|
|
1709
|
-
`;
|
|
1710
|
-
|
|
1711
|
-
const { ingestDocument } = await import("./ingest/pipeline.js");
|
|
1712
|
-
const { linkFactToDocument, getLinksForFact } = await import("./graph/knowledge_linker.js");
|
|
1713
|
-
const { readMemoryRaw, writeMemory, scopeKey } = await import("./memory.js");
|
|
1714
|
-
|
|
1715
|
-
console.log(" 1. Ingesting test document 'Ода о единороге'...");
|
|
1716
|
-
const ingRes = await ingestDocument({
|
|
1717
|
-
content: sampleDoc,
|
|
1718
|
-
type: "text",
|
|
1719
|
-
title: "Ода о единороге",
|
|
1720
|
-
path: "virtual://oda_unicorna.md",
|
|
1721
|
-
generateEmbeddings: false,
|
|
1722
|
-
});
|
|
1723
|
-
console.log(` [OK] Document ingested. Doc ID: ${ingRes.docId}`);
|
|
1724
|
-
|
|
1725
|
-
console.log("\n 2. Saving Notebook fact & linking to lines L7-L8...");
|
|
1726
|
-
const factText = "Project Unicorn backend services must use Go with PostgreSQL 16";
|
|
1727
|
-
const factKey = scopeKey("project", "cli_test_repo", null);
|
|
1728
|
-
|
|
1729
|
-
const entries = await readMemoryRaw(factKey);
|
|
1730
|
-
entries.push(`[2026-07-30] ${factText}`);
|
|
1731
|
-
await writeMemory(factKey, entries);
|
|
1732
|
-
|
|
1733
|
-
const linkRes = linkFactToDocument({
|
|
1734
|
-
factKey,
|
|
1735
|
-
factText,
|
|
1736
|
-
docId: ingRes.docId,
|
|
1737
|
-
startLine: 7,
|
|
1738
|
-
endLine: 8,
|
|
1739
|
-
relationType: "RULES_FOR",
|
|
1740
|
-
});
|
|
1741
|
-
console.log(` [OK] Graph Edge created. Link ID: ${linkRes.linkId} -> L7-L8`);
|
|
1742
|
-
|
|
1743
|
-
console.log("\n 3. Recalling memory (Verifying Graph Document Tag)...");
|
|
1744
|
-
const rawFacts = await readMemoryRaw(factKey);
|
|
1745
|
-
rawFacts.forEach((f, i) => {
|
|
1746
|
-
const links = getLinksForFact(factKey, f);
|
|
1747
|
-
let lStr = ` ${i + 1}. ${f}`;
|
|
1748
|
-
if (links && links.length > 0) {
|
|
1749
|
-
const docStr = links.map(l => `${l.doc_title || l.doc_path}:L${l.start_line}-${l.end_line}`).join(", ");
|
|
1750
|
-
lStr += ` \x1b[36m🔗 [Linked Docs: ${docStr}]\x1b[0m`;
|
|
1751
|
-
}
|
|
1752
|
-
console.log(lStr);
|
|
1753
|
-
});
|
|
1754
|
-
|
|
1755
|
-
console.log("\n \x1b[32m[OK] AGENT-DRIVEN GRAPH LINKING VERIFIED SUCCESSFULLY!\x1b[0m\n");
|
|
1756
|
-
await waitForEnter();
|
|
1757
|
-
break;
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
if (modeRes.value === "gpu_profile") {
|
|
1761
|
-
console.clear();
|
|
1762
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1763
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1764
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mGPU PROFILER BENCHMARK\x1b[0m${" ".repeat(PANEL_WIDTH - 28)}\x1b[36m│\x1b[0m`);
|
|
1765
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90m${"Profiling DirectML tensor execution stages & VRAM throughput".padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1766
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1767
|
-
|
|
1768
|
-
const savedConfig = getConfig();
|
|
1769
|
-
try {
|
|
1770
|
-
const { runGpuProfileBenchmark } = await import("./benchmarks/gpu_profile_benchmark.js");
|
|
1771
|
-
await runGpuProfileBenchmark({
|
|
1772
|
-
modelName: savedConfig.embeddingModel,
|
|
1773
|
-
batchSize: savedConfig.batchSize || 32,
|
|
1774
|
-
totalItems: 512,
|
|
1775
|
-
});
|
|
1776
|
-
} catch (err) {
|
|
1777
|
-
console.error(` \x1b[31m[ERROR] GPU Profile benchmark failed: ${err.message}\x1b[0m\n`);
|
|
1778
|
-
}
|
|
1779
|
-
// Restore original device config
|
|
1780
|
-
updateConfig({ executionDevice: savedConfig.executionDevice });
|
|
1781
|
-
await waitForEnter();
|
|
1782
|
-
break;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
if (modeRes.value === "cpu_vs_gpu") {
|
|
1786
|
-
console.clear();
|
|
1787
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1788
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1789
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mCPU vs GPU COMPARISON BENCHMARK\x1b[0m${" ".repeat(PANEL_WIDTH - 37)}\x1b[36m│\x1b[0m`);
|
|
1790
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90m${"Identical workload on CPU then GPU — automatic device switching".padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1791
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1792
|
-
|
|
1793
|
-
const savedConfig = getConfig();
|
|
1794
|
-
try {
|
|
1795
|
-
const { runCpuVsGpuComparison } = await import("./benchmarks/gpu_profile_benchmark.js");
|
|
1796
|
-
await runCpuVsGpuComparison({
|
|
1797
|
-
modelName: savedConfig.embeddingModel,
|
|
1798
|
-
batchSize: savedConfig.batchSize || 32,
|
|
1799
|
-
totalItems: 512,
|
|
1800
|
-
});
|
|
1801
|
-
} catch (err) {
|
|
1802
|
-
console.error(` \x1b[31m[ERROR] CPU vs GPU benchmark failed: ${err.message}\x1b[0m\n`);
|
|
1803
|
-
}
|
|
1804
|
-
// Restore original device config
|
|
1805
|
-
updateConfig({ executionDevice: savedConfig.executionDevice });
|
|
1806
|
-
await waitForEnter();
|
|
1807
|
-
break;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
const isSmoke = modeRes.value === "smoke";
|
|
1811
|
-
console.clear();
|
|
1812
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1813
|
-
const modeTitle = isSmoke ? "SMOKE BENCHMARK IN PROGRESS" : "BENCHMARK IN PROGRESS";
|
|
1814
|
-
const modeSub = isSmoke ? "Fetch 6 docs + Ingest + Eval 9 queries (stats skipped)" : "Fetch Corpus + Ingest + Evaluate 21 Queries";
|
|
1815
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1816
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37m${modeTitle.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1817
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90m${modeSub.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1818
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1819
|
-
|
|
1820
|
-
const spinFrames = ["|", "/", "-", "\\"];
|
|
1821
|
-
let spinIdx = 0;
|
|
1822
|
-
|
|
1823
|
-
function onProgress({ phase, current, total }) {
|
|
1824
|
-
spinIdx = (spinIdx + 1) % spinFrames.length;
|
|
1825
|
-
const spin = spinFrames[spinIdx];
|
|
1826
|
-
const pct = Math.round((current / total) * 100);
|
|
1827
|
-
const bar = "=".repeat(Math.round(pct / 5)).padEnd(20);
|
|
1828
|
-
let label = "";
|
|
1829
|
-
if (phase === "fetch") label = `Fetching corpus ${current}/${total}`;
|
|
1830
|
-
if (phase === "ingest") label = `Ingesting documents ${current}/${total}`;
|
|
1831
|
-
if (phase === "evaluate") label = `Evaluating queries ${current}/${total}`;
|
|
1832
|
-
process.stdout.write(`\r ${spin} [${bar}] ${pct}% ${label} `);
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
try {
|
|
1836
|
-
const { evaluateSearchQualityComparison } = await import("./benchmarks/quality_evaluator.js");
|
|
1837
|
-
const { runIngestionBenchmark } = await import("./benchmarks/stress_ingestion.js");
|
|
1838
|
-
const ingestOpts = isSmoke
|
|
1839
|
-
? { generateEmbeddings: true, silent: true, onProgress, subsetDocIds: SMOKE_DOC_IDS }
|
|
1840
|
-
: { generateEmbeddings: true, silent: true, onProgress };
|
|
1841
|
-
const ingestRes = await runIngestionBenchmark(ingestOpts);
|
|
1842
|
-
|
|
1843
|
-
const evalOpts = isSmoke ? { silent: true, onProgress, mode: "smoke" } : { silent: true, onProgress };
|
|
1844
|
-
const qualityComp = await evaluateSearchQualityComparison(ingestRes.dbInstance, evalOpts);
|
|
1845
|
-
|
|
1846
|
-
try { ingestRes.dbInstance.close(); } catch (e) {}
|
|
1847
|
-
|
|
1848
|
-
console.clear();
|
|
1849
|
-
renderBenchmarkResultsTable(qualityComp);
|
|
1850
|
-
} catch (err) {
|
|
1851
|
-
process.stdout.write("\n");
|
|
1852
|
-
console.error(" [ERROR] Benchmark execution failed:", err.message);
|
|
1853
|
-
}
|
|
1854
|
-
await waitForEnter();
|
|
1855
|
-
break;
|
|
1856
|
-
}
|
|
1857
|
-
case "test": {
|
|
1858
|
-
const queryRes = await readTextInput("Enter Test Verification Query", "sqlite compact database");
|
|
1859
|
-
if (queryRes.action === "submit" && queryRes.value) {
|
|
1860
|
-
console.clear();
|
|
1861
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1862
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1863
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mSEARCH QUERY EXECUTION\x1b[0m${" ".repeat(PANEL_WIDTH - 26)}\x1b[36m│\x1b[0m`);
|
|
1864
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
1865
|
-
console.log(`\n [SEARCH] Executing query: "\x1b[36m${queryRes.value}\x1b[0m"...\n`);
|
|
1866
|
-
try {
|
|
1867
|
-
const results = await hybridQuery({ query: queryRes.value, limit: 3 });
|
|
1868
|
-
if (!results || results.length === 0) {
|
|
1869
|
-
console.log(" [*] No matching results found in knowledge base.");
|
|
1870
|
-
} else {
|
|
1871
|
-
results.forEach((r, i) => {
|
|
1872
|
-
console.log(`\n \x1b[36m╭─ [Hit #${i + 1}] ${r.doc_title || "Doc"} > ${r.breadcrumbs || ""} ─╮\x1b[0m`);
|
|
1873
|
-
console.log(` \x1b[36m│\x1b[0m Score: \x1b[33m${r.score}\x1b[0m (RSF: ${r.rsf_score}, RRF: ${r.rrf_score}, CosSim: ${r.cosine_sim})`);
|
|
1874
|
-
console.log(` \x1b[36m│\x1b[0m Snippet: \x1b[90m${r.snippet ? r.snippet.substring(0, 100).replace(/\n/g, " ") : ""}...\x1b[0m`);
|
|
1875
|
-
console.log(` \x1b[36m╰${"─".repeat(56)}╯\x1b[0m`);
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
} catch (err) {
|
|
1879
|
-
console.error(" [ERROR] Query execution failed:", err.message);
|
|
1880
|
-
}
|
|
1881
|
-
await waitForEnter();
|
|
1882
|
-
}
|
|
1883
|
-
break;
|
|
1884
|
-
}
|
|
1885
|
-
case "clear_cache": {
|
|
1886
|
-
const cacheSize = await getCorpusCacheSize();
|
|
1887
|
-
if (cacheSize === 0) {
|
|
1888
|
-
console.clear();
|
|
1889
|
-
console.log("\n [*] Benchmark corpus cache is already empty.\n");
|
|
1890
|
-
await waitForEnter();
|
|
1891
|
-
} else {
|
|
1892
|
-
const sizeMB = (cacheSize / (1024 * 1024)).toFixed(2);
|
|
1893
|
-
const confirmRes = await selectSimpleMenu({
|
|
1894
|
-
title: "CLEAR BENCHMARK CACHE",
|
|
1895
|
-
subtitle: `Cache size: ${sizeMB} MB`,
|
|
1896
|
-
items: [
|
|
1897
|
-
{ label: "[DELETE] Delete all cached corpus files", value: "confirm", info: `Remove ${sizeMB} MB of cached GitHub README files` },
|
|
1898
|
-
{ label: "< Cancel / Back", value: "cancel" },
|
|
1899
|
-
],
|
|
1900
|
-
});
|
|
1901
|
-
if (confirmRes.action === "select" && confirmRes.value === "confirm") {
|
|
1902
|
-
await clearCorpusCache();
|
|
1903
|
-
console.clear();
|
|
1904
|
-
console.log(`\n [OK] Benchmark corpus cache cleared (${sizeMB} MB freed).\n`);
|
|
1905
|
-
await waitForEnter();
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
break;
|
|
1909
|
-
}
|
|
1910
|
-
case "cloud_login": {
|
|
1911
|
-
console.clear();
|
|
1912
|
-
console.log("\n [CLOUD] Turso cloud authorization\n");
|
|
1913
|
-
const methodItems = [
|
|
1914
|
-
{ label: "Browser OAuth (GUI)", value: "browser", info: "Opens the system browser for the loopback OAuth flow (requires a desktop session)" },
|
|
1915
|
-
{ label: "Account API Token", value: "api_token", info: "Paste a Turso account API token — works headless (Docker, Google Jules, VPS/VDS)" },
|
|
1916
|
-
{ label: "Database URL + Token", value: "db_token", info: "Paste a libsql:// endpoint and its database auth token — no Platform API needed" },
|
|
1917
|
-
{ 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" },
|
|
1918
|
-
{ label: "< Cancel", value: "cancel", info: "Return to the main menu" },
|
|
1919
|
-
];
|
|
1920
|
-
const methodRes = await selectSimpleMenu({
|
|
1921
|
-
title: "CHOOSE LOGIN METHOD",
|
|
1922
|
-
subtitle: "Browser login needs a GUI. Token / env methods work in Docker, Google Jules and on VPS/VDS.",
|
|
1923
|
-
items: methodItems,
|
|
1924
|
-
});
|
|
1925
|
-
if (methodRes.action !== "select" || methodRes.value === "cancel") break;
|
|
1926
|
-
|
|
1927
|
-
const { loginToCloud, loginWithApiToken, loginWithDatabaseToken, loginFromEnv } = await import("./admin/auth.js");
|
|
1928
|
-
try {
|
|
1929
|
-
let secrets;
|
|
1930
|
-
if (methodRes.value === "browser") {
|
|
1931
|
-
secrets = await loginToCloud();
|
|
1932
|
-
} else if (methodRes.value === "api_token") {
|
|
1933
|
-
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`)");
|
|
1934
|
-
if (!token) throw new Error("Empty API token.");
|
|
1935
|
-
secrets = await loginWithApiToken({ token });
|
|
1936
|
-
} else if (methodRes.value === "db_token") {
|
|
1937
|
-
const dbUrl = await promptText("Paste your database URL (libsql://<database>-<org>.turso.io)");
|
|
1938
|
-
const token = await promptText("Paste your database auth token");
|
|
1939
|
-
if (!dbUrl || !token) throw new Error("Empty URL or token.");
|
|
1940
|
-
secrets = await loginWithDatabaseToken({ dbUrl, token, validate: false });
|
|
1941
|
-
} else if (methodRes.value === "env") {
|
|
1942
|
-
const res = await loginFromEnv({ persist: true });
|
|
1943
|
-
if (!res.ok) throw new Error(res.reason);
|
|
1944
|
-
secrets = res.secrets;
|
|
1945
|
-
}
|
|
1946
|
-
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
|
|
1947
|
-
} catch (e) {
|
|
1948
|
-
console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
|
|
1949
|
-
}
|
|
1950
|
-
await waitForEnter();
|
|
1951
|
-
break;
|
|
1952
|
-
}
|
|
1953
|
-
case "cloud_logout": {
|
|
1954
|
-
console.clear();
|
|
1955
|
-
console.log("\n [CLOUD] Signing out of the cloud...");
|
|
1956
|
-
const { logoutFromCloud } = await import("./admin/auth.js");
|
|
1957
|
-
const deleted = logoutFromCloud();
|
|
1958
|
-
if (deleted) {
|
|
1959
|
-
console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
|
|
1960
|
-
} else {
|
|
1961
|
-
console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
|
|
1962
|
-
}
|
|
1963
|
-
await waitForEnter();
|
|
1964
|
-
break;
|
|
1965
|
-
}
|
|
1966
|
-
case "cloud_api_set": {
|
|
1967
|
-
console.clear();
|
|
1968
|
-
console.log("\n [API KEY] Set / replace the Turso account API token\n");
|
|
1969
|
-
const { setApiKey } = await import("./admin/auth.js");
|
|
1970
|
-
try {
|
|
1971
|
-
const token = await promptText(
|
|
1972
|
-
"Paste your Turso account API token\n (create one at https://console.turso.tech or via `turso auth api-tokens create`)"
|
|
1973
|
-
);
|
|
1974
|
-
if (!token) throw new Error("Empty API token.");
|
|
1975
|
-
const res = await setApiKey(token);
|
|
1976
|
-
console.log(
|
|
1977
|
-
`\n \x1b[32m[OK] API token stored. Authorized as "${res.secrets.username}" — endpoint: ${res.secrets.dbUrl}\x1b[0m\n`
|
|
1978
|
-
);
|
|
1979
|
-
} catch (e) {
|
|
1980
|
-
console.error(`\n \x1b[31m[ERROR] Failed to set API key: ${e.message}\x1b[0m\n`);
|
|
1981
|
-
}
|
|
1982
|
-
await waitForEnter();
|
|
1983
|
-
break;
|
|
1984
|
-
}
|
|
1985
|
-
case "cloud_api_clear": {
|
|
1986
|
-
console.clear();
|
|
1987
|
-
console.log("\n [API KEY] Removing the stored account API token...");
|
|
1988
|
-
const { clearApiKey } = await import("./admin/auth.js");
|
|
1989
|
-
const res = clearApiKey();
|
|
1990
|
-
if (res.removed) {
|
|
1991
|
-
console.log(
|
|
1992
|
-
res.keptDbSession
|
|
1993
|
-
? " \x1b[32m[OK] API token removed. The resolved database session is kept and stays authorized.\x1b[0m\n"
|
|
1994
|
-
: " \x1b[32m[OK] API token removed. Encrypted secrets purged.\x1b[0m\n"
|
|
1995
|
-
);
|
|
1996
|
-
} else {
|
|
1997
|
-
console.log(" [*] No stored API token to remove.\x1b[0m\n");
|
|
1998
|
-
}
|
|
1999
|
-
await waitForEnter();
|
|
2000
|
-
break;
|
|
2001
|
-
}
|
|
2002
|
-
case "cloud_mode": {
|
|
2003
|
-
const modeItems = [
|
|
2004
|
-
{ label: "only-local (Local only)", value: "only-local", info: "Fully private, offline-first mode (everything stored on disk)" },
|
|
2005
|
-
{ label: "only-cloud (Cloud only)", value: "only-cloud", info: "Fully serverless cloud mode with no local caching" },
|
|
2006
|
-
{ label: "hybrid-sync (Local with background sync)", value: "hybrid-sync", info: "Instant local operations with a background sync daemon" },
|
|
2007
|
-
];
|
|
2008
|
-
const initialIdx = Math.max(0, modeItems.findIndex((i) => i.value === config.mode));
|
|
2009
|
-
const subRes = await selectSimpleMenu({
|
|
2010
|
-
title: "CHOOSE OPERATIONAL MODE",
|
|
2011
|
-
subtitle: "Configure database storage and cloud sync behavior",
|
|
2012
|
-
items: modeItems,
|
|
2013
|
-
initialIndex: initialIdx,
|
|
2014
|
-
});
|
|
2015
|
-
|
|
2016
|
-
if (subRes.action === "select") {
|
|
2017
|
-
updateConfig({ mode: subRes.value });
|
|
2018
|
-
}
|
|
2019
|
-
break;
|
|
2020
|
-
}
|
|
2021
|
-
case "conflict_strategy": {
|
|
2022
|
-
const strategyItems = [
|
|
2023
|
-
{ label: "merge (Union local + cloud)", value: "merge", info: "Facts from both sides are merged and deduplicated — no data loss (recommended)" },
|
|
2024
|
-
{ label: "cloud-wins (Cloud overwrites local)", value: "cloud-wins", info: "On conflict, the cloud copy replaces the local store" },
|
|
2025
|
-
{ label: "local-wins (Local overwrites cloud)", value: "local-wins", info: "On conflict, the local copy replaces the cloud store" },
|
|
2026
|
-
];
|
|
2027
|
-
const initialIdx = Math.max(0, strategyItems.findIndex((i) => i.value === (config.conflictStrategy || "merge")));
|
|
2028
|
-
const subRes = await selectSimpleMenu({
|
|
2029
|
-
title: "CHOOSE CONFLICT STRATEGY",
|
|
2030
|
-
subtitle: "How hybrid-sync resolves differing local vs cloud stores",
|
|
2031
|
-
items: strategyItems,
|
|
2032
|
-
initialIndex: initialIdx,
|
|
2033
|
-
});
|
|
2034
|
-
|
|
2035
|
-
if (subRes.action === "select") {
|
|
2036
|
-
updateConfig({ conflictStrategy: subRes.value });
|
|
2037
|
-
console.log(`\n [OK] Conflict strategy set to: ${subRes.value}`);
|
|
2038
|
-
}
|
|
2039
|
-
break;
|
|
2040
|
-
}
|
|
2041
|
-
case "enable_prompt": {
|
|
2042
|
-
const { enableGlobalPrompt } = await import("./prompt_manager.js");
|
|
2043
|
-
const results = await enableGlobalPrompt();
|
|
2044
|
-
console.clear();
|
|
2045
|
-
console.log("\n [OK] Global prompt enabled across client configurations:\n");
|
|
2046
|
-
results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
|
|
2047
|
-
await waitForEnter();
|
|
2048
|
-
break;
|
|
2049
|
-
}
|
|
2050
|
-
case "disable_prompt": {
|
|
2051
|
-
const { disableGlobalPrompt } = await import("./prompt_manager.js");
|
|
2052
|
-
const results = await disableGlobalPrompt();
|
|
2053
|
-
console.clear();
|
|
2054
|
-
console.log("\n [OK] Global prompt disabled across client configurations:\n");
|
|
2055
|
-
results.forEach((r) => console.log(` - ${r.name}: ${r.filePath} (${r.status})`));
|
|
2056
|
-
await waitForEnter();
|
|
2057
|
-
break;
|
|
2058
|
-
}
|
|
2059
|
-
case "reset": {
|
|
2060
|
-
resetConfig();
|
|
2061
|
-
console.clear();
|
|
2062
|
-
console.log("\n [OK] Configuration reset to factory defaults (RSF 50/50, e5-small, no reranker).\n");
|
|
2063
|
-
await waitForEnter();
|
|
2064
|
-
break;
|
|
2065
|
-
}
|
|
2066
|
-
case "exit": {
|
|
2067
|
-
running = false;
|
|
2068
|
-
console.clear();
|
|
2069
|
-
console.log("Exiting CLI. Configuration saved.");
|
|
2070
|
-
break;
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
if (process.argv[1] && process.argv[1].includes("cli.js")) {
|
|
2077
|
-
if (typeof global.gc !== "function") {
|
|
2078
|
-
const { spawn } = await import("node:child_process");
|
|
2079
|
-
const args = ["--expose-gc", ...process.argv.slice(1)];
|
|
2080
|
-
const child = spawn(process.execPath, args, { stdio: "inherit" });
|
|
2081
|
-
child.on("exit", (code) => process.exit(code));
|
|
2082
|
-
} else {
|
|
2083
|
-
runCli().catch((err) => console.error("CLI error:", err));
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
import { getConfig } from "./config/config_manager.js";
|
|
4
|
+
import { selectCategoryMenu, selectSimpleMenu } from "./cli/ui.js";
|
|
5
|
+
import { getQuickStats, invalidateQuickStats } from "./cli/quick_stats.js";
|
|
6
|
+
import { handleDirectCommands } from "./cli/direct_commands.js";
|
|
7
|
+
import { handleEngineAction } from "./cli/handlers/engine_actions.js";
|
|
8
|
+
import { handleStorageAction } from "./cli/handlers/storage_actions.js";
|
|
9
|
+
import { handleCloudAction } from "./cli/handlers/cloud_actions.js";
|
|
10
|
+
import { handlePromptAction } from "./cli/handlers/prompt_actions.js";
|
|
11
|
+
import { handleDiagnosticsAction } from "./cli/handlers/diagnostics_actions.js";
|
|
12
|
+
|
|
13
|
+
export async function runCli() {
|
|
14
|
+
const cliArgs = process.argv.slice(2);
|
|
15
|
+
const handled = await handleDirectCommands(cliArgs);
|
|
16
|
+
if (handled) return;
|
|
17
|
+
|
|
18
|
+
readline.emitKeypressEvents(process.stdin);
|
|
19
|
+
|
|
20
|
+
let running = true;
|
|
21
|
+
let selectedCategory = 0;
|
|
22
|
+
|
|
23
|
+
while (running) {
|
|
24
|
+
let config = getConfig();
|
|
25
|
+
const stats = await getQuickStats();
|
|
26
|
+
const semPct = Math.round(config.alpha * 100);
|
|
27
|
+
const lexPct = 100 - semPct;
|
|
28
|
+
|
|
29
|
+
const categories = [
|
|
30
|
+
{
|
|
31
|
+
label: "ENGINE & HYBRID SEARCH SETTINGS",
|
|
32
|
+
value: "engine",
|
|
33
|
+
hint: `${config.fusionAlgorithm.toUpperCase()} | ${semPct}% Sem / ${lexPct}% Lex | ${config.embeddingModel.split("/").pop()}`,
|
|
34
|
+
info: "Configure search algorithm, embedding models, batch sizes, and hardware",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: "KNOWLEDGE BASE & STORAGE MANAGEMENT",
|
|
38
|
+
value: "storage",
|
|
39
|
+
hint: `${stats.docCount} Docs | ${stats.chunkCount} Chunks | ${stats.factCount} Facts`,
|
|
40
|
+
info: "Manage facts, RAG documents, snapshots, models, and database",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "CLOUD SYNCHRONIZATION & TURSO",
|
|
44
|
+
value: "cloud",
|
|
45
|
+
hint: config.mode.toUpperCase(),
|
|
46
|
+
info: "Login, logout, API keys, operational mode, and conflict strategy",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "GLOBAL PROMPT & INTEGRATION MANAGEMENT",
|
|
50
|
+
value: "prompt",
|
|
51
|
+
info: "Enable or disable memory instructions in client configs",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "DIAGNOSTICS & SYSTEM ACTIONS",
|
|
55
|
+
value: "diagnostics",
|
|
56
|
+
info: "Benchmarks, search verification, cache, and config reset",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "EXIT",
|
|
60
|
+
value: "exit",
|
|
61
|
+
info: "Save configuration and exit to terminal",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const res = await selectCategoryMenu({
|
|
66
|
+
title: "MEMORY PLUGIN RAG ENGINE CONTROL PANEL",
|
|
67
|
+
stats,
|
|
68
|
+
categories,
|
|
69
|
+
initialIndex: selectedCategory,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (res.action === "back" || res.value === "exit") {
|
|
73
|
+
running = false;
|
|
74
|
+
console.clear();
|
|
75
|
+
console.log("Exiting CLI. Configuration saved.");
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
selectedCategory = res.index;
|
|
80
|
+
|
|
81
|
+
let categoryRunning = true;
|
|
82
|
+
let categoryItemIndex = 0;
|
|
83
|
+
|
|
84
|
+
while (categoryRunning) {
|
|
85
|
+
config = getConfig();
|
|
86
|
+
invalidateQuickStats();
|
|
87
|
+
const currentStats = await getQuickStats();
|
|
88
|
+
const catRes = await showCategorySubmenu(res.value, config, currentStats, categoryItemIndex);
|
|
89
|
+
|
|
90
|
+
if (catRes.action === "back") {
|
|
91
|
+
categoryRunning = false;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
categoryItemIndex = catRes.index;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function showCategorySubmenu(category, config, stats, initialIndex = 0) {
|
|
101
|
+
const semPct = Math.round(config.alpha * 100);
|
|
102
|
+
const lexPct = 100 - semPct;
|
|
103
|
+
|
|
104
|
+
let items = [];
|
|
105
|
+
|
|
106
|
+
switch (category) {
|
|
107
|
+
case "engine":
|
|
108
|
+
items = [
|
|
109
|
+
{
|
|
110
|
+
label: "Fusion Algorithm",
|
|
111
|
+
badge: config.fusionAlgorithm.toUpperCase(),
|
|
112
|
+
value: "algo",
|
|
113
|
+
info: "Choose how vector similarity and BM25 text ranks are fused",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
label: "RSF Alpha Balance",
|
|
117
|
+
badge: `${semPct}% Sem / ${lexPct}% Lex`,
|
|
118
|
+
value: "alpha",
|
|
119
|
+
info: `Current Alpha: ${config.alpha.toFixed(2)}. Adjust ratio of Vector vs BM25 Keyword score`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: "Embedding Model",
|
|
123
|
+
badge: config.embeddingModel.split("/").pop(),
|
|
124
|
+
value: "embedding",
|
|
125
|
+
info: `Model: ${config.embeddingModel}. ONNX Feature Extraction via @huggingface/transformers`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
label: "Reranker Model",
|
|
129
|
+
badge: config.rerankerEnabled ? config.rerankerModel.split("/").pop() : "DISABLED",
|
|
130
|
+
value: "reranker",
|
|
131
|
+
info: config.rerankerEnabled ? `Reranker active: ${config.rerankerModel}` : "Optional Cross-Encoder re-ranking pass",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: "Vector Batch Size",
|
|
135
|
+
badge: `${config.batchSize || 12} Chunks`,
|
|
136
|
+
value: "batch_size",
|
|
137
|
+
info: `Ingestion batch size: ${config.batchSize || 12} micro-chunks per ONNX pass`,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
label: "GPU Attention Budget",
|
|
141
|
+
badge: `${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M Units`,
|
|
142
|
+
value: "gpu_budget",
|
|
143
|
+
info: `Micro-batch tensor budget: ${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M quadratic units (controls max peak VRAM usage on GPU)`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
label: "CPU WASM Threads",
|
|
147
|
+
badge: config.onnxThreads > 0 ? `${config.onnxThreads} Threads` : "AUTO (CPU Cores)",
|
|
148
|
+
value: "onnx_threads",
|
|
149
|
+
info: config.onnxThreads > 0 ? `ONNX execution threads manually set to ${config.onnxThreads}` : "Auto-detect optimal physical CPU threads",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: "Execution Hardware",
|
|
153
|
+
badge: (config.executionDevice || "cpu").toUpperCase() === "WEBGPU" || (config.executionDevice || "cpu").toUpperCase() === "GPU" ? "\x1b[31mGPU (EXPERIMENTAL)\x1b[0m" : "CPU (AVX2)",
|
|
154
|
+
value: "execution_device",
|
|
155
|
+
info: config.executionDevice === "webgpu" || config.executionDevice === "gpu"
|
|
156
|
+
? "⚠️ EXPERIMENTAL: ONNX DirectML GPU execution (high VRAM/padding overhead, CPU AVX2 recommended)"
|
|
157
|
+
: "CPU inference via AVX2 / WASM SIMD (Recommended for stability & speed)",
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "storage":
|
|
163
|
+
items = [
|
|
164
|
+
{
|
|
165
|
+
label: "[NOTEBOOK] Layer 1 Facts",
|
|
166
|
+
badge: `${stats.factCount} Facts Saved`,
|
|
167
|
+
value: "notebook",
|
|
168
|
+
info: "Inspect & delete durable user identity facts (global & project)",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: "[PROJECT IDENTITY] Manage Git Link & Aliases",
|
|
172
|
+
value: "git_identity",
|
|
173
|
+
info: "Link directory to Git project identity, unlink, relink, or view aliases",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
label: "[FACTS] Migrate Titles to Legacy Facts",
|
|
177
|
+
value: "migrate_titles",
|
|
178
|
+
info: "Mass-stamp auto titles onto facts that lack a **Title** prefix",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: "[RAG DOCS] Layer 2 RAG Base",
|
|
182
|
+
badge: `${stats.docCount} Docs / ${stats.chunkCount} Chunks`,
|
|
183
|
+
value: "rag_docs",
|
|
184
|
+
info: "Inspect ingested Markdown/code docs & delete chunks from SQLite",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: "[SNAPSHOT EXPORT] Export RAG Base Snapshot",
|
|
188
|
+
value: "export_snapshot",
|
|
189
|
+
info: "Export full RAG database, vectors & blobs into a snapshot file (.json or .json.gz)",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
label: "[SNAPSHOT IMPORT] Import RAG Base Snapshot",
|
|
193
|
+
value: "import_snapshot",
|
|
194
|
+
info: "Import RAG database, vectors & blobs from a snapshot file (.json or .json.gz)",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
label: "[MODELS] Manage & Purge ML Model Cache",
|
|
198
|
+
value: "manage_models",
|
|
199
|
+
info: "Inspect cached ONNX models on disk, check status (Ready / Partial / Not Downloaded) & delete models to free disk space",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
label: "[HARD RESET] Purge RAG Base & Blob Storage",
|
|
203
|
+
value: "hard_reset",
|
|
204
|
+
info: "Permanently delete all documents, sections, vectors, FTS indexes, and blobs",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
case "cloud":
|
|
210
|
+
items = [
|
|
211
|
+
{
|
|
212
|
+
label: "[CLOUD] Login to Turso Cloud",
|
|
213
|
+
value: "cloud_login",
|
|
214
|
+
info: "Browser OAuth, account API token, database URL+token, or import from environment (.env) — token/env methods work headless in Docker, Google Jules and VPS",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
label: "[CLOUD] Logout",
|
|
218
|
+
value: "cloud_logout",
|
|
219
|
+
info: "Sign out, purge encrypted secrets, and revert mode to only-local",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
label: "[API KEY] Set / Replace Account API Token",
|
|
223
|
+
value: "cloud_api_set",
|
|
224
|
+
info: "Paste a Turso account API token to authorize headless (Docker, Google Jules, VPS) — validated and persisted",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
label: "[API KEY] Remove Account API Token",
|
|
228
|
+
value: "cloud_api_clear",
|
|
229
|
+
info: "Delete the stored API token; the resolved database session is kept",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
label: "Operational Mode",
|
|
233
|
+
badge: config.mode.toUpperCase(),
|
|
234
|
+
value: "cloud_mode",
|
|
235
|
+
info: "Choose Operational Mode: only-local | only-cloud | hybrid-sync",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
label: "Conflict Strategy",
|
|
239
|
+
badge: (config.conflictStrategy || "merge").toUpperCase(),
|
|
240
|
+
value: "conflict_strategy",
|
|
241
|
+
info: "How hybrid-sync resolves differing local vs cloud stores: merge | cloud-wins | local-wins",
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case "prompt":
|
|
247
|
+
items = [
|
|
248
|
+
{
|
|
249
|
+
label: "[PROMPT ENABLE] Enable Global Prompt (Antigravity / Codex / Claude)",
|
|
250
|
+
value: "enable_prompt",
|
|
251
|
+
info: "Inject memory instructions into ~/.gemini/config/AGENTS.md, ~/.codex/AGENTS.md, ~/.claude/CLAUDE.md",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
label: "[PROMPT DISABLE] Disable Global Prompt",
|
|
255
|
+
value: "disable_prompt",
|
|
256
|
+
info: "Remove memory instructions from global AGENTS.md / CLAUDE.md files",
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case "diagnostics":
|
|
262
|
+
items = [
|
|
263
|
+
{
|
|
264
|
+
label: "[SEARCH] Run Search Verification Query",
|
|
265
|
+
value: "test",
|
|
266
|
+
info: "Execute hybrid search query and display result hit scores",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
label: "[GRAPH] Graph & Notebook Linking Verification",
|
|
270
|
+
value: "graph_test",
|
|
271
|
+
info: "Ingest sample doc + save Notebook fact linked to line range + verify recall & raw document reader",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
label: "[RESET] Reset Config to Factory Defaults",
|
|
275
|
+
value: "reset",
|
|
276
|
+
info: "Reset RSF alpha to 50/50 and restore factory default config",
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
items.push({ label: "< Back to Main Menu", value: "back" });
|
|
283
|
+
|
|
284
|
+
const titles = {
|
|
285
|
+
engine: "ENGINE & HYBRID SEARCH SETTINGS",
|
|
286
|
+
storage: "KNOWLEDGE BASE & STORAGE MANAGEMENT",
|
|
287
|
+
cloud: "CLOUD SYNCHRONIZATION & TURSO",
|
|
288
|
+
prompt: "GLOBAL PROMPT & INTEGRATION MANAGEMENT",
|
|
289
|
+
diagnostics: "DIAGNOSTICS & SYSTEM ACTIONS",
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const subRes = await selectSimpleMenu({
|
|
293
|
+
title: titles[category],
|
|
294
|
+
subtitle: "↑ / ↓ Navigate • ENTER Select • BACKSPACE Back",
|
|
295
|
+
items,
|
|
296
|
+
initialIndex,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (subRes.action === "back" || subRes.value === "back") {
|
|
300
|
+
return { action: "back" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await handleSubmenuItem(subRes.value, config, stats);
|
|
304
|
+
return { action: "select", index: subRes.index };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function handleSubmenuItem(value, config, stats) {
|
|
308
|
+
await handleEngineAction(value, config);
|
|
309
|
+
await handleStorageAction(value, config, stats);
|
|
310
|
+
await handleCloudAction(value, config);
|
|
311
|
+
await handlePromptAction(value);
|
|
312
|
+
await handleDiagnosticsAction(value, config, stats);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (process.argv[1] && process.argv[1].includes("cli.js")) {
|
|
316
|
+
if (typeof global.gc !== "function") {
|
|
317
|
+
const { spawn } = await import("node:child_process");
|
|
318
|
+
const args = ["--expose-gc", ...process.argv.slice(1)];
|
|
319
|
+
const child = spawn(process.execPath, args, { stdio: "inherit" });
|
|
320
|
+
child.on("exit", (code) => process.exit(code));
|
|
321
|
+
} else {
|
|
322
|
+
runCli().catch((err) => console.error("CLI error:", err));
|
|
323
|
+
}
|
|
324
|
+
}
|