@lzhzzzzwill/cofos 1.3.3 → 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
@@ -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] [--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,6 @@ 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");
434
232
  console.log(" -p, --prompt TEXT Ask once and exit");
435
233
  console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
436
234
  console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
@@ -447,7 +245,6 @@ async function main() {
447
245
  let maxNewTokens = null;
448
246
  let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
449
247
  let rebuildPdfIndex = false;
450
- let useTui = false;
451
248
  let singlePrompt = null;
452
249
  const backendArgs = [chatScript];
453
250
 
@@ -479,8 +276,6 @@ async function main() {
479
276
  cacheDir = resolve(value(++i, arg));
480
277
  } else if (arg === "--rebuild-pdf-index") {
481
278
  rebuildPdfIndex = true;
482
- } else if (arg === "--tui") {
483
- useTui = true;
484
279
  } else if (arg === "-p" || arg === "--prompt") {
485
280
  singlePrompt = value(++i, arg);
486
281
  } else if (arg === "--no-rag") {
@@ -498,8 +293,8 @@ async function main() {
498
293
  if (!py) die("Python not found. Install Python 3.9+ and re-run.");
499
294
  if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
500
295
 
501
- const ui = useTui ? await createTui() : createPlainUi();
502
- ui.print(useTui ? plain(render({ modelPath, cacheDir, pdfDir })) : render({ modelPath, cacheDir, pdfDir }));
296
+ const ui = createPlainUi();
297
+ ui.print(render({ modelPath, cacheDir, pdfDir }));
503
298
  ensureDeps(py, (msg) => ui.print(msg));
504
299
  ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
505
300
  if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
@@ -569,7 +364,7 @@ async function main() {
569
364
  if (!input) return false;
570
365
 
571
366
  if (input === "/") {
572
- ui.print("Type a command prefix, for example /r or /to, then press Tab. Run /help for all commands.");
367
+ ui.print("Commands: " + SLASH_COMMANDS.map(({ cmd }) => cmd).join(" ") + "\nType a prefix such as /r or /to, then press Tab for completion.");
573
368
  return false;
574
369
  }
575
370
  if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
@@ -681,65 +476,16 @@ async function main() {
681
476
 
682
477
  function streamPlainAssistant(text, final = false) {
683
478
  if (ui.supportsReplace) return;
684
- plainAssistantPending += text;
685
479
  if (plainAssistantMode === "undecided") {
686
- const start = plainAssistantPending.trimStart();
687
- if (!start && !final) return;
688
- if (start.startsWith("<think>") || reasonRe.test(start)) {
689
- plainAssistantMode = "reason";
690
- plainReasonBuffer = start.replace(/^<think>\s*/i, "");
691
- plainReasonPrinted = 0;
692
- plainAssistantPending = "";
693
- ui.append(`${G}-${R} ${G}`);
694
- } else if (final || start.length >= 12 || /\s/.test(start)) {
695
- plainAssistantMode = "answer";
696
- plainAssistantPending = start;
697
- ui.append(`${GR}${B}◆${R} `);
698
- } else {
699
- return;
700
- }
701
- }
702
-
703
- if (plainAssistantMode === "reason") {
704
- plainReasonBuffer += plainAssistantPending;
705
- plainAssistantPending = "";
706
- const thinkEnd = plainReasonBuffer.indexOf("</think>");
707
- const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
708
- const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
709
- if (split !== -1) {
710
- const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
711
- const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
712
- const rest = plainReasonBuffer.slice(restStart).trimStart();
713
- if (reason) ui.append(reason);
714
- ui.append(`${R}\n${GR}${B}◆${R} `);
715
- plainAssistantMode = "answer";
716
- plainAssistantPending = rest;
717
- plainReasonBuffer = "";
718
- plainReasonPrinted = 0;
719
- } else {
720
- const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
721
- if (printableUntil > plainReasonPrinted) {
722
- ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
723
- plainReasonPrinted = printableUntil;
724
- }
725
- if (!final) return;
726
- if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
727
- plainReasonBuffer = "";
728
- plainReasonPrinted = 0;
729
- ui.append(R);
730
- return;
731
- }
732
- }
733
-
734
- if (plainAssistantMode === "answer") {
735
- if (plainAssistantPending) {
736
- ui.append(plainAssistantPending);
737
- plainAssistantPending = "";
738
- }
480
+ if (!text && !final) return;
481
+ plainAssistantMode = "answer";
482
+ ui.append(`${GR}${B}◆${R} `);
739
483
  }
484
+ if (text) ui.append(text);
740
485
  }
741
486
 
742
487
 
488
+
743
489
  proc.stdout.on("data", (chunk) => {
744
490
  buf += chunk;
745
491
  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.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>"):