@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 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`, or
143
- `python scripts/sync_fwdemo_runtime.py`.
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 { clearLine, createInterface, cursorTo } from "node:readline";
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
- console.log(lines.join("\n"));
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
- console.log("[cofos] Checking Python dependencies …");
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
- console.log("[cofos] Installing Python dependencies (first run, may take a while) …");
149
- const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "inherit" });
150
- if (r.status !== 0) { console.error("[cofos] Failed to install Python dependencies."); process.exit(1); }
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
- console.log(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
229
- if (pdfDir) console.log(" Local PDF RAG: enabled for this session");
230
- else console.log(" Tip: add local papers with `cofos --pdf-dir ./new_pdfs`");
231
- console.log("");
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
- const mainPrompt = C + " cofos " + R;
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 resetDock() {
277
- if (docked) process.stdout.write("\x1b[r");
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
- function setupDock() {
281
- if (!docked) return;
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 redrawInput() {
292
- if (!docked || closed) return;
293
- cursorTo(rl.output, 0, dockRows - 1);
294
- clearLine(rl.output, 0);
295
- rl.prompt(true);
403
+ function drain() {
404
+ while (inputQ.length) {
405
+ const line = inputQ.shift();
406
+ if (handle(line)) return;
407
+ }
296
408
  }
297
409
 
298
- function writeHistory(data) {
299
- if (!docked) {
300
- process.stdout.write(data);
301
- return;
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
- cursorTo(process.stdout, historyCol, historyRow);
304
- for (const ch of data) {
305
- process.stdout.write(ch);
306
- if (ch === "\n") {
307
- historyCol = 0;
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
- function preserveInputWrite(stream, chunk) {
319
- const data = String(chunk);
320
- if (!rl.terminal || closed) {
321
- stream.write(data);
322
- return;
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
- setupDock();
328
- process.stdout.on("resize", () => { setupDock(); redrawInput(); });
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
- // Let users type while the Python backend is still loading. Submitted lines
331
- // are queued until READY, while status logs are written above and the input
332
- // line is restored afterward.
333
- ask();
451
+ ui.setSubmitHandler((line) => {
452
+ if (!ready || generating) inputQ.push(line);
453
+ else handle(line);
454
+ });
334
455
 
335
- // Forward Python stderr (status / progress messages) without destroying the input line.
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) preserveInputWrite(process.stdout, 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
- preserveInputWrite(process.stdout, "\n");
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) preserveInputWrite(process.stdout, 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) => { console.error("[cofos] Python error:", e.message); process.exit(1); });
504
+ proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
390
505
  proc.on("exit", (c) => {
391
506
  closed = true;
392
- resetDock();
393
- if (c && c !== 0) console.error("[cofos] Python exited with code", c);
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.1.19",
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
  }