@lzhzzzzwill/cofos 1.1.15 → 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";
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = process.env.COFOS_CONFIG || "config/config.yaml";
16
16
  /* ─── ANSI ─── */
17
17
  const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
18
18
  const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m", Y = "\x1b[93m";
19
+ const M = "\x1b[95m", BL = "\x1b[94m", GR = "\x1b[92m", A = "\x1b[38;5;208m";
19
20
 
20
21
  /* ─── Protocol markers ─── */
21
22
  const READY = "===READY===", RESPONSE = "===RESPONSE===", DONE = "===DONE===";
@@ -30,9 +31,16 @@ const ART_W = ART[0].length;
30
31
 
31
32
  function vis(t) { return t.replace(/\x1b\[[0-9;]*m/g, "").length; }
32
33
  function fw() { return Math.min(Math.max((process.stdout.columns || 110) - 2, 82), 150); }
33
- function top(w) { return "╭" + "─".repeat(w) + ""; }
34
- function bot(w) { return "╰" + "".repeat(w) + "╯"; }
35
- function sep(w) { return "├" + "─".repeat(w) + "┤"; }
34
+ function plain(t) { return t.replace(/\x1b\[[0-9;]*m/g, ""); }
35
+ function topTitle(w, left, right = "") {
36
+ const l = ` ${C}${B}${left}${R} `;
37
+ const r = right ? ` ${G}${right}${R} ` : "";
38
+ const room = Math.max(0, w - vis(l) - vis(r));
39
+ return C + "╭" + R + l + C + "─".repeat(room) + R + r + C + "╮" + R;
40
+ }
41
+ function top(w) { return C + "╭" + "─".repeat(w) + "╮" + R; }
42
+ function bot(w) { return C + "╰" + "─".repeat(w) + "╯" + R; }
43
+ function sep(w) { return C + "├" + "─".repeat(w) + "┤" + R; }
36
44
  function vpad(t, w) { return t + " ".repeat(Math.max(0, w - vis(t))); }
37
45
  function clip(t, w) {
38
46
  const plain = t.replace(/\x1b\[[0-9;]*m/g, "");
@@ -41,17 +49,16 @@ function clip(t, w) {
41
49
  return plain.slice(0, w - 1) + "…";
42
50
  }
43
51
  function cell(t, w) { return vpad(clip(t, w), w); }
44
- function row(left, right, lw, rw) { return "│ " + cell(left, lw) + " │ " + cell(right, rw) + " │"; }
45
- function full(t, w) { return "│ " + cell(t, w - 2) + " │"; }
52
+ function row(left, right, lw, rw) { return C + "│ " + R + cell(left, lw) + C + " │ " + R + cell(right, rw) + C + " │" + R; }
53
+ function full(t, w) { return C + "│ " + R + cell(t, w - 2) + C + " │" + R; }
46
54
  function empty(w) { return full("", w); }
47
55
 
48
56
  function logoLines(width) {
49
- if (width >= ART_W) return ART.map((line) => C + B + line + R);
57
+ if (width >= ART_W) return ART.map((line, i) => (i < 2 ? C : i < 4 ? BL : M) + B + line + R);
50
58
  return [C + B + "COFOS" + R];
51
59
  }
52
60
 
53
61
  function render({ modelPath, cacheDir, pdfDir }) {
54
- console.clear();
55
62
  const w = fw();
56
63
  const minSplit = 96;
57
64
  const cwd = process.cwd();
@@ -60,25 +67,26 @@ function render({ modelPath, cacheDir, pdfDir }) {
60
67
  ...logoLines(Math.floor((w - 5) * 0.48)),
61
68
  "",
62
69
  G + "Scientific RAG CLI" + R,
63
- W + "Model" + R + " " + modelPath,
64
- W + "Params" + R + " 9B",
65
- W + "Path" + R + " " + cwd,
66
- W + "Cache" + R + " " + cacheDir,
70
+ Y + "Model" + R + " " + W + modelPath + R,
71
+ Y + "Params" + R + " " + W + "9B" + R,
72
+ Y + "Path" + R + " " + W + cwd + R,
73
+ Y + "Cache" + R + " " + W + cacheDir + R,
67
74
  ];
68
75
  const right = [
69
- W + B + "What COFOS Does" + R,
76
+ GR + B + "What COFOS Does" + R,
70
77
  "COFOS answers scientific questions about covalent organic frameworks,",
71
78
  "photocatalysis, reactive oxygen species, and oxygen-reduction products.",
72
79
  rag,
73
80
  "",
74
- W + B + "Suggested Commands" + R,
75
- "/help show commands and PDF-RAG usage",
76
- "/topk 5 change retrieval depth",
77
- "/rag off answer without retrieval",
78
- "cofos --pdf-dir ./new_pdfs add local papers",
81
+ A + B + "Suggested Commands" + R,
82
+ C + "/help" + R + " show commands and PDF-RAG usage",
83
+ C + "/topk 5" + R + " change retrieval depth",
84
+ C + "/rag off" + R + " answer without retrieval",
85
+ M + "cofos --pdf-dir ./new_pdfs" + R + " add local papers",
79
86
  ];
80
87
 
81
- const lines = [top(w)];
88
+ const titleLeft = `COFOS CLI v${pkg.version}`;
89
+ const lines = [topTitle(w, titleLeft)];
82
90
  if (w >= minSplit) {
83
91
  const inner = w - 5;
84
92
  const lw = Math.max(46, Math.floor(inner * 0.48));
@@ -91,7 +99,7 @@ function render({ modelPath, cacheDir, pdfDir }) {
91
99
  for (const item of right) lines.push(full(item, w));
92
100
  }
93
101
  lines.push(bot(w));
94
- console.log(lines.join("\n"));
102
+ return lines.join("\n");
95
103
  }
96
104
 
97
105
  /* ─── Slash commands ─── */
@@ -133,12 +141,149 @@ function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python")
133
141
  // Only the modules the chat backend actually imports for the standalone merged-model path.
134
142
  const REQUIRED_MODULES = "import transformers, torch, yaml, huggingface_hub, pdfplumber, rank_bm25";
135
143
 
136
- function ensureDeps(py) {
137
- console.log("[cofos] Checking Python dependencies …");
144
+ function ensureDeps(py, log = console.log) {
145
+ log("[cofos] Checking Python dependencies …");
138
146
  if (spawnSync(py, ["-c", `${REQUIRED_MODULES}; print('ok')`], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
139
- console.log("[cofos] Installing Python dependencies (first run, may take a while) …");
140
- const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "inherit" });
141
- 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
+ };
142
287
  }
143
288
 
144
289
  /* ─── main ─── */
@@ -209,17 +354,17 @@ function main() {
209
354
  if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
210
355
  if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
211
356
 
212
- render({ modelPath, cacheDir, pdfDir });
213
-
214
357
  const py = findPy();
215
358
  if (!py) die("Python not found. Install Python 3.9+ and re-run.");
216
359
  if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
217
- ensureDeps(py);
218
360
 
219
- console.log(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
220
- if (pdfDir) console.log(" Local PDF RAG: enabled for this session");
221
- else console.log(" Tip: add local papers with `cofos --pdf-dir ./new_pdfs`");
222
- 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("");
223
368
 
224
369
  const backendEnv = {
225
370
  ...process.env,
@@ -237,50 +382,78 @@ function main() {
237
382
  env: backendEnv,
238
383
  });
239
384
 
240
- // `encoding` is not a spawn() option — without setEncoding each Buffer chunk
241
- // is decoded on its own, so a multi-byte character (·OH, ¹O₂, any Chinese)
242
- // straddling a chunk boundary comes out as mojibake.
243
385
  proc.stdout.setEncoding("utf-8");
244
386
  proc.stderr.setEncoding("utf-8");
245
387
 
246
388
  let buf = "", ready = false, generating = false, responseOpen = false, closed = false;
247
389
  const inputQ = [];
248
- const mainPrompt = C + " cofos " + R;
249
- const contPrompt = G + " ... " + R;
250
- const rl = createInterface({
251
- input: process.stdin,
252
- output: process.stdout,
253
- terminal: true,
254
- prompt: mainPrompt,
255
- completer: slashCompletions,
256
- });
257
- rl.on("line", (l) => {
258
- if (!ready || generating) inputQ.push(l);
259
- else if (!handle(l)) ask();
260
- });
390
+ let mlBuf = [];
261
391
 
262
- function preserveInputWrite(stream, chunk) {
263
- const data = String(chunk);
264
- if (!rl.terminal || closed) {
265
- stream.write(data);
266
- return;
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();
398
+ }
399
+
400
+ process.on("SIGTERM", shutdown);
401
+ process.on("SIGINT", shutdown);
402
+
403
+ function drain() {
404
+ while (inputQ.length) {
405
+ const line = inputQ.shift();
406
+ if (handle(line)) return;
407
+ }
408
+ }
409
+
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;
416
+ }
417
+ if (mlBuf.length > 0) {
418
+ mlBuf.push(input);
419
+ input = mlBuf.join("\n");
420
+ mlBuf = [];
421
+ ui.setPrompt("cofos");
422
+ }
423
+
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;
267
431
  }
268
- const activeInput = !generating;
269
- if (activeInput) {
270
- clearLine(rl.output, 0);
271
- cursorTo(rl.output, 0);
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;
272
442
  }
273
- stream.write(data);
274
- if (activeInput) rl.prompt(true);
443
+
444
+ ui.print(G + " you " + R + input);
445
+ responseOpen = false;
446
+ generating = true;
447
+ proc.stdin.write(input + "\n");
448
+ return true;
275
449
  }
276
450
 
277
- // Let users type while the Python backend is still loading. Submitted lines
278
- // are queued until READY, while status logs are written above and the input
279
- // line is restored afterward.
280
- ask();
451
+ ui.setSubmitHandler((line) => {
452
+ if (!ready || generating) inputQ.push(line);
453
+ else handle(line);
454
+ });
281
455
 
282
- // Forward Python stderr (status / progress messages) without destroying the input line.
283
- proc.stderr.on("data", (chunk) => { preserveInputWrite(process.stderr, chunk); });
456
+ proc.stderr.on("data", (chunk) => ui.append(chunk));
284
457
 
285
458
  proc.stdout.on("data", (chunk) => {
286
459
  buf += chunk;
@@ -294,14 +467,11 @@ function main() {
294
467
  }
295
468
  return;
296
469
  }
297
- // Anything arriving while no answer is in flight is not part of a response;
298
- // dropping it keeps it from being prepended to the next one.
299
470
  if (!generating) { buf = ""; return; }
300
471
 
301
472
  if (!responseOpen) {
302
473
  const ri = buf.indexOf(RESPONSE);
303
474
  if (ri === -1) {
304
- // Keep enough tail to catch a RESPONSE marker split across chunks.
305
475
  const keep = RESPONSE.length - 1;
306
476
  if (buf.length > keep) buf = buf.slice(-keep);
307
477
  return;
@@ -311,116 +481,34 @@ function main() {
311
481
  responseOpen = true;
312
482
  }
313
483
 
314
- // Scan for DONE marker — stream everything before it.
315
484
  const di = buf.indexOf(DONE);
316
485
  if (di !== -1) {
317
486
  const pre = buf.slice(0, di);
318
- if (pre) preserveInputWrite(process.stdout, pre);
487
+ if (pre) ui.append(pre);
319
488
  buf = buf.slice(di + DONE.length);
320
489
  if (buf[0] === "\n") buf = buf.slice(1);
321
490
  generating = false;
322
491
  responseOpen = false;
323
- preserveInputWrite(process.stdout, "\n");
492
+ ui.append("\n");
324
493
  drain();
325
494
  } else {
326
- // Stream buffered content, keeping enough tail to catch a partial DONE marker.
327
495
  const keep = DONE.length - 1;
328
496
  if (buf.length > keep) {
329
497
  const out = buf.slice(0, -keep);
330
- if (out) preserveInputWrite(process.stdout, out);
498
+ if (out) ui.append(out);
331
499
  buf = buf.slice(-keep);
332
500
  }
333
501
  }
334
502
  });
335
503
 
336
- 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(); });
337
505
  proc.on("exit", (c) => {
338
506
  closed = true;
339
- if (c && c !== 0) console.error("[cofos] Python exited with code", c);
340
- try { rl.close(); } catch (_) {}
341
- // 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();
342
509
  process.exitCode = c ?? 0;
343
510
  });
344
511
 
345
- function shutdown() {
346
- if (closed) return;
347
- closed = true;
348
- try { proc.stdin.end(); } catch (_) {}
349
- try { rl.close(); } catch (_) {}
350
- proc.kill("SIGTERM");
351
- }
352
-
353
- // Ctrl+C and Ctrl+D should stop the backend instead of orphaning it.
354
- rl.on("SIGINT", () => { console.log("\n[cofos] Interrupted."); shutdown(); });
355
- rl.on("close", () => { if (!closed) shutdown(); });
356
- process.on("SIGTERM", shutdown);
357
-
358
- function drain() {
359
- while (inputQ.length) {
360
- const line = inputQ.shift();
361
- if (handle(line)) return;
362
- }
363
- ask();
364
- }
365
-
366
- function ask(prompt = mainPrompt) {
367
- if (closed) return;
368
- try {
369
- rl.setPrompt(prompt);
370
- rl.prompt();
371
- } catch (_) {}
372
- }
373
-
374
- /* ─── multi-line accumulator ─── */
375
- let mlBuf = [];
376
-
377
- function handle(input) {
378
- // Multi-line continuation: lines ending with "\"
379
- if (input.endsWith("\\")) {
380
- mlBuf.push(input.slice(0, -1));
381
- ask(contPrompt);
382
- return true;
383
- }
384
- if (mlBuf.length > 0) {
385
- mlBuf.push(input);
386
- input = mlBuf.join("\n");
387
- mlBuf = [];
388
- }
389
-
390
- input = input.trim();
391
- if (!input) return false;
392
-
393
- // Local-only commands (don't hit the backend).
394
- if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return true; }
395
- if (/^\/(help)$/.test(input)) {
396
- 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");
397
- return false;
398
- }
399
- if (/^\/(clear)$/.test(input)) {
400
- mlBuf = [];
401
- responseOpen = false;
402
- proc.stdin.write(input + "\n");
403
- generating = true;
404
- return true;
405
- }
406
- if (/^\/(info)$/.test(input)) {
407
- console.log(`\n COFOS CLI — v${pkg.version}`);
408
- console.log(" Model: Willlzh/COFOS (Hugging Face)");
409
- console.log(" RAG: Willlzh/COFOS_data/runtime (KG + BM25) + optional --pdf-dir BM25 evidence");
410
- console.log(` Cache: ${cacheDir}`);
411
- console.log(` HF: ${backendEnv.HF_HOME}`);
412
- console.log(" Engine: StudentAdapterInference + transformers + PyTorch");
413
- console.log(" Hist: ~/.cofos_history.jsonl\n");
414
- return false;
415
- }
416
-
417
- // Forward to backend. Echo a stable user turn so pasted/submitted questions remain visible in the transcript.
418
- console.log(G + " you " + R + input);
419
- responseOpen = false;
420
- generating = true;
421
- proc.stdin.write(input + "\n");
422
- return true;
423
- }
424
512
  }
425
513
 
426
514
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.1.15",
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
  }