@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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getDatabase } from "../db/database.js";
|
|
2
|
+
import { readMemoryRaw, GLOBAL_KEY, projectKey } from "../memory.js";
|
|
3
|
+
|
|
4
|
+
let _quickStatsCache = null;
|
|
5
|
+
let _quickStatsAt = 0;
|
|
6
|
+
const QUICK_STATS_TTL_MS = 3_000; // 3s — enough for one submenu round-trip
|
|
7
|
+
|
|
8
|
+
export function invalidateQuickStats() {
|
|
9
|
+
_quickStatsCache = null;
|
|
10
|
+
_quickStatsAt = 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getQuickStats() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
if (_quickStatsCache && (now - _quickStatsAt) < QUICK_STATS_TTL_MS) {
|
|
16
|
+
return _quickStatsCache;
|
|
17
|
+
}
|
|
18
|
+
let docCount = 0;
|
|
19
|
+
let chunkCount = 0;
|
|
20
|
+
try {
|
|
21
|
+
const db = await getDatabase();
|
|
22
|
+
const docRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
|
|
23
|
+
docCount = docRow ? docRow.cnt : 0;
|
|
24
|
+
const chunkRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
|
|
25
|
+
chunkCount = chunkRow ? chunkRow.cnt : 0;
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
|
|
28
|
+
let factCount = 0;
|
|
29
|
+
try {
|
|
30
|
+
const projKey = await projectKey(null, null);
|
|
31
|
+
const globalF = await readMemoryRaw(GLOBAL_KEY);
|
|
32
|
+
const projF = await readMemoryRaw(projKey);
|
|
33
|
+
factCount = (globalF ? globalF.length : 0) + (projF ? projF.length : 0);
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
|
|
36
|
+
_quickStatsCache = { docCount, chunkCount, factCount };
|
|
37
|
+
_quickStatsAt = now;
|
|
38
|
+
return _quickStatsCache;
|
|
39
|
+
}
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import readline from "readline";
|
|
2
|
+
import { getModelStorageInfo } from "../ml/model_manager.js";
|
|
3
|
+
|
|
4
|
+
export const EMBEDDING_PRESETS = [
|
|
5
|
+
"Xenova/multilingual-e5-small",
|
|
6
|
+
"Xenova/multilingual-e5-base",
|
|
7
|
+
"Xenova/multilingual-e5-large",
|
|
8
|
+
"Xenova/bge-small-en-v1.5",
|
|
9
|
+
"Xenova/bge-base-en-v1.5",
|
|
10
|
+
"Xenova/bge-large-en-v1.5",
|
|
11
|
+
"Xenova/bge-m3",
|
|
12
|
+
"Xenova/all-MiniLM-L6-v2",
|
|
13
|
+
"Xenova/all-mpnet-base-v2",
|
|
14
|
+
"Xenova/paraphrase-multilingual-MiniLM-L12-v2",
|
|
15
|
+
"Xenova/gte-small",
|
|
16
|
+
"Xenova/gte-large",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const RERANKER_PRESETS = [
|
|
20
|
+
"none",
|
|
21
|
+
"Xenova/bge-reranker-base",
|
|
22
|
+
"Xenova/bge-reranker-large",
|
|
23
|
+
"Xenova/ms-marco-MiniLM-L-6-v2",
|
|
24
|
+
"Xenova/ms-marco-TinyBERT-L-2-v2",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const PANEL_WIDTH = 58;
|
|
28
|
+
|
|
29
|
+
export async function downloadModelWithProgress(modelName, type = "embedding") {
|
|
30
|
+
console.clear();
|
|
31
|
+
console.log(`\n MODEL DOWNLOAD & PRELOAD`);
|
|
32
|
+
console.log(` \x1b[90m${type.toUpperCase()}: ${modelName.substring(0, 36)}\x1b[0m\n`);
|
|
33
|
+
|
|
34
|
+
const spinFrames = ["|", "/", "-", "\\"];
|
|
35
|
+
let spinIdx = 0;
|
|
36
|
+
let lastProgress = 0;
|
|
37
|
+
|
|
38
|
+
const handleProgress = (p) => {
|
|
39
|
+
if (!p) return;
|
|
40
|
+
spinIdx = (spinIdx + 1) % spinFrames.length;
|
|
41
|
+
const spin = spinFrames[spinIdx];
|
|
42
|
+
|
|
43
|
+
const filename = p.file ? p.file.split("/").pop() : (p.name || "weights");
|
|
44
|
+
const pct = typeof p.progress === "number" ? Math.round(p.progress) : lastProgress;
|
|
45
|
+
if (typeof p.progress === "number") lastProgress = pct;
|
|
46
|
+
|
|
47
|
+
const loadedMB = p.loaded ? (p.loaded / (1024 * 1024)).toFixed(1) : "0.0";
|
|
48
|
+
const totalMB = p.total ? (p.total / (1024 * 1024)).toFixed(1) : "0.0";
|
|
49
|
+
|
|
50
|
+
const barLen = 18;
|
|
51
|
+
const filled = Math.round((pct / 100) * barLen);
|
|
52
|
+
const bar = "=".repeat(filled).padEnd(barLen);
|
|
53
|
+
|
|
54
|
+
let statusMsg = "";
|
|
55
|
+
if (p.status === "initiate") statusMsg = "Initiating...";
|
|
56
|
+
else if (p.status === "download" || p.status === "progress") statusMsg = `${pct}% (${loadedMB}/${totalMB} MB)`;
|
|
57
|
+
else if (p.status === "done") statusMsg = "Verifying...";
|
|
58
|
+
else if (p.status === "ready") statusMsg = "Ready!";
|
|
59
|
+
else statusMsg = `${pct}%`;
|
|
60
|
+
|
|
61
|
+
const fileLabel = filename.length > 18 ? filename.substring(0, 15) + "..." : filename;
|
|
62
|
+
process.stdout.write(`\r ${spin} [${bar}] ${fileLabel.padEnd(18)} ${statusMsg.padEnd(22)}`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const { preloadModel } = await import("../ml/model_manager.js");
|
|
67
|
+
await preloadModel(modelName, type, handleProgress);
|
|
68
|
+
process.stdout.write("\r" + " ".repeat(72) + "\r");
|
|
69
|
+
console.log(` \x1b[32m[OK] Model "${modelName}" ready!\x1b[0m\n`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
process.stdout.write("\r" + " ".repeat(72) + "\r");
|
|
72
|
+
console.error(` \x1b[31m[ERROR] Download for "${modelName}" failed: ${err.message}\x1b[0m\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function printHeaderPanel(title, stats) {
|
|
77
|
+
console.log(`\n \x1b[1m\x1b[37m${title}\x1b[0m`);
|
|
78
|
+
console.log(` \x1b[90mStorage: ${stats.docCount} Docs | ${stats.chunkCount} Chunks | ${stats.factCount} Facts\x1b[0m`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function printQuickInfoBox(infoText) {
|
|
82
|
+
console.log(` \x1b[90mINFO: ${infoText}\x1b[0m\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function padVisible(str, width, align = "left") {
|
|
86
|
+
const visibleLength = String(str).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
87
|
+
const padding = " ".repeat(Math.max(0, width - visibleLength));
|
|
88
|
+
return align === "right" ? padding + str : str + padding;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatRankColor(rankStr) {
|
|
92
|
+
if (rankStr === "#1") return "\x1b[1m\x1b[32m#1\x1b[0m";
|
|
93
|
+
if (rankStr.startsWith("#")) return `\x1b[33m${rankStr}\x1b[0m`;
|
|
94
|
+
return "\x1b[90mMISSED\x1b[0m";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function wrapText(text, width) {
|
|
98
|
+
if (!text || text.length <= width) return [text || ""];
|
|
99
|
+
const words = text.split(/\s+/);
|
|
100
|
+
const lines = [];
|
|
101
|
+
let currentLine = "";
|
|
102
|
+
|
|
103
|
+
for (const word of words) {
|
|
104
|
+
if (word.length > width) {
|
|
105
|
+
if (currentLine) {
|
|
106
|
+
lines.push(currentLine);
|
|
107
|
+
currentLine = "";
|
|
108
|
+
}
|
|
109
|
+
let rem = word;
|
|
110
|
+
while (rem.length > width) {
|
|
111
|
+
lines.push(rem.substring(0, width));
|
|
112
|
+
rem = rem.substring(width);
|
|
113
|
+
}
|
|
114
|
+
currentLine = rem;
|
|
115
|
+
} else if ((currentLine + (currentLine ? " " : "") + word).length <= width) {
|
|
116
|
+
currentLine += (currentLine ? " " : "") + word;
|
|
117
|
+
} else {
|
|
118
|
+
lines.push(currentLine);
|
|
119
|
+
currentLine = word;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (currentLine) lines.push(currentLine);
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderPerQueryBreakdownTable(breakdown) {
|
|
127
|
+
if (!breakdown || breakdown.length === 0) return;
|
|
128
|
+
|
|
129
|
+
console.log(`\n PER-QUERY RESULTS BREAKDOWN (${breakdown.length} Queries)\n`);
|
|
130
|
+
|
|
131
|
+
breakdown.forEach((item, itemIdx) => {
|
|
132
|
+
const isMatch = item.topHit && (item.topHit === item.target || (item.expectedDocIds && item.expectedDocIds.includes(item.topHit)));
|
|
133
|
+
|
|
134
|
+
console.log(` ${item.id}. ${item.query}`);
|
|
135
|
+
console.log(` Target: \x1b[36m${item.target}\x1b[0m`);
|
|
136
|
+
console.log(` BM25: ${formatRankColor(item.bm25Rank)} Vector: ${formatRankColor(item.vectorRank)} RRF: ${formatRankColor(item.rrfRank)} RSF: ${formatRankColor(item.rsfRank)}`);
|
|
137
|
+
console.log(` Top Hit: ${isMatch ? "\x1b[32m" : "\x1b[33m"}${item.topHit || "NONE"}\x1b[0m`);
|
|
138
|
+
|
|
139
|
+
if (itemIdx < breakdown.length - 1) {
|
|
140
|
+
console.log("");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderBenchmarkResultsTable(results) {
|
|
148
|
+
const isSmoke = results && results.mode === "smoke";
|
|
149
|
+
const title = isSmoke ? "SMOKE BENCHMARK RESULTS" : "SEARCH QUALITY BENCHMARK RESULTS";
|
|
150
|
+
const nQueries = results && results.bm25 ? results.bm25.n : 0;
|
|
151
|
+
const subtitle = isSmoke
|
|
152
|
+
? `Smoke: ${nQueries} queries (stats skipped, fast iteration)`
|
|
153
|
+
: `Evaluated over ${nQueries} challenging cross-lingual queries`;
|
|
154
|
+
|
|
155
|
+
console.log(`\n \x1b[1m\x1b[37m${title}\x1b[0m`);
|
|
156
|
+
console.log(` \x1b[90m${subtitle}\x1b[0m\n`);
|
|
157
|
+
|
|
158
|
+
if (results && results.breakdown) {
|
|
159
|
+
renderPerQueryBreakdownTable(results.breakdown);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\n METRIC COMPARISON BY SEARCH STRATEGY\n`);
|
|
163
|
+
console.log(` Strategy MRR@5 Recall@5 NDCG@5`);
|
|
164
|
+
console.log(` ${"─".repeat(50)}`);
|
|
165
|
+
|
|
166
|
+
const strategies = [
|
|
167
|
+
{ name: "BM25 Search Only", data: results.bm25, key: "bm25" },
|
|
168
|
+
{ name: "Dense ONNX Vector", data: results.vector, key: "vector" },
|
|
169
|
+
{ name: "Hybrid RRF (Rank)", data: results.hybridRrf, key: "hybrid_rrf" },
|
|
170
|
+
{ name: "Hybrid RSF (Score)", data: results.hybridRsf, key: "hybrid_rsf" },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const getMrr = (d) => (d ? (d.mrr ?? d.mrrAtK ?? 0) : 0);
|
|
174
|
+
const getRecall = (d) => (d ? (d.recall ?? d.recallAtK ?? 0) : 0);
|
|
175
|
+
const getNdcg = (d) => (d ? (d.ndcg ?? d.ndcgAtK ?? 0) : 0);
|
|
176
|
+
|
|
177
|
+
strategies.forEach((s) => {
|
|
178
|
+
const nameStr = s.name.padEnd(20);
|
|
179
|
+
const mrrStr = getMrr(s.data).toFixed(4).padEnd(10);
|
|
180
|
+
const recallPct = (getRecall(s.data) * 100).toFixed(1) + "%";
|
|
181
|
+
const recallStr = recallPct.padEnd(13);
|
|
182
|
+
const ndcgStr = getNdcg(s.data).toFixed(4);
|
|
183
|
+
|
|
184
|
+
const isBest = results.winner && s.key === results.winner;
|
|
185
|
+
const color = isBest ? "\x1b[1m\x1b[36m" : "\x1b[37m";
|
|
186
|
+
|
|
187
|
+
console.log(` ${color}${nameStr}${mrrStr}${recallStr}${ndcgStr}\x1b[0m`);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log("");
|
|
191
|
+
|
|
192
|
+
if (results && results.winner) {
|
|
193
|
+
const winnerLabel =
|
|
194
|
+
results.winner === "hybrid_rsf" ? "RSF"
|
|
195
|
+
: results.winner === "hybrid_rrf" ? "RRF"
|
|
196
|
+
: results.winner === "vector" ? "Vector"
|
|
197
|
+
: results.winner === "bm25" ? "BM25"
|
|
198
|
+
: results.winner;
|
|
199
|
+
const p = results.pairedTests && results.pairedTests.rrfVsRsf;
|
|
200
|
+
const sigNote = p
|
|
201
|
+
? (p.p < 0.05 ? ` (RRF vs RSF p=${p.p}, significant)` : ` (RRF vs RSF p=${p.p}, NOT significant at N=${p.n})`)
|
|
202
|
+
: (results.mode === "smoke" ? " (smoke: stats skipped)" : "");
|
|
203
|
+
console.log(` \x1b[90m Winner by MRR: \x1b[1m\x1b[36m${winnerLabel}\x1b[0m\x1b[90m${sigNote}\x1b[0m\n`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function selectCategoryMenu({ title, stats, categories, initialIndex = 0 }) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
let activeIndex = Math.min(Math.max(0, initialIndex), categories.length - 1);
|
|
210
|
+
|
|
211
|
+
if (process.stdin.isTTY) {
|
|
212
|
+
process.stdin.setRawMode(true);
|
|
213
|
+
}
|
|
214
|
+
process.stdin.resume();
|
|
215
|
+
|
|
216
|
+
function render() {
|
|
217
|
+
console.clear();
|
|
218
|
+
console.log(`\n ${title}\n`);
|
|
219
|
+
console.log(` Storage: ${stats.docCount} Docs | ${stats.chunkCount} Chunks | ${stats.factCount} Facts\n`);
|
|
220
|
+
console.log(" Controls: ↑ / ↓ - Navigate [ENTER] - Select [BACKSPACE] - Exit\n");
|
|
221
|
+
|
|
222
|
+
categories.forEach((cat, idx) => {
|
|
223
|
+
const isSelected = idx === activeIndex;
|
|
224
|
+
const pointer = isSelected ? " > " : " ";
|
|
225
|
+
const label = isSelected ? `\x1b[1m\x1b[36m${cat.label}\x1b[0m` : cat.label;
|
|
226
|
+
const hint = cat.hint ? ` \x1b[90m(${cat.hint})\x1b[0m` : "";
|
|
227
|
+
console.log(`${pointer}${label}${hint}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log("");
|
|
231
|
+
|
|
232
|
+
const activeCat = categories[activeIndex];
|
|
233
|
+
if (activeCat && activeCat.info) {
|
|
234
|
+
console.log(` \x1b[90m${activeCat.info}\x1b[0m\n`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
render();
|
|
239
|
+
|
|
240
|
+
function onKeypress(str, key) {
|
|
241
|
+
if (!key) return;
|
|
242
|
+
if (key.ctrl && key.name === "c") {
|
|
243
|
+
cleanup();
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
if (key.name === "up") {
|
|
247
|
+
activeIndex = (activeIndex - 1 + categories.length) % categories.length;
|
|
248
|
+
render();
|
|
249
|
+
} else if (key.name === "down") {
|
|
250
|
+
activeIndex = (activeIndex + 1) % categories.length;
|
|
251
|
+
render();
|
|
252
|
+
} else if (key.name === "return") {
|
|
253
|
+
cleanup();
|
|
254
|
+
resolve({ action: "select", index: activeIndex, value: categories[activeIndex].value });
|
|
255
|
+
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
256
|
+
cleanup();
|
|
257
|
+
resolve({ action: "back" });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function cleanup() {
|
|
262
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
263
|
+
if (process.stdin.isTTY) {
|
|
264
|
+
process.stdin.setRawMode(false);
|
|
265
|
+
}
|
|
266
|
+
process.stdin.pause();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.stdin.on("keypress", onKeypress);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function selectSimpleMenu({ title, subtitle = "", items, initialIndex = 0 }) {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
let index = Math.min(Math.max(0, initialIndex), items.length - 1);
|
|
276
|
+
|
|
277
|
+
if (process.stdin.isTTY) {
|
|
278
|
+
process.stdin.setRawMode(true);
|
|
279
|
+
}
|
|
280
|
+
process.stdin.resume();
|
|
281
|
+
|
|
282
|
+
function render() {
|
|
283
|
+
console.clear();
|
|
284
|
+
console.log(`\n ${title}`);
|
|
285
|
+
if (subtitle) {
|
|
286
|
+
console.log(` \x1b[90m${subtitle}\x1b[0m`);
|
|
287
|
+
}
|
|
288
|
+
console.log("\n Controls: ↑ / ↓ - Navigate [ENTER] - Select [BACKSPACE] - Back\n");
|
|
289
|
+
|
|
290
|
+
items.forEach((item, idx) => {
|
|
291
|
+
const isSelected = idx === index;
|
|
292
|
+
const pointer = isSelected ? " > " : " ";
|
|
293
|
+
const label = isSelected ? `\x1b[1m\x1b[36m${item.label}\x1b[0m` : item.label;
|
|
294
|
+
const badge = item.badge ? ` \x1b[33m[${item.badge}]\x1b[0m` : "";
|
|
295
|
+
const hint = item.hint ? ` \x1b[90m(${item.hint})\x1b[0m` : "";
|
|
296
|
+
console.log(`${pointer}${label}${badge}${hint}`);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
console.log("");
|
|
300
|
+
|
|
301
|
+
const activeItem = items[index];
|
|
302
|
+
if (activeItem && activeItem.info) {
|
|
303
|
+
console.log(` \x1b[90m${activeItem.info}\x1b[0m\n`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
render();
|
|
308
|
+
|
|
309
|
+
function onKeypress(str, key) {
|
|
310
|
+
if (!key) return;
|
|
311
|
+
if (key.ctrl && key.name === "c") {
|
|
312
|
+
cleanup();
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
if (key.name === "up") {
|
|
316
|
+
index = (index - 1 + items.length) % items.length;
|
|
317
|
+
render();
|
|
318
|
+
} else if (key.name === "down") {
|
|
319
|
+
index = (index + 1) % items.length;
|
|
320
|
+
render();
|
|
321
|
+
} else if (key.name === "return") {
|
|
322
|
+
cleanup();
|
|
323
|
+
resolve({ action: "select", index, value: items[index].value });
|
|
324
|
+
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
325
|
+
cleanup();
|
|
326
|
+
resolve({ action: "back" });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function cleanup() {
|
|
331
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
332
|
+
if (process.stdin.isTTY) {
|
|
333
|
+
process.stdin.setRawMode(false);
|
|
334
|
+
}
|
|
335
|
+
process.stdin.pause();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
process.stdin.on("keypress", onKeypress);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function adjustAlphaMenu(initialAlpha) {
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
let alpha = initialAlpha;
|
|
345
|
+
|
|
346
|
+
if (process.stdin.isTTY) {
|
|
347
|
+
process.stdin.setRawMode(true);
|
|
348
|
+
}
|
|
349
|
+
process.stdin.resume();
|
|
350
|
+
|
|
351
|
+
function render() {
|
|
352
|
+
console.clear();
|
|
353
|
+
console.log(`\n RSF ALPHA WEIGHT BALANCER`);
|
|
354
|
+
console.log(` \x1b[90mAdjust Vector Similarity vs BM25 Score Weight\x1b[0m`);
|
|
355
|
+
console.log("\n Controls: ← / → or ↑ / ↓ - Adjust (5% step) [ENTER] - Save [BACKSPACE] - Cancel\n");
|
|
356
|
+
|
|
357
|
+
const semPct = Math.round(alpha * 100);
|
|
358
|
+
const lexPct = 100 - semPct;
|
|
359
|
+
|
|
360
|
+
const totalBlocks = 20;
|
|
361
|
+
const semBlocks = Math.round(alpha * totalBlocks);
|
|
362
|
+
const lexBlocks = totalBlocks - semBlocks;
|
|
363
|
+
|
|
364
|
+
const bar = "━".repeat(semBlocks) + "─".repeat(lexBlocks);
|
|
365
|
+
|
|
366
|
+
console.log(` Balance: \x1b[36m${semPct}% Semantic (Vector)\x1b[0m / \x1b[33m${lexPct}% Lexical (BM25)\x1b[0m`);
|
|
367
|
+
console.log(` [ \x1b[36m${bar}\x1b[0m ] Alpha: \x1b[1m\x1b[32m${alpha.toFixed(2)}\x1b[0m\n`);
|
|
368
|
+
|
|
369
|
+
if (alpha === 0.5) {
|
|
370
|
+
console.log(" [*] \x1b[32mMode: 50 / 50 Balanced Hybrid Fusion (Recommended)\x1b[0m\n");
|
|
371
|
+
} else if (alpha > 0.5) {
|
|
372
|
+
console.log(` [*] Mode: Semantic Vector Priority (${semPct}%)\n`);
|
|
373
|
+
} else {
|
|
374
|
+
console.log(` [*] Mode: Exact Keyword BM25 Priority (${lexPct}%)\n`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
printQuickInfoBox(`RSF Formula: Score = ${alpha.toFixed(2)} * NormVector + ${(1 - alpha).toFixed(2)} * NormBM25`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
render();
|
|
381
|
+
|
|
382
|
+
function onKeypress(str, key) {
|
|
383
|
+
if (!key) return;
|
|
384
|
+
if (key.ctrl && key.name === "c") {
|
|
385
|
+
cleanup();
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
if (key.name === "left" || key.name === "down") {
|
|
389
|
+
alpha = Math.max(0.0, Math.round((alpha - 0.05) * 100) / 100);
|
|
390
|
+
render();
|
|
391
|
+
} else if (key.name === "right" || key.name === "up") {
|
|
392
|
+
alpha = Math.min(1.0, Math.round((alpha + 0.05) * 100) / 100);
|
|
393
|
+
render();
|
|
394
|
+
} else if (key.name === "return") {
|
|
395
|
+
cleanup();
|
|
396
|
+
resolve({ action: "save", value: alpha });
|
|
397
|
+
} else if (key.name === "backspace" || key.name === "escape" || key.name === "delete") {
|
|
398
|
+
cleanup();
|
|
399
|
+
resolve({ action: "cancel" });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function cleanup() {
|
|
404
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
405
|
+
if (process.stdin.isTTY) {
|
|
406
|
+
process.stdin.setRawMode(false);
|
|
407
|
+
}
|
|
408
|
+
process.stdin.pause();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
process.stdin.on("keypress", onKeypress);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function readTextInput(promptText, defaultValue = "") {
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
let text = defaultValue;
|
|
418
|
+
|
|
419
|
+
if (process.stdin.isTTY) {
|
|
420
|
+
process.stdin.setRawMode(true);
|
|
421
|
+
}
|
|
422
|
+
process.stdin.resume();
|
|
423
|
+
|
|
424
|
+
function render() {
|
|
425
|
+
console.clear();
|
|
426
|
+
console.log(`\n INPUT: ${promptText.toUpperCase()}`);
|
|
427
|
+
console.log(" Controls: Type text [ENTER] - Submit [BACKSPACE] - Delete / Cancel\n");
|
|
428
|
+
console.log(` > \x1b[36m${text}\x1b[0m_\n`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
render();
|
|
432
|
+
|
|
433
|
+
function onKeypress(str, key) {
|
|
434
|
+
if (!key) return;
|
|
435
|
+
if (key.ctrl && key.name === "c") {
|
|
436
|
+
cleanup();
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
if (key.name === "return") {
|
|
440
|
+
cleanup();
|
|
441
|
+
resolve({ action: "submit", value: text.trim() });
|
|
442
|
+
} else if (key.name === "backspace" || key.name === "delete") {
|
|
443
|
+
if (text.length > 0) {
|
|
444
|
+
text = text.slice(0, -1);
|
|
445
|
+
render();
|
|
446
|
+
} else {
|
|
447
|
+
cleanup();
|
|
448
|
+
resolve({ action: "cancel" });
|
|
449
|
+
}
|
|
450
|
+
} else if (key.name === "escape") {
|
|
451
|
+
cleanup();
|
|
452
|
+
resolve({ action: "cancel" });
|
|
453
|
+
} else if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
454
|
+
text += str;
|
|
455
|
+
render();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function cleanup() {
|
|
460
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
461
|
+
if (process.stdin.isTTY) {
|
|
462
|
+
process.stdin.setRawMode(false);
|
|
463
|
+
}
|
|
464
|
+
process.stdin.pause();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
process.stdin.on("keypress", onKeypress);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function waitForEnter() {
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
console.log("\n \x1b[90mPress [ENTER] or [BACKSPACE] to return to menu...\x1b[0m");
|
|
474
|
+
if (process.stdin.isTTY) {
|
|
475
|
+
process.stdin.setRawMode(true);
|
|
476
|
+
}
|
|
477
|
+
process.stdin.resume();
|
|
478
|
+
|
|
479
|
+
function onKeypress(str, key) {
|
|
480
|
+
if (!key) return;
|
|
481
|
+
if (key.ctrl && key.name === "c") {
|
|
482
|
+
cleanup();
|
|
483
|
+
process.exit(0);
|
|
484
|
+
}
|
|
485
|
+
if (key.name === "return" || key.name === "backspace" || key.name === "escape" || key.name === "delete" || key.name === "space") {
|
|
486
|
+
cleanup();
|
|
487
|
+
resolve();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function cleanup() {
|
|
492
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
493
|
+
if (process.stdin.isTTY) {
|
|
494
|
+
process.stdin.setRawMode(false);
|
|
495
|
+
}
|
|
496
|
+
process.stdin.pause();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
process.stdin.on("keypress", onKeypress);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function promptText(question) {
|
|
504
|
+
return new Promise((resolve) => {
|
|
505
|
+
let input = "";
|
|
506
|
+
let cursorPos = 0;
|
|
507
|
+
|
|
508
|
+
console.log(`\n ${question}\n > `);
|
|
509
|
+
|
|
510
|
+
if (process.stdin.isTTY) {
|
|
511
|
+
process.stdin.setRawMode(true);
|
|
512
|
+
}
|
|
513
|
+
process.stdin.resume();
|
|
514
|
+
|
|
515
|
+
function render() {
|
|
516
|
+
process.stdout.write(`\r > ${input}\x1b[K`);
|
|
517
|
+
process.stdout.write(`\r > ${input.substring(0, cursorPos)}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function onKeypress(str, key) {
|
|
521
|
+
if (!key) return;
|
|
522
|
+
if (key.ctrl && key.name === "c") {
|
|
523
|
+
cleanup();
|
|
524
|
+
process.exit(0);
|
|
525
|
+
}
|
|
526
|
+
if (key.name === "return") {
|
|
527
|
+
cleanup();
|
|
528
|
+
resolve(input.trim());
|
|
529
|
+
} else if (key.name === "backspace") {
|
|
530
|
+
if (cursorPos > 0) {
|
|
531
|
+
input = input.substring(0, cursorPos - 1) + input.substring(cursorPos);
|
|
532
|
+
cursorPos--;
|
|
533
|
+
render();
|
|
534
|
+
}
|
|
535
|
+
} else if (key.name === "delete") {
|
|
536
|
+
if (cursorPos < input.length) {
|
|
537
|
+
input = input.substring(0, cursorPos) + input.substring(cursorPos + 1);
|
|
538
|
+
render();
|
|
539
|
+
}
|
|
540
|
+
} else if (key.name === "left") {
|
|
541
|
+
if (cursorPos > 0) { cursorPos--; render(); }
|
|
542
|
+
} else if (key.name === "right") {
|
|
543
|
+
if (cursorPos < input.length) { cursorPos++; render(); }
|
|
544
|
+
} else if (key.name === "home") {
|
|
545
|
+
cursorPos = 0; render();
|
|
546
|
+
} else if (key.name === "end") {
|
|
547
|
+
cursorPos = input.length; render();
|
|
548
|
+
} else if (str && !key.ctrl && !key.meta) {
|
|
549
|
+
input = input.substring(0, cursorPos) + str + input.substring(cursorPos);
|
|
550
|
+
cursorPos += str.length;
|
|
551
|
+
render();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function cleanup() {
|
|
556
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
557
|
+
if (process.stdin.isTTY) {
|
|
558
|
+
process.stdin.setRawMode(false);
|
|
559
|
+
}
|
|
560
|
+
process.stdin.pause();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
process.stdin.on("keypress", onKeypress);
|
|
564
|
+
});
|
|
565
|
+
}
|