@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 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,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
- 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
+ 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
- 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("");
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
- 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
- });
398
+ let mlBuf = [];
275
399
 
276
- function resetDock() {
277
- if (docked) process.stdout.write("\x1b[r");
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
- 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
- }
408
+ process.on("SIGTERM", shutdown);
409
+ process.on("SIGINT", shutdown);
290
410
 
291
- function redrawInput() {
292
- if (!docked || closed) return;
293
- cursorTo(rl.output, 0, dockRows - 1);
294
- clearLine(rl.output, 0);
295
- rl.prompt(true);
411
+ function drain() {
412
+ while (inputQ.length) {
413
+ const line = inputQ.shift();
414
+ if (handle(line)) return;
415
+ }
296
416
  }
297
417
 
298
- function writeHistory(data) {
299
- if (!docked) {
300
- process.stdout.write(data);
301
- return;
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
- 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
- }
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
- function preserveInputWrite(stream, chunk) {
319
- const data = String(chunk);
320
- if (!rl.terminal || closed) {
321
- stream.write(data);
322
- return;
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
- setupDock();
328
- process.stdout.on("resize", () => { setupDock(); redrawInput(); });
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
- // 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();
459
+ ui.setSubmitHandler((line) => {
460
+ if (!ready || generating) inputQ.push(line);
461
+ else handle(line);
462
+ });
334
463
 
335
- // Forward Python stderr (status / progress messages) without destroying the input line.
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) preserveInputWrite(process.stdout, 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
- preserveInputWrite(process.stdout, "\n");
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) preserveInputWrite(process.stdout, 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) => { console.error("[cofos] Python error:", e.message); process.exit(1); });
512
+ proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
390
513
  proc.on("exit", (c) => {
391
514
  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.
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.19",
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
  }