@lzhzzzzwill/cofos 1.1.19 → 1.2.1
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 +5 -2
- package/RUNTIME.md +14 -0
- package/bin/cofos.js +219 -179
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -14,9 +14,11 @@ The standalone npm package includes a minimal Python runtime under `runtime/`:
|
|
|
14
14
|
fwdemo/
|
|
15
15
|
├── bin/cofos.js
|
|
16
16
|
├── scripts/chat.py
|
|
17
|
+
├── scripts/sync_runtime.py
|
|
17
18
|
├── runtime/
|
|
18
19
|
│ ├── config/
|
|
19
20
|
│ └── src/
|
|
21
|
+
├── RUNTIME.md
|
|
20
22
|
└── requirements.txt
|
|
21
23
|
```
|
|
22
24
|
|
|
@@ -139,8 +141,9 @@ under `runtime/` exists only so the published package is self-contained.
|
|
|
139
141
|
- Fix bugs in `src/` (or `config/`), never in `runtime/`.
|
|
140
142
|
- `npm pack` / `npm publish` rebuild `runtime/` automatically via the package-local
|
|
141
143
|
`prepack` wrapper when the full source checkout is present.
|
|
142
|
-
- Rebuild by hand with `npm run build
|
|
143
|
-
|
|
144
|
+
- Rebuild by hand from `fwdemo/` with `npm run build`.
|
|
145
|
+
- From the repository root, rebuild with `python scripts/sync_fwdemo_runtime.py`.
|
|
146
|
+
- The package-local wrapper `fwdemo/scripts/sync_runtime.py` delegates to the root sync script in a source checkout, and only validates bundled files in an installed package.
|
|
144
147
|
- Check for drift with `python scripts/sync_fwdemo_runtime.py --check`.
|
|
145
148
|
|
|
146
149
|
The published package requires its bundled `runtime/`; the backend does not fall
|
package/RUNTIME.md
CHANGED
|
@@ -21,6 +21,20 @@ cofos --model /path/to/local/merged_model
|
|
|
21
21
|
|
|
22
22
|
PDFs passed with `--pdf-dir` are parsed into local chunks and merged into BM25 retrieval data under the COFOS cache root, defaulting to `./.cofos/`. They are not uploaded anywhere.
|
|
23
23
|
|
|
24
|
+
## Packaged file scope
|
|
25
|
+
|
|
26
|
+
The npm package intentionally ships only the runtime subset needed for QA:
|
|
27
|
+
|
|
28
|
+
- `runtime/config/config.yaml`
|
|
29
|
+
- `runtime/config/prompt_templates.yaml`
|
|
30
|
+
- PDF parsing helpers
|
|
31
|
+
- student inference, query routing, KG/BM25 retrieval, BM25 building, and text search
|
|
32
|
+
|
|
33
|
+
Training, extraction, SFT generation, FAISS building, and full KG construction
|
|
34
|
+
remain repository-only tools and are not included in the package. The package
|
|
35
|
+
uses `fwdemo/scripts/sync_runtime.py` during `prepack`; in a source checkout that
|
|
36
|
+
wrapper delegates to `scripts/sync_fwdemo_runtime.py`.
|
|
37
|
+
|
|
24
38
|
## Runtime config scope
|
|
25
39
|
|
|
26
40
|
The packaged `runtime/config/config.yaml` is generated from the full project config with a strict whitelist. It keeps only the fields needed by the standalone QA path: paths, retrieval, generation, prompt location, ROS normalization, and the student model pointer.
|
package/bin/cofos.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import
|
|
3
|
+
import blessed from "blessed";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -59,7 +59,6 @@ function logoLines(width) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function render({ modelPath, cacheDir, pdfDir }) {
|
|
62
|
-
console.clear();
|
|
63
62
|
const w = fw();
|
|
64
63
|
const minSplit = 96;
|
|
65
64
|
const cwd = process.cwd();
|
|
@@ -100,7 +99,7 @@ function render({ modelPath, cacheDir, pdfDir }) {
|
|
|
100
99
|
for (const item of right) lines.push(full(item, w));
|
|
101
100
|
}
|
|
102
101
|
lines.push(bot(w));
|
|
103
|
-
|
|
102
|
+
return lines.join("\n");
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
/* ─── Slash commands ─── */
|
|
@@ -142,12 +141,157 @@ function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python")
|
|
|
142
141
|
// Only the modules the chat backend actually imports for the standalone merged-model path.
|
|
143
142
|
const REQUIRED_MODULES = "import transformers, torch, yaml, huggingface_hub, pdfplumber, rank_bm25";
|
|
144
143
|
|
|
145
|
-
function ensureDeps(py) {
|
|
146
|
-
|
|
144
|
+
function ensureDeps(py, log = console.log) {
|
|
145
|
+
log("[cofos] Checking Python dependencies …");
|
|
147
146
|
if (spawnSync(py, ["-c", `${REQUIRED_MODULES}; print('ok')`], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
|
|
148
|
-
|
|
149
|
-
const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "
|
|
150
|
-
if (r.
|
|
147
|
+
log("[cofos] Installing Python dependencies (first run, may take a while) …");
|
|
148
|
+
const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "pipe" });
|
|
149
|
+
if (r.stdout) log(r.stdout.trimEnd());
|
|
150
|
+
if (r.stderr) log(r.stderr.trimEnd());
|
|
151
|
+
if (r.status !== 0) { log("[cofos] Failed to install Python dependencies."); process.exit(1); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ─── TUI ─── */
|
|
155
|
+
|
|
156
|
+
function createTui() {
|
|
157
|
+
const screen = blessed.screen({
|
|
158
|
+
smartCSR: true,
|
|
159
|
+
fullUnicode: true,
|
|
160
|
+
title: "COFOS",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const history = blessed.box({
|
|
164
|
+
top: 0,
|
|
165
|
+
left: 0,
|
|
166
|
+
width: "100%",
|
|
167
|
+
height: "100%-3",
|
|
168
|
+
scrollable: true,
|
|
169
|
+
alwaysScroll: true,
|
|
170
|
+
keys: true,
|
|
171
|
+
vi: true,
|
|
172
|
+
mouse: true,
|
|
173
|
+
tags: false,
|
|
174
|
+
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const suggestions = blessed.box({
|
|
178
|
+
bottom: 3,
|
|
179
|
+
left: 2,
|
|
180
|
+
width: "70%",
|
|
181
|
+
height: 6,
|
|
182
|
+
hidden: true,
|
|
183
|
+
border: { type: "line" },
|
|
184
|
+
tags: true,
|
|
185
|
+
style: { border: { fg: "yellow" }, fg: "white", bg: "black" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const input = blessed.textbox({
|
|
189
|
+
bottom: 0,
|
|
190
|
+
left: 0,
|
|
191
|
+
width: "100%",
|
|
192
|
+
height: 3,
|
|
193
|
+
inputOnFocus: true,
|
|
194
|
+
keys: true,
|
|
195
|
+
mouse: true,
|
|
196
|
+
border: { type: "line" },
|
|
197
|
+
label: " cofos ",
|
|
198
|
+
style: {
|
|
199
|
+
border: { fg: "cyan" },
|
|
200
|
+
focus: { border: { fg: "green" } },
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
screen.append(history);
|
|
205
|
+
screen.append(suggestions);
|
|
206
|
+
screen.append(input);
|
|
207
|
+
|
|
208
|
+
let buffer = "";
|
|
209
|
+
let onSubmit = () => {};
|
|
210
|
+
|
|
211
|
+
function append(text = "") {
|
|
212
|
+
const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
213
|
+
for (const part of data.split("\r")) {
|
|
214
|
+
if (part === "") continue;
|
|
215
|
+
if (data.includes("\r")) {
|
|
216
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
217
|
+
buffer = (lastNewline === -1 ? "" : buffer.slice(0, lastNewline + 1)) + part;
|
|
218
|
+
} else {
|
|
219
|
+
buffer += part;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
223
|
+
history.setContent(buffer);
|
|
224
|
+
history.setScrollPerc(100);
|
|
225
|
+
screen.render();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hideSuggestions() {
|
|
229
|
+
if (!suggestions.hidden) {
|
|
230
|
+
suggestions.hide();
|
|
231
|
+
screen.render();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function updateSuggestions() {
|
|
236
|
+
const value = input.getValue() || "";
|
|
237
|
+
if (!value.startsWith("/")) {
|
|
238
|
+
hideSuggestions();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const matches = slashMatches(value);
|
|
242
|
+
if (!matches.length) {
|
|
243
|
+
hideSuggestions();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
suggestions.setContent(matches.map(({ cmd, desc }) => `{cyan-fg}${cmd}{/cyan-fg} ${desc}`).join("\n"));
|
|
247
|
+
suggestions.height = Math.min(8, matches.length + 2);
|
|
248
|
+
suggestions.show();
|
|
249
|
+
screen.render();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function submitInput() {
|
|
253
|
+
const value = input.getValue();
|
|
254
|
+
hideSuggestions();
|
|
255
|
+
input.clearValue();
|
|
256
|
+
input.focus();
|
|
257
|
+
screen.render();
|
|
258
|
+
onSubmit(String(value || ""));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
input.key(["enter"], submitInput);
|
|
262
|
+
|
|
263
|
+
input.on("keypress", () => setTimeout(updateSuggestions, 0));
|
|
264
|
+
|
|
265
|
+
screen.key(["tab"], () => {
|
|
266
|
+
const value = input.getValue() || "";
|
|
267
|
+
if (!value.startsWith("/")) return;
|
|
268
|
+
const matches = slashMatches(value);
|
|
269
|
+
if (!matches.length) return;
|
|
270
|
+
if (matches.length === 1) {
|
|
271
|
+
input.setValue(matches[0].cmd);
|
|
272
|
+
hideSuggestions();
|
|
273
|
+
input.focus();
|
|
274
|
+
screen.render();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
updateSuggestions();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
screen.key(["escape"], hideSuggestions);
|
|
281
|
+
screen.key(["C-c"], () => process.emit("SIGINT"));
|
|
282
|
+
|
|
283
|
+
input.focus();
|
|
284
|
+
screen.render();
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
screen,
|
|
288
|
+
input,
|
|
289
|
+
append,
|
|
290
|
+
print: (text = "") => append(String(text) + "\n"),
|
|
291
|
+
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
292
|
+
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
293
|
+
destroy: () => screen.destroy(),
|
|
294
|
+
};
|
|
151
295
|
}
|
|
152
296
|
|
|
153
297
|
/* ─── main ─── */
|
|
@@ -218,17 +362,17 @@ function main() {
|
|
|
218
362
|
if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
|
|
219
363
|
if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
|
|
220
364
|
|
|
221
|
-
render({ modelPath, cacheDir, pdfDir });
|
|
222
|
-
|
|
223
365
|
const py = findPy();
|
|
224
366
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
225
367
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
226
|
-
ensureDeps(py);
|
|
227
368
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
369
|
+
const ui = createTui();
|
|
370
|
+
ui.print(plain(render({ modelPath, cacheDir, pdfDir })));
|
|
371
|
+
ensureDeps(py, (msg) => ui.print(msg));
|
|
372
|
+
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
373
|
+
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
374
|
+
else ui.print(" Tip: add local papers with `cofos --pdf-dir ./new_pdfs`");
|
|
375
|
+
ui.print("");
|
|
232
376
|
|
|
233
377
|
const backendEnv = {
|
|
234
378
|
...process.env,
|
|
@@ -246,94 +390,78 @@ function main() {
|
|
|
246
390
|
env: backendEnv,
|
|
247
391
|
});
|
|
248
392
|
|
|
249
|
-
// `encoding` is not a spawn() option — without setEncoding each Buffer chunk
|
|
250
|
-
// is decoded on its own, so a multi-byte character (·OH, ¹O₂, any Chinese)
|
|
251
|
-
// straddling a chunk boundary comes out as mojibake.
|
|
252
393
|
proc.stdout.setEncoding("utf-8");
|
|
253
394
|
proc.stderr.setEncoding("utf-8");
|
|
254
395
|
|
|
255
396
|
let buf = "", ready = false, generating = false, responseOpen = false, closed = false;
|
|
256
397
|
const inputQ = [];
|
|
257
|
-
|
|
258
|
-
const contPrompt = G + " ... " + R;
|
|
259
|
-
const docked = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
260
|
-
let dockRows = process.stdout.rows || 24;
|
|
261
|
-
let dockCols = process.stdout.columns || 80;
|
|
262
|
-
let historyRow = Math.max(0, dockRows - 2);
|
|
263
|
-
let historyCol = 0;
|
|
264
|
-
const rl = createInterface({
|
|
265
|
-
input: process.stdin,
|
|
266
|
-
output: process.stdout,
|
|
267
|
-
terminal: true,
|
|
268
|
-
prompt: mainPrompt,
|
|
269
|
-
completer: slashCompletions,
|
|
270
|
-
});
|
|
271
|
-
rl.on("line", (l) => {
|
|
272
|
-
if (!ready || generating) inputQ.push(l);
|
|
273
|
-
else if (!handle(l)) ask();
|
|
274
|
-
});
|
|
398
|
+
let mlBuf = [];
|
|
275
399
|
|
|
276
|
-
function
|
|
277
|
-
if (
|
|
400
|
+
function shutdown() {
|
|
401
|
+
if (closed) return;
|
|
402
|
+
closed = true;
|
|
403
|
+
try { proc.stdin.end(); } catch (_) {}
|
|
404
|
+
try { proc.kill("SIGTERM"); } catch (_) {}
|
|
405
|
+
ui.destroy();
|
|
278
406
|
}
|
|
279
407
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
dockRows = process.stdout.rows || dockRows;
|
|
283
|
-
dockCols = process.stdout.columns || dockCols;
|
|
284
|
-
historyRow = Math.max(0, dockRows - 2);
|
|
285
|
-
historyCol = Math.min(historyCol, Math.max(0, dockCols - 1));
|
|
286
|
-
process.stdout.write(`\x1b[1;${Math.max(1, dockRows - 1)}r`);
|
|
287
|
-
cursorTo(process.stdout, 0, dockRows - 1);
|
|
288
|
-
clearLine(process.stdout, 0);
|
|
289
|
-
}
|
|
408
|
+
process.on("SIGTERM", shutdown);
|
|
409
|
+
process.on("SIGINT", shutdown);
|
|
290
410
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
411
|
+
function drain() {
|
|
412
|
+
while (inputQ.length) {
|
|
413
|
+
const line = inputQ.shift();
|
|
414
|
+
if (handle(line)) return;
|
|
415
|
+
}
|
|
296
416
|
}
|
|
297
417
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
418
|
+
function handle(rawInput) {
|
|
419
|
+
let input = String(rawInput || "");
|
|
420
|
+
if (input.endsWith("\\")) {
|
|
421
|
+
mlBuf.push(input.slice(0, -1));
|
|
422
|
+
ui.setPrompt("...");
|
|
423
|
+
return false;
|
|
302
424
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
} else if (ch === "\r") {
|
|
309
|
-
historyCol = 0;
|
|
310
|
-
} else {
|
|
311
|
-
historyCol += 1;
|
|
312
|
-
if (historyCol >= dockCols) historyCol = 0;
|
|
313
|
-
}
|
|
425
|
+
if (mlBuf.length > 0) {
|
|
426
|
+
mlBuf.push(input);
|
|
427
|
+
input = mlBuf.join("\n");
|
|
428
|
+
mlBuf = [];
|
|
429
|
+
ui.setPrompt("cofos");
|
|
314
430
|
}
|
|
315
|
-
redrawInput();
|
|
316
|
-
}
|
|
317
431
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
432
|
+
input = input.trim();
|
|
433
|
+
if (!input) return false;
|
|
434
|
+
|
|
435
|
+
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
436
|
+
if (/^\/(help)$/.test(input)) {
|
|
437
|
+
ui.print(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.");
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (/^\/(clear)$/.test(input)) {
|
|
441
|
+
mlBuf = [];
|
|
442
|
+
responseOpen = false;
|
|
443
|
+
proc.stdin.write(input + "\n");
|
|
444
|
+
generating = true;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
if (/^\/(info)$/.test(input)) {
|
|
448
|
+
ui.print(`\n COFOS CLI — v${pkg.version}\n Model: ${modelPath} (Hugging Face)\n RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence\n Cache: ${cacheDir}\n HF: ${backendEnv.HF_HOME}\n Engine: StudentAdapterInference + transformers + PyTorch\n Hist: ~/.cofos_history.jsonl\n`);
|
|
449
|
+
return false;
|
|
323
450
|
}
|
|
324
|
-
writeHistory(data);
|
|
325
|
-
}
|
|
326
451
|
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
ui.print(G + " you " + R + input);
|
|
453
|
+
responseOpen = false;
|
|
454
|
+
generating = true;
|
|
455
|
+
proc.stdin.write(input + "\n");
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
329
458
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
459
|
+
ui.setSubmitHandler((line) => {
|
|
460
|
+
if (!ready || generating) inputQ.push(line);
|
|
461
|
+
else handle(line);
|
|
462
|
+
});
|
|
334
463
|
|
|
335
|
-
|
|
336
|
-
proc.stderr.on("data", (chunk) => { preserveInputWrite(process.stderr, chunk); });
|
|
464
|
+
proc.stderr.on("data", (chunk) => ui.append(chunk));
|
|
337
465
|
|
|
338
466
|
proc.stdout.on("data", (chunk) => {
|
|
339
467
|
buf += chunk;
|
|
@@ -347,14 +475,11 @@ function main() {
|
|
|
347
475
|
}
|
|
348
476
|
return;
|
|
349
477
|
}
|
|
350
|
-
// Anything arriving while no answer is in flight is not part of a response;
|
|
351
|
-
// dropping it keeps it from being prepended to the next one.
|
|
352
478
|
if (!generating) { buf = ""; return; }
|
|
353
479
|
|
|
354
480
|
if (!responseOpen) {
|
|
355
481
|
const ri = buf.indexOf(RESPONSE);
|
|
356
482
|
if (ri === -1) {
|
|
357
|
-
// Keep enough tail to catch a RESPONSE marker split across chunks.
|
|
358
483
|
const keep = RESPONSE.length - 1;
|
|
359
484
|
if (buf.length > keep) buf = buf.slice(-keep);
|
|
360
485
|
return;
|
|
@@ -364,119 +489,34 @@ function main() {
|
|
|
364
489
|
responseOpen = true;
|
|
365
490
|
}
|
|
366
491
|
|
|
367
|
-
// Scan for DONE marker — stream everything before it.
|
|
368
492
|
const di = buf.indexOf(DONE);
|
|
369
493
|
if (di !== -1) {
|
|
370
494
|
const pre = buf.slice(0, di);
|
|
371
|
-
if (pre)
|
|
495
|
+
if (pre) ui.append(pre);
|
|
372
496
|
buf = buf.slice(di + DONE.length);
|
|
373
497
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
374
498
|
generating = false;
|
|
375
499
|
responseOpen = false;
|
|
376
|
-
|
|
500
|
+
ui.append("\n");
|
|
377
501
|
drain();
|
|
378
502
|
} else {
|
|
379
|
-
// Stream buffered content, keeping enough tail to catch a partial DONE marker.
|
|
380
503
|
const keep = DONE.length - 1;
|
|
381
504
|
if (buf.length > keep) {
|
|
382
505
|
const out = buf.slice(0, -keep);
|
|
383
|
-
if (out)
|
|
506
|
+
if (out) ui.append(out);
|
|
384
507
|
buf = buf.slice(-keep);
|
|
385
508
|
}
|
|
386
509
|
}
|
|
387
510
|
});
|
|
388
511
|
|
|
389
|
-
proc.on("error", (e) => {
|
|
512
|
+
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
390
513
|
proc.on("exit", (c) => {
|
|
391
514
|
closed = true;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
try { rl.close(); } catch (_) {}
|
|
395
|
-
// Let queued stdout writes flush before tearing the process down.
|
|
515
|
+
if (c && c !== 0) ui.print("[cofos] Python exited with code " + c);
|
|
516
|
+
ui.destroy();
|
|
396
517
|
process.exitCode = c ?? 0;
|
|
397
518
|
});
|
|
398
519
|
|
|
399
|
-
function shutdown() {
|
|
400
|
-
if (closed) return;
|
|
401
|
-
closed = true;
|
|
402
|
-
resetDock();
|
|
403
|
-
try { proc.stdin.end(); } catch (_) {}
|
|
404
|
-
try { rl.close(); } catch (_) {}
|
|
405
|
-
proc.kill("SIGTERM");
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Ctrl+C and Ctrl+D should stop the backend instead of orphaning it.
|
|
409
|
-
rl.on("SIGINT", () => { console.log("\n[cofos] Interrupted."); shutdown(); });
|
|
410
|
-
rl.on("close", () => { if (!closed) shutdown(); });
|
|
411
|
-
process.on("SIGTERM", shutdown);
|
|
412
|
-
process.on("exit", resetDock);
|
|
413
|
-
|
|
414
|
-
function drain() {
|
|
415
|
-
while (inputQ.length) {
|
|
416
|
-
const line = inputQ.shift();
|
|
417
|
-
if (handle(line)) return;
|
|
418
|
-
}
|
|
419
|
-
ask();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function ask(prompt = mainPrompt) {
|
|
423
|
-
if (closed) return;
|
|
424
|
-
try {
|
|
425
|
-
rl.setPrompt(prompt);
|
|
426
|
-
rl.prompt();
|
|
427
|
-
} catch (_) {}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/* ─── multi-line accumulator ─── */
|
|
431
|
-
let mlBuf = [];
|
|
432
|
-
|
|
433
|
-
function handle(input) {
|
|
434
|
-
// Multi-line continuation: lines ending with "\"
|
|
435
|
-
if (input.endsWith("\\")) {
|
|
436
|
-
mlBuf.push(input.slice(0, -1));
|
|
437
|
-
ask(contPrompt);
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
if (mlBuf.length > 0) {
|
|
441
|
-
mlBuf.push(input);
|
|
442
|
-
input = mlBuf.join("\n");
|
|
443
|
-
mlBuf = [];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
input = input.trim();
|
|
447
|
-
if (!input) return false;
|
|
448
|
-
|
|
449
|
-
// Local-only commands (don't hit the backend).
|
|
450
|
-
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
451
|
-
if (/^\/(help)$/.test(input)) {
|
|
452
|
-
console.log(slashHint("/") + "\n Add local papers to RAG:\n cofos --pdf-dir ./new_pdfs\n cofos --pdf-dir ./new_pdfs --rebuild-pdf-index\n\n Multi-line: end a line with \\ to continue.\n");
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
if (/^\/(clear)$/.test(input)) {
|
|
456
|
-
mlBuf = [];
|
|
457
|
-
responseOpen = false;
|
|
458
|
-
proc.stdin.write(input + "\n");
|
|
459
|
-
generating = true;
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
if (/^\/(info)$/.test(input)) {
|
|
463
|
-
console.log(`\n COFOS CLI — v${pkg.version}`);
|
|
464
|
-
console.log(" Model: Willlzh/COFOS (Hugging Face)");
|
|
465
|
-
console.log(" RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence");
|
|
466
|
-
console.log(` Cache: ${cacheDir}`);
|
|
467
|
-
console.log(` HF: ${backendEnv.HF_HOME}`);
|
|
468
|
-
console.log(" Engine: StudentAdapterInference + transformers + PyTorch");
|
|
469
|
-
console.log(" Hist: ~/.cofos_history.jsonl\n");
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Forward to backend. Echo a stable user turn so pasted/submitted questions remain visible in the transcript.
|
|
474
|
-
console.log(G + " you " + R + input);
|
|
475
|
-
responseOpen = false;
|
|
476
|
-
generating = true;
|
|
477
|
-
proc.stdin.write(input + "\n");
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
520
|
}
|
|
481
521
|
|
|
482
522
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzhzzzzwill/cofos",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,5 +38,8 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"blessed": "^0.1.81"
|
|
41
44
|
}
|
|
42
45
|
}
|