@lzhzzzzwill/cofos 1.3.3 → 1.3.6

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
@@ -6,7 +6,7 @@ COFOS CLI is a standalone Node.js command line wrapper around the COFOS Python r
6
6
  npx @lzhzzzzwill/cofos
7
7
  ```
8
8
 
9
- By default COFOS uses a plain terminal interface, so normal terminal scrollback, mouse selection, copy, and `Ctrl+C` work as expected. The fixed-input full-screen interface is still available as an experimental mode with `--tui`.
9
+ COFOS uses a plain terminal interface, so normal terminal scrollback, mouse selection, copy, and `Ctrl+C` work as expected.
10
10
 
11
11
  ## Runtime Model
12
12
 
@@ -71,18 +71,19 @@ Disable RAG for a session:
71
71
  npx @lzhzzzzwill/cofos --no-rag
72
72
  ```
73
73
 
74
- Ask once and exit, useful for scripts and smoke tests:
74
+ Show model reasoning scaffold when emitted:
75
75
 
76
76
  ```bash
77
- npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
77
+ npx @lzhzzzzwill/cofos --show-reasoning
78
78
  ```
79
79
 
80
- Use the experimental fixed-input TUI:
80
+ Ask once and exit, useful for scripts and smoke tests:
81
81
 
82
82
  ```bash
83
- npx @lzhzzzzwill/cofos --tui
83
+ npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
84
84
  ```
85
85
 
86
+
86
87
  ## Startup Behavior
87
88
 
88
89
  On startup, the CLI does the following:
@@ -133,7 +134,7 @@ cofos \
133
134
  | `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
134
135
  | `--top-k` | Retrieval top-k |
135
136
  | `-p`, `--prompt` | Ask one question and exit |
136
- | `--tui` | Use the experimental fixed-input full-screen TUI |
137
+ | `--show-reasoning` | Show emitted reasoning scaffold in dim text before the final answer |
137
138
  | `--max-new-tokens` | Override generation length |
138
139
  | `--no-rag` | Run without KG/BM25 retrieval |
139
140
 
package/bin/cofos.js CHANGED
@@ -158,208 +158,7 @@ function ensureDeps(py, log = console.log) {
158
158
  if (r.status !== 0) { log("[cofos] Failed to install Python dependencies."); process.exit(1); }
159
159
  }
160
160
 
161
- /* ─── TUI ─── */
162
-
163
- async function createTui() {
164
- const { default: blessed } = await import("blessed");
165
- const screen = blessed.screen({
166
- smartCSR: true,
167
- fullUnicode: true,
168
- title: "COFOS",
169
- mouse: false,
170
- });
171
- screen.program.disableMouse();
172
-
173
- const history = blessed.box({
174
- top: 0,
175
- left: 0,
176
- width: "100%",
177
- height: "100%-3",
178
- scrollable: true,
179
- alwaysScroll: true,
180
- keys: true,
181
- vi: true,
182
- mouse: false,
183
- tags: true,
184
- scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
185
- });
186
-
187
- const suggestions = blessed.box({
188
- bottom: 3,
189
- left: 2,
190
- width: "70%",
191
- height: 6,
192
- hidden: true,
193
- border: { type: "line" },
194
- tags: true,
195
- style: { border: { fg: "yellow" }, fg: "white", bg: "black" },
196
- });
197
-
198
- const input = blessed.textbox({
199
- bottom: 0,
200
- left: 0,
201
- width: "100%",
202
- height: 3,
203
- inputOnFocus: true,
204
- keys: true,
205
- mouse: false,
206
- border: { type: "line" },
207
- label: " cofos ",
208
- style: {
209
- border: { fg: "cyan" },
210
- focus: { border: { fg: "green" } },
211
- },
212
- });
213
-
214
- screen.append(history);
215
- screen.append(suggestions);
216
- screen.append(input);
217
-
218
- let buffer = "";
219
- let onSubmit = () => {};
220
- let onExit = () => {};
221
- let submitting = false;
222
- let suggestionMatches = [];
223
- let selectedSuggestion = 0;
224
-
225
- function renderBuffer(wasAtBottom = true) {
226
- history.setContent(buffer);
227
- if (wasAtBottom) history.setScrollPerc(100);
228
- screen.render();
229
- }
230
-
231
- function append(text = "") {
232
- const wasAtBottom = history.getScrollPerc() >= 98;
233
- const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
234
- for (const part of data.split("\r")) {
235
- if (part === "") continue;
236
- if (data.includes("\r")) {
237
- const lastNewline = buffer.lastIndexOf("\n");
238
- buffer = (lastNewline === -1 ? "" : buffer.slice(0, lastNewline + 1)) + part;
239
- } else {
240
- buffer += part;
241
- }
242
- }
243
- if (buffer.length > 250000) buffer = buffer.slice(-220000);
244
- renderBuffer(wasAtBottom);
245
- }
246
-
247
- function hideSuggestions() {
248
- suggestionMatches = [];
249
- selectedSuggestion = 0;
250
- if (!suggestions.hidden) {
251
- suggestions.hide();
252
- screen.render();
253
- }
254
- }
255
-
256
- function renderSuggestions() {
257
- if (!suggestionMatches.length) {
258
- hideSuggestions();
259
- return;
260
- }
261
- selectedSuggestion = Math.max(0, Math.min(selectedSuggestion, suggestionMatches.length - 1));
262
- suggestions.setContent(suggestionMatches.map(({ cmd, desc }, i) => {
263
- const marker = i === selectedSuggestion ? "{black-fg}{yellow-bg}›" : " ";
264
- const close = i === selectedSuggestion ? "{/yellow-bg}{/black-fg}" : "";
265
- return `${marker} {cyan-fg}${cmd}{/cyan-fg} ${desc}${close}`;
266
- }).join("\n"));
267
- suggestions.height = Math.min(8, suggestionMatches.length + 2);
268
- suggestions.show();
269
- screen.render();
270
- }
271
-
272
- function updateSuggestions() {
273
- const value = input.getValue() || "";
274
- if (!value.startsWith("/")) {
275
- hideSuggestions();
276
- return;
277
- }
278
- suggestionMatches = slashMatches(value);
279
- selectedSuggestion = 0;
280
- renderSuggestions();
281
- }
282
-
283
- function selectedCommand() {
284
- return suggestionMatches[selectedSuggestion]?.cmd || "";
285
- }
286
-
287
- function submitInput() {
288
- if (submitting) return;
289
- submitting = true;
290
- let value = input.getValue();
291
- if (!suggestions.hidden && selectedCommand()) value = selectedCommand();
292
- hideSuggestions();
293
- input.clearValue();
294
- input.focus();
295
- screen.render();
296
- onSubmit(String(value || ""));
297
- setTimeout(() => { submitting = false; }, 0);
298
- }
299
-
300
- input.key(["enter"], submitInput);
301
- screen.key(["enter"], submitInput);
302
-
303
- input.on("keypress", (_ch, key = {}) => {
304
- if (["up", "down", "tab", "enter", "escape"].includes(key.name)) return;
305
- setTimeout(updateSuggestions, 0);
306
- });
307
-
308
- screen.key(["tab"], () => {
309
- const value = input.getValue() || "";
310
- if (!value.startsWith("/")) return;
311
- if (!suggestionMatches.length) suggestionMatches = slashMatches(value);
312
- if (!suggestionMatches.length) return;
313
- input.setValue(selectedCommand());
314
- hideSuggestions();
315
- input.focus();
316
- screen.render();
317
- });
318
-
319
- screen.key(["up"], () => {
320
- if (suggestions.hidden || !suggestionMatches.length) return;
321
- selectedSuggestion = (selectedSuggestion - 1 + suggestionMatches.length) % suggestionMatches.length;
322
- renderSuggestions();
323
- });
324
-
325
- screen.key(["down"], () => {
326
- if (suggestions.hidden || !suggestionMatches.length) return;
327
- selectedSuggestion = (selectedSuggestion + 1) % suggestionMatches.length;
328
- renderSuggestions();
329
- });
330
-
331
- screen.key(["escape"], hideSuggestions);
332
- screen.key(["pageup"], () => { history.scroll(-Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
333
- screen.key(["pagedown"], () => { history.scroll(Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
334
- screen.key(["home"], () => { history.setScrollPerc(0); screen.render(); });
335
- screen.key(["end"], () => { history.setScrollPerc(100); screen.render(); });
336
- screen.key(["C-c"], () => onExit());
337
-
338
- input.focus();
339
- screen.render();
340
-
341
- return {
342
- supportsReplace: true,
343
- echoUser: true,
344
- screen,
345
- input,
346
- append,
347
- print: (text = "") => append(String(text) + "\n"),
348
- clear: () => { buffer = ""; renderBuffer(true); },
349
- length: () => buffer.length,
350
- replaceFrom: (index, text = "") => { buffer = buffer.slice(0, Math.max(0, index)) + String(text); renderBuffer(true); },
351
- setSubmitHandler: (fn) => { onSubmit = fn; },
352
- setExitHandler: (fn) => { onExit = fn; },
353
- focus: () => { input.focus(); screen.render(); },
354
- setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
355
- destroy: () => {
356
- try { screen.program.disableMouse(); } catch (_) {}
357
- screen.destroy();
358
- },
359
- };
360
- }
361
-
362
- function blessedToAnsi(text) {
161
+ function markupToAnsi(text) {
363
162
  return String(text)
364
163
  .replace(/\{bold\}/g, B)
365
164
  .replace(/\{\/bold\}/g, R)
@@ -399,11 +198,11 @@ function createPlainUi() {
399
198
  return {
400
199
  supportsReplace: false,
401
200
  echoUser: false,
402
- append: (text = "") => process.stdout.write(blessedToAnsi(text)),
403
- print: (text = "") => console.log(blessedToAnsi(text)),
201
+ append: (text = "") => process.stdout.write(markupToAnsi(text)),
202
+ print: (text = "") => console.log(markupToAnsi(text)),
404
203
  clear: () => console.clear(),
405
204
  length: () => 0,
406
- replaceFrom: (_index, text = "") => process.stdout.write(blessedToAnsi(text)),
205
+ replaceFrom: (_index, text = "") => process.stdout.write(markupToAnsi(text)),
407
206
  setSubmitHandler: (fn) => { onSubmit = fn; },
408
207
  setExitHandler: (fn) => { onExit = fn; },
409
208
  focus: () => rl.prompt(),
@@ -417,7 +216,7 @@ function createPlainUi() {
417
216
  async function main() {
418
217
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
419
218
  console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
420
- console.log(" Usage: cofos [--tui] [-p QUESTION] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
219
+ console.log(" Usage: cofos [-p QUESTION] [--show-reasoning] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
421
220
  console.log(" Commands in chat:");
422
221
  console.log(" /help Show this help");
423
222
  console.log(" /exit Quit");
@@ -430,7 +229,7 @@ async function main() {
430
229
  console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
431
230
  console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
432
231
  console.log(" --max-new-tokens N Override generation length");
433
- console.log(" --tui Use the experimental fixed-input TUI");
232
+ console.log(" --show-reasoning Show model reasoning scaffold in dim text when emitted");
434
233
  console.log(" -p, --prompt TEXT Ask once and exit");
435
234
  console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
436
235
  console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
@@ -447,7 +246,7 @@ async function main() {
447
246
  let maxNewTokens = null;
448
247
  let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
449
248
  let rebuildPdfIndex = false;
450
- let useTui = false;
249
+ let showReasoning = false;
451
250
  let singlePrompt = null;
452
251
  const backendArgs = [chatScript];
453
252
 
@@ -479,8 +278,8 @@ async function main() {
479
278
  cacheDir = resolve(value(++i, arg));
480
279
  } else if (arg === "--rebuild-pdf-index") {
481
280
  rebuildPdfIndex = true;
482
- } else if (arg === "--tui") {
483
- useTui = true;
281
+ } else if (arg === "--show-reasoning") {
282
+ showReasoning = true;
484
283
  } else if (arg === "-p" || arg === "--prompt") {
485
284
  singlePrompt = value(++i, arg);
486
285
  } else if (arg === "--no-rag") {
@@ -498,8 +297,8 @@ async function main() {
498
297
  if (!py) die("Python not found. Install Python 3.9+ and re-run.");
499
298
  if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
500
299
 
501
- const ui = useTui ? await createTui() : createPlainUi();
502
- ui.print(useTui ? plain(render({ modelPath, cacheDir, pdfDir })) : render({ modelPath, cacheDir, pdfDir }));
300
+ const ui = createPlainUi();
301
+ ui.print(render({ modelPath, cacheDir, pdfDir }));
503
302
  ensureDeps(py, (msg) => ui.print(msg));
504
303
  ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
505
304
  if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
@@ -516,6 +315,9 @@ async function main() {
516
315
  HF_HUB_ETAG_TIMEOUT: process.env.HF_HUB_ETAG_TIMEOUT || "30",
517
316
  HF_HUB_VERBOSITY: process.env.HF_HUB_VERBOSITY || "error",
518
317
  TRANSFORMERS_VERBOSITY: process.env.TRANSFORMERS_VERBOSITY || "error",
318
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || "1",
319
+ PYTHONIOENCODING: process.env.PYTHONIOENCODING || "utf-8",
320
+ COFOS_SHOW_REASONING: showReasoning ? "1" : (process.env.COFOS_SHOW_REASONING || "0"),
519
321
  };
520
322
  const proc = spawn(py, backendArgs, {
521
323
  stdio: ["pipe", "pipe", "pipe"],
@@ -551,6 +353,33 @@ async function main() {
551
353
  }
552
354
  }
553
355
 
356
+ function handleLocalOnly(rawInput) {
357
+ const input = String(rawInput || "").trim();
358
+ if (!input) return true;
359
+ if (input === "/") {
360
+ ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
361
+ return true;
362
+ }
363
+ if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
364
+ if (/^\/(help)$/.test(input)) {
365
+ 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.");
366
+ return true;
367
+ }
368
+ if (/^\/(cls)$/.test(input)) {
369
+ ui.clear();
370
+ return true;
371
+ }
372
+ if (/^\/(info)$/.test(input)) {
373
+ 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 Status: ${ready ? "ready" : "loading"}\n Hist: ~/.cofos_history.jsonl\n`);
374
+ return true;
375
+ }
376
+ if (input.startsWith("/") && !/^\/(clear|save|topk)(?:\s|$)/.test(input) && !/^\/rag\s+(?:on|off)$/.test(input)) {
377
+ ui.print(`{red-fg}•{/red-fg} Unknown command: ${input}.\n${slashPrefixHint(input)}`);
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+
554
383
  function handle(rawInput) {
555
384
  let input = String(rawInput || "");
556
385
  if (input.endsWith("\\")) {
@@ -569,7 +398,7 @@ async function main() {
569
398
  if (!input) return false;
570
399
 
571
400
  if (input === "/") {
572
- ui.print("Type a command prefix, for example /r or /to, then press Tab. Run /help for all commands.");
401
+ ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
573
402
  return false;
574
403
  }
575
404
  if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
@@ -632,8 +461,14 @@ async function main() {
632
461
  }
633
462
 
634
463
  ui.setSubmitHandler((line) => {
464
+ if (handleLocalOnly(line)) {
465
+ if (!closed) ui.focus();
466
+ return;
467
+ }
635
468
  if (!ready || generating) {
636
469
  inputQ.push(line);
470
+ ui.print(ready ? "• Queued until the current answer finishes." : "• Queued until the model is ready. Use /exit to quit while loading.");
471
+ if (!closed) ui.focus();
637
472
  return;
638
473
  }
639
474
  const started = handle(line);
@@ -681,6 +516,16 @@ async function main() {
681
516
 
682
517
  function streamPlainAssistant(text, final = false) {
683
518
  if (ui.supportsReplace) return;
519
+ if (!showReasoning) {
520
+ if (plainAssistantMode === "undecided") {
521
+ if (!text && !final) return;
522
+ plainAssistantMode = "answer";
523
+ ui.append(`${GR}${B}◆${R} `);
524
+ }
525
+ if (text) ui.append(text);
526
+ return;
527
+ }
528
+
684
529
  plainAssistantPending += text;
685
530
  if (plainAssistantMode === "undecided") {
686
531
  const start = plainAssistantPending.trimStart();
@@ -691,12 +536,10 @@ async function main() {
691
536
  plainReasonPrinted = 0;
692
537
  plainAssistantPending = "";
693
538
  ui.append(`${G}-${R} ${G}`);
694
- } else if (final || start.length >= 12 || /\s/.test(start)) {
539
+ } else {
695
540
  plainAssistantMode = "answer";
696
541
  plainAssistantPending = start;
697
542
  ui.append(`${GR}${B}◆${R} `);
698
- } else {
699
- return;
700
543
  }
701
544
  }
702
545
 
@@ -731,15 +574,14 @@ async function main() {
731
574
  }
732
575
  }
733
576
 
734
- if (plainAssistantMode === "answer") {
735
- if (plainAssistantPending) {
736
- ui.append(plainAssistantPending);
737
- plainAssistantPending = "";
738
- }
577
+ if (plainAssistantMode === "answer" && plainAssistantPending) {
578
+ ui.append(plainAssistantPending);
579
+ plainAssistantPending = "";
739
580
  }
740
581
  }
741
582
 
742
583
 
584
+
743
585
  proc.stdout.on("data", (chunk) => {
744
586
  buf += chunk;
745
587
  if (!ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.3",
3
+ "version": "1.3.6",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,8 +38,5 @@
38
38
  "license": "MIT",
39
39
  "engines": {
40
40
  "node": ">=18"
41
- },
42
- "dependencies": {
43
- "blessed": "^0.1.81"
44
41
  }
45
42
  }
@@ -489,6 +489,11 @@ class StudentAdapterInference:
489
489
  @staticmethod
490
490
  def _sanitize_stream(chunks: Any) -> Generator[str, None, None]:
491
491
  """Drop ``<think>`` blocks and leading labels from a text stream."""
492
+ if os.environ.get("COFOS_SHOW_REASONING", "").lower() in {"1", "true", "yes", "on"}:
493
+ for chunk in chunks:
494
+ yield chunk
495
+ return
496
+
492
497
  buffer = ""
493
498
  started = False
494
499
  in_think = False
@@ -538,7 +543,23 @@ class StudentAdapterInference:
538
543
  )
539
544
  if not stripped.strip():
540
545
  continue
541
- buffer = stripped.lstrip()
546
+ stripped = stripped.lstrip()
547
+ if awaiting_initial_answer and re.match(
548
+ r"(?is)^(?:the user|okay[,,]?\s+the user|用户问|用户用|i need|i should|i'll|let me|since the user|we need)\b",
549
+ stripped,
550
+ ):
551
+ paragraph_end = re.search(r"\n\s*\n", stripped)
552
+ if paragraph_end is None:
553
+ # Keep waiting for the end of the leading reasoning paragraph.
554
+ buffer = stripped[-4000:]
555
+ continue
556
+ buffer = stripped[paragraph_end.end():].lstrip()
557
+ awaiting_initial_answer = False
558
+ if not buffer.strip():
559
+ continue
560
+ else:
561
+ buffer = stripped
562
+ awaiting_initial_answer = False
542
563
  started = True
543
564
 
544
565
  if len(buffer) > len("<think>"):