@lzhzzzzwill/cofos 1.3.2 → 1.3.4

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
 
@@ -77,11 +77,6 @@ Ask once and exit, useful for scripts and smoke tests:
77
77
  npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
78
78
  ```
79
79
 
80
- Use the experimental fixed-input TUI:
81
-
82
- ```bash
83
- npx @lzhzzzzwill/cofos --tui
84
- ```
85
80
 
86
81
  ## Startup Behavior
87
82
 
@@ -133,7 +128,6 @@ cofos \
133
128
  | `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
134
129
  | `--top-k` | Retrieval top-k |
135
130
  | `-p`, `--prompt` | Ask one question and exit |
136
- | `--tui` | Use the experimental fixed-input full-screen TUI |
137
131
  | `--max-new-tokens` | Override generation length |
138
132
  | `--no-rag` | Run without KG/BM25 retrieval |
139
133
 
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 { createInterface, emitKeypressEvents } from "node:readline";
3
+ import { createInterface } from "node:readline";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -121,23 +121,23 @@ function slashMatches(line) {
121
121
  }
122
122
 
123
123
  function slashCompletions(line) {
124
- if (!line.startsWith("/")) return [[], line];
124
+ if (!line.startsWith("/") || line.trim() === "/") return [[], line];
125
125
  const hits = slashMatches(line).map(({ cmd }) => cmd);
126
126
  return [hits, line];
127
127
  }
128
128
 
129
129
  function slashHint(line = "/") {
130
- const hits = slashMatches(line);
131
- const rows = (hits.length ? hits : SLASH_COMMANDS)
130
+ const hits = line.trim() === "/" ? SLASH_COMMANDS : slashMatches(line);
131
+ const rows = hits
132
132
  .map(({ cmd, desc }) => ` ${cmd.padEnd(10)} ${desc}`)
133
133
  .join("\n");
134
134
  return `\n Commands\n${rows}\n Press Tab to complete.\n`;
135
135
  }
136
136
 
137
- function slashInlineHint(line = "/") {
137
+ function slashPrefixHint(line) {
138
138
  const hits = slashMatches(line);
139
- if (!hits.length) return "";
140
- return hits.map(({ cmd, desc }) => ` ${C}${cmd}${R} ${desc}`).join("\n");
139
+ if (!hits.length) return `No command matches ${line}. Run /help for commands.`;
140
+ return hits.map(({ cmd, desc }) => ` ${cmd.padEnd(10)} ${desc}`).join("\n");
141
141
  }
142
142
 
143
143
  /* ─── helpers ─── */
@@ -158,207 +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
- screen,
344
- input,
345
- append,
346
- print: (text = "") => append(String(text) + "\n"),
347
- clear: () => { buffer = ""; renderBuffer(true); },
348
- length: () => buffer.length,
349
- replaceFrom: (index, text = "") => { buffer = buffer.slice(0, Math.max(0, index)) + String(text); renderBuffer(true); },
350
- setSubmitHandler: (fn) => { onSubmit = fn; },
351
- setExitHandler: (fn) => { onExit = fn; },
352
- focus: () => { input.focus(); screen.render(); },
353
- setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
354
- destroy: () => {
355
- try { screen.program.disableMouse(); } catch (_) {}
356
- screen.destroy();
357
- },
358
- };
359
- }
360
-
361
- function blessedToAnsi(text) {
161
+ function markupToAnsi(text) {
362
162
  return String(text)
363
163
  .replace(/\{bold\}/g, B)
364
164
  .replace(/\{\/bold\}/g, R)
@@ -382,7 +182,6 @@ function createPlainUi() {
382
182
  let onSubmit = () => {};
383
183
  let onExit = () => {};
384
184
  let prompt = "cofos";
385
- let slashHintShown = false;
386
185
 
387
186
  function setPrompt(label) {
388
187
  prompt = label;
@@ -390,23 +189,7 @@ function createPlainUi() {
390
189
  }
391
190
 
392
191
  setPrompt(prompt);
393
- if (process.stdin.isTTY) {
394
- emitKeypressEvents(process.stdin, rl);
395
- process.stdin.on("keypress", () => {
396
- setTimeout(() => {
397
- const line = rl.line || "";
398
- if (line === "/" && !slashHintShown) {
399
- slashHintShown = true;
400
- process.stdout.write(`\n${slashInlineHint("/")}\n`);
401
- rl.prompt(true);
402
- } else if (!line.startsWith("/")) {
403
- slashHintShown = false;
404
- }
405
- }, 0);
406
- });
407
- }
408
192
  rl.on("line", (line) => {
409
- slashHintShown = false;
410
193
  onSubmit(line);
411
194
  });
412
195
  rl.on("SIGINT", () => onExit());
@@ -414,11 +197,12 @@ function createPlainUi() {
414
197
 
415
198
  return {
416
199
  supportsReplace: false,
417
- append: (text = "") => process.stdout.write(blessedToAnsi(text)),
418
- print: (text = "") => console.log(blessedToAnsi(text)),
200
+ echoUser: false,
201
+ append: (text = "") => process.stdout.write(markupToAnsi(text)),
202
+ print: (text = "") => console.log(markupToAnsi(text)),
419
203
  clear: () => console.clear(),
420
204
  length: () => 0,
421
- replaceFrom: (_index, text = "") => process.stdout.write(blessedToAnsi(text)),
205
+ replaceFrom: (_index, text = "") => process.stdout.write(markupToAnsi(text)),
422
206
  setSubmitHandler: (fn) => { onSubmit = fn; },
423
207
  setExitHandler: (fn) => { onExit = fn; },
424
208
  focus: () => rl.prompt(),
@@ -432,7 +216,7 @@ function createPlainUi() {
432
216
  async function main() {
433
217
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
434
218
  console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
435
- 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] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
436
220
  console.log(" Commands in chat:");
437
221
  console.log(" /help Show this help");
438
222
  console.log(" /exit Quit");
@@ -445,7 +229,6 @@ async function main() {
445
229
  console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
446
230
  console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
447
231
  console.log(" --max-new-tokens N Override generation length");
448
- console.log(" --tui Use the experimental fixed-input TUI");
449
232
  console.log(" -p, --prompt TEXT Ask once and exit");
450
233
  console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
451
234
  console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
@@ -462,7 +245,6 @@ async function main() {
462
245
  let maxNewTokens = null;
463
246
  let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
464
247
  let rebuildPdfIndex = false;
465
- let useTui = false;
466
248
  let singlePrompt = null;
467
249
  const backendArgs = [chatScript];
468
250
 
@@ -494,8 +276,6 @@ async function main() {
494
276
  cacheDir = resolve(value(++i, arg));
495
277
  } else if (arg === "--rebuild-pdf-index") {
496
278
  rebuildPdfIndex = true;
497
- } else if (arg === "--tui") {
498
- useTui = true;
499
279
  } else if (arg === "-p" || arg === "--prompt") {
500
280
  singlePrompt = value(++i, arg);
501
281
  } else if (arg === "--no-rag") {
@@ -513,8 +293,8 @@ async function main() {
513
293
  if (!py) die("Python not found. Install Python 3.9+ and re-run.");
514
294
  if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
515
295
 
516
- const ui = useTui ? await createTui() : createPlainUi();
517
- ui.print(useTui ? plain(render({ modelPath, cacheDir, pdfDir })) : render({ modelPath, cacheDir, pdfDir }));
296
+ const ui = createPlainUi();
297
+ ui.print(render({ modelPath, cacheDir, pdfDir }));
518
298
  ensureDeps(py, (msg) => ui.print(msg));
519
299
  ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
520
300
  if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
@@ -584,7 +364,7 @@ async function main() {
584
364
  if (!input) return false;
585
365
 
586
366
  if (input === "/") {
587
- ui.print(slashHint("/"));
367
+ ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
588
368
  return false;
589
369
  }
590
370
  if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
@@ -629,11 +409,11 @@ async function main() {
629
409
  return false;
630
410
  }
631
411
  if (input.startsWith("/")) {
632
- ui.print(`{red-fg}•{/red-fg} Unknown command: ${input}. Press / then Tab for commands, or run /help.`);
412
+ ui.print(`{red-fg}•{/red-fg} Unknown command: ${input}.\n${slashPrefixHint(input)}`);
633
413
  return false;
634
414
  }
635
415
 
636
- ui.print("{yellow-fg}●{/yellow-fg} " + input);
416
+ if (ui.echoUser) ui.print("{yellow-fg}●{/yellow-fg} " + input);
637
417
  responseOpen = false;
638
418
  assistantStart = -1;
639
419
  currentAnswer = "";
@@ -674,7 +454,7 @@ async function main() {
674
454
  }
675
455
 
676
456
  const paragraphs = raw.split(/\n\s*\n/);
677
- const reasonRe = /^(The user|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|用户|让我)/i;
457
+ const reasonRe = /^(The user|用户问|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|让我)/i;
678
458
  let reasonCount = 0;
679
459
  while (reasonCount < paragraphs.length && reasonRe.test(paragraphs[reasonCount].trim())) reasonCount += 1;
680
460
  if (reasonCount > 0) {
@@ -687,7 +467,7 @@ async function main() {
687
467
  return "{green-fg}{bold}◆{/bold}{/green-fg} " + raw;
688
468
  }
689
469
 
690
- const reasonRe = /^(The user|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|用户|让我)/i;
470
+ const reasonRe = /^(The user|用户问|Okay[,,]|I need|We need|I should|I'll|Let me|Since the input|In COF nomenclature|Based on the provided information[,,]? I can|让我)/i;
691
471
 
692
472
  function redrawAssistant() {
693
473
  if (assistantStart < 0 || !ui.supportsReplace) return;
@@ -696,65 +476,16 @@ async function main() {
696
476
 
697
477
  function streamPlainAssistant(text, final = false) {
698
478
  if (ui.supportsReplace) return;
699
- plainAssistantPending += text;
700
479
  if (plainAssistantMode === "undecided") {
701
- const start = plainAssistantPending.trimStart();
702
- if (!start && !final) return;
703
- if (start.startsWith("<think>") || reasonRe.test(start)) {
704
- plainAssistantMode = "reason";
705
- plainReasonBuffer = start.replace(/^<think>\s*/i, "");
706
- plainReasonPrinted = 0;
707
- plainAssistantPending = "";
708
- ui.append(`${G}-${R} ${G}`);
709
- } else if (final || start.length >= 12 || /\s/.test(start)) {
710
- plainAssistantMode = "answer";
711
- plainAssistantPending = start;
712
- ui.append(`${GR}${B}◆${R} `);
713
- } else {
714
- return;
715
- }
716
- }
717
-
718
- if (plainAssistantMode === "reason") {
719
- plainReasonBuffer += plainAssistantPending;
720
- plainAssistantPending = "";
721
- const thinkEnd = plainReasonBuffer.indexOf("</think>");
722
- const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
723
- const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
724
- if (split !== -1) {
725
- const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
726
- const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
727
- const rest = plainReasonBuffer.slice(restStart).trimStart();
728
- if (reason) ui.append(reason);
729
- ui.append(`${R}\n${GR}${B}◆${R} `);
730
- plainAssistantMode = "answer";
731
- plainAssistantPending = rest;
732
- plainReasonBuffer = "";
733
- plainReasonPrinted = 0;
734
- } else {
735
- const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
736
- if (printableUntil > plainReasonPrinted) {
737
- ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
738
- plainReasonPrinted = printableUntil;
739
- }
740
- if (!final) return;
741
- if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
742
- plainReasonBuffer = "";
743
- plainReasonPrinted = 0;
744
- ui.append(R);
745
- return;
746
- }
747
- }
748
-
749
- if (plainAssistantMode === "answer") {
750
- if (plainAssistantPending) {
751
- ui.append(plainAssistantPending);
752
- plainAssistantPending = "";
753
- }
480
+ if (!text && !final) return;
481
+ plainAssistantMode = "answer";
482
+ ui.append(`${GR}${B}◆${R} `);
754
483
  }
484
+ if (text) ui.append(text);
755
485
  }
756
486
 
757
487
 
488
+
758
489
  proc.stdout.on("data", (chunk) => {
759
490
  buf += chunk;
760
491
  if (!ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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
  }
@@ -538,7 +538,23 @@ class StudentAdapterInference:
538
538
  )
539
539
  if not stripped.strip():
540
540
  continue
541
- buffer = stripped.lstrip()
541
+ stripped = stripped.lstrip()
542
+ if awaiting_initial_answer and re.match(
543
+ r"(?is)^(?:the user|okay[,,]?\s+the user|用户问|用户用|i need|i should|i'll|let me|since the user|we need)\b",
544
+ stripped,
545
+ ):
546
+ paragraph_end = re.search(r"\n\s*\n", stripped)
547
+ if paragraph_end is None:
548
+ # Keep waiting for the end of the leading reasoning paragraph.
549
+ buffer = stripped[-4000:]
550
+ continue
551
+ buffer = stripped[paragraph_end.end():].lstrip()
552
+ awaiting_initial_answer = False
553
+ if not buffer.strip():
554
+ continue
555
+ else:
556
+ buffer = stripped
557
+ awaiting_initial_answer = False
542
558
  started = True
543
559
 
544
560
  if len(buffer) > len("<think>"):