@lzhzzzzwill/cofos 1.1.19 → 1.2.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 +5 -2
- package/RUNTIME.md +14 -0
- package/bin/cofos.js +211 -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,149 @@ 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
|
+
border: { type: "line" },
|
|
175
|
+
style: {
|
|
176
|
+
border: { fg: "cyan" },
|
|
177
|
+
scrollbar: { bg: "cyan" },
|
|
178
|
+
},
|
|
179
|
+
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const suggestions = blessed.box({
|
|
183
|
+
bottom: 3,
|
|
184
|
+
left: 2,
|
|
185
|
+
width: "70%",
|
|
186
|
+
height: 6,
|
|
187
|
+
hidden: true,
|
|
188
|
+
border: { type: "line" },
|
|
189
|
+
tags: true,
|
|
190
|
+
style: { border: { fg: "yellow" }, fg: "white", bg: "black" },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const input = blessed.textbox({
|
|
194
|
+
bottom: 0,
|
|
195
|
+
left: 0,
|
|
196
|
+
width: "100%",
|
|
197
|
+
height: 3,
|
|
198
|
+
inputOnFocus: true,
|
|
199
|
+
keys: true,
|
|
200
|
+
mouse: true,
|
|
201
|
+
border: { type: "line" },
|
|
202
|
+
label: " cofos ",
|
|
203
|
+
style: {
|
|
204
|
+
border: { fg: "cyan" },
|
|
205
|
+
focus: { border: { fg: "green" } },
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
screen.append(history);
|
|
210
|
+
screen.append(suggestions);
|
|
211
|
+
screen.append(input);
|
|
212
|
+
|
|
213
|
+
let buffer = "";
|
|
214
|
+
let onSubmit = () => {};
|
|
215
|
+
|
|
216
|
+
function append(text = "") {
|
|
217
|
+
buffer += String(text).replace(/\r/g, "");
|
|
218
|
+
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
219
|
+
history.setContent(buffer);
|
|
220
|
+
history.setScrollPerc(100);
|
|
221
|
+
screen.render();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hideSuggestions() {
|
|
225
|
+
if (!suggestions.hidden) {
|
|
226
|
+
suggestions.hide();
|
|
227
|
+
screen.render();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function updateSuggestions() {
|
|
232
|
+
const value = input.getValue() || "";
|
|
233
|
+
if (!value.startsWith("/")) {
|
|
234
|
+
hideSuggestions();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const matches = slashMatches(value);
|
|
238
|
+
if (!matches.length) {
|
|
239
|
+
hideSuggestions();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
suggestions.setContent(matches.map(({ cmd, desc }) => `{cyan-fg}${cmd}{/cyan-fg} ${desc}`).join("\n"));
|
|
243
|
+
suggestions.height = Math.min(8, matches.length + 2);
|
|
244
|
+
suggestions.show();
|
|
245
|
+
screen.render();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
input.on("submit", (value) => {
|
|
249
|
+
hideSuggestions();
|
|
250
|
+
input.clearValue();
|
|
251
|
+
screen.render();
|
|
252
|
+
onSubmit(String(value || ""));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
input.on("keypress", () => setTimeout(updateSuggestions, 0));
|
|
256
|
+
|
|
257
|
+
screen.key(["tab"], () => {
|
|
258
|
+
const value = input.getValue() || "";
|
|
259
|
+
if (!value.startsWith("/")) return;
|
|
260
|
+
const matches = slashMatches(value);
|
|
261
|
+
if (!matches.length) return;
|
|
262
|
+
if (matches.length === 1) {
|
|
263
|
+
input.setValue(matches[0].cmd);
|
|
264
|
+
hideSuggestions();
|
|
265
|
+
input.focus();
|
|
266
|
+
screen.render();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
updateSuggestions();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
screen.key(["escape"], hideSuggestions);
|
|
273
|
+
screen.key(["C-c"], () => process.emit("SIGINT"));
|
|
274
|
+
|
|
275
|
+
input.focus();
|
|
276
|
+
screen.render();
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
screen,
|
|
280
|
+
input,
|
|
281
|
+
append,
|
|
282
|
+
print: (text = "") => append(String(text) + "\n"),
|
|
283
|
+
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
284
|
+
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
285
|
+
destroy: () => screen.destroy(),
|
|
286
|
+
};
|
|
151
287
|
}
|
|
152
288
|
|
|
153
289
|
/* ─── main ─── */
|
|
@@ -218,17 +354,17 @@ function main() {
|
|
|
218
354
|
if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
|
|
219
355
|
if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
|
|
220
356
|
|
|
221
|
-
render({ modelPath, cacheDir, pdfDir });
|
|
222
|
-
|
|
223
357
|
const py = findPy();
|
|
224
358
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
225
359
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
226
|
-
ensureDeps(py);
|
|
227
360
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
361
|
+
const ui = createTui();
|
|
362
|
+
ui.print(plain(render({ modelPath, cacheDir, pdfDir })));
|
|
363
|
+
ensureDeps(py, (msg) => ui.print(msg));
|
|
364
|
+
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
365
|
+
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
366
|
+
else ui.print(" Tip: add local papers with `cofos --pdf-dir ./new_pdfs`");
|
|
367
|
+
ui.print("");
|
|
232
368
|
|
|
233
369
|
const backendEnv = {
|
|
234
370
|
...process.env,
|
|
@@ -246,94 +382,78 @@ function main() {
|
|
|
246
382
|
env: backendEnv,
|
|
247
383
|
});
|
|
248
384
|
|
|
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
385
|
proc.stdout.setEncoding("utf-8");
|
|
253
386
|
proc.stderr.setEncoding("utf-8");
|
|
254
387
|
|
|
255
388
|
let buf = "", ready = false, generating = false, responseOpen = false, closed = false;
|
|
256
389
|
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
|
-
});
|
|
390
|
+
let mlBuf = [];
|
|
275
391
|
|
|
276
|
-
function
|
|
277
|
-
if (
|
|
392
|
+
function shutdown() {
|
|
393
|
+
if (closed) return;
|
|
394
|
+
closed = true;
|
|
395
|
+
try { proc.stdin.end(); } catch (_) {}
|
|
396
|
+
try { proc.kill("SIGTERM"); } catch (_) {}
|
|
397
|
+
ui.destroy();
|
|
278
398
|
}
|
|
279
399
|
|
|
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
|
-
}
|
|
400
|
+
process.on("SIGTERM", shutdown);
|
|
401
|
+
process.on("SIGINT", shutdown);
|
|
290
402
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
403
|
+
function drain() {
|
|
404
|
+
while (inputQ.length) {
|
|
405
|
+
const line = inputQ.shift();
|
|
406
|
+
if (handle(line)) return;
|
|
407
|
+
}
|
|
296
408
|
}
|
|
297
409
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
410
|
+
function handle(rawInput) {
|
|
411
|
+
let input = String(rawInput || "");
|
|
412
|
+
if (input.endsWith("\\")) {
|
|
413
|
+
mlBuf.push(input.slice(0, -1));
|
|
414
|
+
ui.setPrompt("...");
|
|
415
|
+
return false;
|
|
302
416
|
}
|
|
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
|
-
}
|
|
417
|
+
if (mlBuf.length > 0) {
|
|
418
|
+
mlBuf.push(input);
|
|
419
|
+
input = mlBuf.join("\n");
|
|
420
|
+
mlBuf = [];
|
|
421
|
+
ui.setPrompt("cofos");
|
|
314
422
|
}
|
|
315
|
-
redrawInput();
|
|
316
|
-
}
|
|
317
423
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
424
|
+
input = input.trim();
|
|
425
|
+
if (!input) return false;
|
|
426
|
+
|
|
427
|
+
if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
|
|
428
|
+
if (/^\/(help)$/.test(input)) {
|
|
429
|
+
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.");
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (/^\/(clear)$/.test(input)) {
|
|
433
|
+
mlBuf = [];
|
|
434
|
+
responseOpen = false;
|
|
435
|
+
proc.stdin.write(input + "\n");
|
|
436
|
+
generating = true;
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
if (/^\/(info)$/.test(input)) {
|
|
440
|
+
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`);
|
|
441
|
+
return false;
|
|
323
442
|
}
|
|
324
|
-
writeHistory(data);
|
|
325
|
-
}
|
|
326
443
|
|
|
327
|
-
|
|
328
|
-
|
|
444
|
+
ui.print(G + " you " + R + input);
|
|
445
|
+
responseOpen = false;
|
|
446
|
+
generating = true;
|
|
447
|
+
proc.stdin.write(input + "\n");
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
329
450
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
451
|
+
ui.setSubmitHandler((line) => {
|
|
452
|
+
if (!ready || generating) inputQ.push(line);
|
|
453
|
+
else handle(line);
|
|
454
|
+
});
|
|
334
455
|
|
|
335
|
-
|
|
336
|
-
proc.stderr.on("data", (chunk) => { preserveInputWrite(process.stderr, chunk); });
|
|
456
|
+
proc.stderr.on("data", (chunk) => ui.append(chunk));
|
|
337
457
|
|
|
338
458
|
proc.stdout.on("data", (chunk) => {
|
|
339
459
|
buf += chunk;
|
|
@@ -347,14 +467,11 @@ function main() {
|
|
|
347
467
|
}
|
|
348
468
|
return;
|
|
349
469
|
}
|
|
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
470
|
if (!generating) { buf = ""; return; }
|
|
353
471
|
|
|
354
472
|
if (!responseOpen) {
|
|
355
473
|
const ri = buf.indexOf(RESPONSE);
|
|
356
474
|
if (ri === -1) {
|
|
357
|
-
// Keep enough tail to catch a RESPONSE marker split across chunks.
|
|
358
475
|
const keep = RESPONSE.length - 1;
|
|
359
476
|
if (buf.length > keep) buf = buf.slice(-keep);
|
|
360
477
|
return;
|
|
@@ -364,119 +481,34 @@ function main() {
|
|
|
364
481
|
responseOpen = true;
|
|
365
482
|
}
|
|
366
483
|
|
|
367
|
-
// Scan for DONE marker — stream everything before it.
|
|
368
484
|
const di = buf.indexOf(DONE);
|
|
369
485
|
if (di !== -1) {
|
|
370
486
|
const pre = buf.slice(0, di);
|
|
371
|
-
if (pre)
|
|
487
|
+
if (pre) ui.append(pre);
|
|
372
488
|
buf = buf.slice(di + DONE.length);
|
|
373
489
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
374
490
|
generating = false;
|
|
375
491
|
responseOpen = false;
|
|
376
|
-
|
|
492
|
+
ui.append("\n");
|
|
377
493
|
drain();
|
|
378
494
|
} else {
|
|
379
|
-
// Stream buffered content, keeping enough tail to catch a partial DONE marker.
|
|
380
495
|
const keep = DONE.length - 1;
|
|
381
496
|
if (buf.length > keep) {
|
|
382
497
|
const out = buf.slice(0, -keep);
|
|
383
|
-
if (out)
|
|
498
|
+
if (out) ui.append(out);
|
|
384
499
|
buf = buf.slice(-keep);
|
|
385
500
|
}
|
|
386
501
|
}
|
|
387
502
|
});
|
|
388
503
|
|
|
389
|
-
proc.on("error", (e) => {
|
|
504
|
+
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
390
505
|
proc.on("exit", (c) => {
|
|
391
506
|
closed = true;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
try { rl.close(); } catch (_) {}
|
|
395
|
-
// Let queued stdout writes flush before tearing the process down.
|
|
507
|
+
if (c && c !== 0) ui.print("[cofos] Python exited with code " + c);
|
|
508
|
+
ui.destroy();
|
|
396
509
|
process.exitCode = c ?? 0;
|
|
397
510
|
});
|
|
398
511
|
|
|
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
512
|
}
|
|
481
513
|
|
|
482
514
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lzhzzzzwill/cofos",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
}
|