@lzhzzzzwill/cofos 1.3.0 → 1.3.2

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.
Files changed (2) hide show
  1. package/bin/cofos.js +130 -12
  2. package/package.json +1 -1
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 } from "node:readline";
3
+ import { createInterface, emitKeypressEvents } 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";
@@ -134,6 +134,12 @@ function slashHint(line = "/") {
134
134
  return `\n Commands\n${rows}\n Press Tab to complete.\n`;
135
135
  }
136
136
 
137
+ function slashInlineHint(line = "/") {
138
+ const hits = slashMatches(line);
139
+ if (!hits.length) return "";
140
+ return hits.map(({ cmd, desc }) => ` ${C}${cmd}${R} ${desc}`).join("\n");
141
+ }
142
+
137
143
  /* ─── helpers ─── */
138
144
 
139
145
  function cmdExist(c) { return spawnSync(c, ["--version"], { encoding: "utf-8", stdio: "pipe" }).status === 0; }
@@ -352,9 +358,17 @@ async function createTui() {
352
358
  };
353
359
  }
354
360
 
355
- function stripBlessedTags(text) {
361
+ function blessedToAnsi(text) {
356
362
  return String(text)
357
- .replace(/\{\/?(?:bold|cyan-fg|yellow-fg|green-fg|gray-fg|red-fg|black-fg|yellow-bg)\}/g, "")
363
+ .replace(/\{bold\}/g, B)
364
+ .replace(/\{\/bold\}/g, R)
365
+ .replace(/\{cyan-fg\}/g, C)
366
+ .replace(/\{yellow-fg\}/g, Y)
367
+ .replace(/\{green-fg\}/g, GR)
368
+ .replace(/\{gray-fg\}/g, G)
369
+ .replace(/\{red-fg\}/g, "\x1b[91m")
370
+ .replace(/\{black-fg\}|\{yellow-bg\}|\{\/yellow-bg\}|\{\/black-fg\}/g, "")
371
+ .replace(/\{\/(?:cyan-fg|yellow-fg|green-fg|gray-fg|red-fg)\}/g, R)
358
372
  .replace(/[{}]/g, "");
359
373
  }
360
374
 
@@ -368,6 +382,7 @@ function createPlainUi() {
368
382
  let onSubmit = () => {};
369
383
  let onExit = () => {};
370
384
  let prompt = "cofos";
385
+ let slashHintShown = false;
371
386
 
372
387
  function setPrompt(label) {
373
388
  prompt = label;
@@ -375,7 +390,23 @@ function createPlainUi() {
375
390
  }
376
391
 
377
392
  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
+ }
378
408
  rl.on("line", (line) => {
409
+ slashHintShown = false;
379
410
  onSubmit(line);
380
411
  });
381
412
  rl.on("SIGINT", () => onExit());
@@ -383,11 +414,11 @@ function createPlainUi() {
383
414
 
384
415
  return {
385
416
  supportsReplace: false,
386
- append: (text = "") => process.stdout.write(stripBlessedTags(text)),
387
- print: (text = "") => console.log(stripBlessedTags(text)),
417
+ append: (text = "") => process.stdout.write(blessedToAnsi(text)),
418
+ print: (text = "") => console.log(blessedToAnsi(text)),
388
419
  clear: () => console.clear(),
389
420
  length: () => 0,
390
- replaceFrom: (_index, text = "") => process.stdout.write(stripBlessedTags(text)),
421
+ replaceFrom: (_index, text = "") => process.stdout.write(blessedToAnsi(text)),
391
422
  setSubmitHandler: (fn) => { onSubmit = fn; },
392
423
  setExitHandler: (fn) => { onExit = fn; },
393
424
  focus: () => rl.prompt(),
@@ -510,7 +541,8 @@ async function main() {
510
541
  proc.stderr.setEncoding("utf-8");
511
542
 
512
543
  let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
513
- let currentAnswer = "", assistantStart = -1;
544
+ let currentAnswer = "", assistantStart = -1, plainAssistantMode = "undecided", plainAssistantPending = "";
545
+ let plainReasonBuffer = "", plainReasonPrinted = 0;
514
546
  const inputQ = [];
515
547
  let mlBuf = [];
516
548
 
@@ -551,6 +583,10 @@ async function main() {
551
583
  input = input.trim();
552
584
  if (!input) return false;
553
585
 
586
+ if (input === "/") {
587
+ ui.print(slashHint("/"));
588
+ return false;
589
+ }
554
590
  if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
555
591
  if (/^\/(help)$/.test(input)) {
556
592
  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.");
@@ -566,6 +602,10 @@ async function main() {
566
602
  responseOpen = false;
567
603
  assistantStart = -1;
568
604
  currentAnswer = "";
605
+ plainAssistantMode = "undecided";
606
+ plainAssistantPending = "";
607
+ plainReasonBuffer = "";
608
+ plainReasonPrinted = 0;
569
609
  commandOpen = true;
570
610
  proc.stdin.write(input + "\n");
571
611
  generating = true;
@@ -575,6 +615,10 @@ async function main() {
575
615
  responseOpen = false;
576
616
  assistantStart = -1;
577
617
  currentAnswer = "";
618
+ plainAssistantMode = "undecided";
619
+ plainAssistantPending = "";
620
+ plainReasonBuffer = "";
621
+ plainReasonPrinted = 0;
578
622
  commandOpen = true;
579
623
  proc.stdin.write(input + "\n");
580
624
  generating = true;
@@ -593,14 +637,22 @@ async function main() {
593
637
  responseOpen = false;
594
638
  assistantStart = -1;
595
639
  currentAnswer = "";
640
+ plainAssistantMode = "undecided";
641
+ plainAssistantPending = "";
642
+ plainReasonBuffer = "";
643
+ plainReasonPrinted = 0;
596
644
  generating = true;
597
645
  proc.stdin.write(input + "\n");
598
646
  return true;
599
647
  }
600
648
 
601
649
  ui.setSubmitHandler((line) => {
602
- if (!ready || generating) inputQ.push(line);
603
- else handle(line);
650
+ if (!ready || generating) {
651
+ inputQ.push(line);
652
+ return;
653
+ }
654
+ const started = handle(line);
655
+ if (!started && !closed) ui.focus();
604
656
  });
605
657
 
606
658
  proc.stderr.on("data", (chunk) => ui.append(chunk));
@@ -635,11 +687,73 @@ async function main() {
635
687
  return "{green-fg}{bold}◆{/bold}{/green-fg} " + raw;
636
688
  }
637
689
 
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;
691
+
638
692
  function redrawAssistant() {
639
693
  if (assistantStart < 0 || !ui.supportsReplace) return;
640
694
  ui.replaceFrom(assistantStart, formatAssistant(currentAnswer));
641
695
  }
642
696
 
697
+ function streamPlainAssistant(text, final = false) {
698
+ if (ui.supportsReplace) return;
699
+ plainAssistantPending += text;
700
+ 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
+ }
754
+ }
755
+ }
756
+
643
757
 
644
758
  proc.stdout.on("data", (chunk) => {
645
759
  buf += chunk;
@@ -684,16 +798,20 @@ async function main() {
684
798
  responseOpen = true;
685
799
  assistantStart = ui.supportsReplace ? ui.length() : 0;
686
800
  currentAnswer = "";
801
+ plainAssistantMode = "undecided";
802
+ plainAssistantPending = "";
803
+ plainReasonBuffer = "";
804
+ plainReasonPrinted = 0;
687
805
  }
688
806
 
689
807
  const di = buf.indexOf(DONE);
690
808
  if (di !== -1) {
691
809
  const pre = buf.slice(0, di);
692
- if (pre) { currentAnswer += pre; redrawAssistant(); }
810
+ if (pre) { currentAnswer += pre; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(pre); }
693
811
  buf = buf.slice(di + DONE.length);
694
812
  if (buf[0] === "\n") buf = buf.slice(1);
695
813
  if (ui.supportsReplace) redrawAssistant();
696
- else ui.print(formatAssistant(currentAnswer));
814
+ else streamPlainAssistant("", true);
697
815
  generating = false;
698
816
  responseOpen = false;
699
817
  if (ui.supportsReplace) ui.append("\n");
@@ -704,7 +822,7 @@ async function main() {
704
822
  const keep = DONE.length - 1;
705
823
  if (buf.length > keep) {
706
824
  const out = buf.slice(0, -keep);
707
- if (out) { currentAnswer += out; redrawAssistant(); }
825
+ if (out) { currentAnswer += out; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(out); }
708
826
  buf = buf.slice(-keep);
709
827
  }
710
828
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {