@lzhzzzzwill/cofos 1.3.0 → 1.3.1

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 +120 -10
  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
 
@@ -566,6 +598,10 @@ async function main() {
566
598
  responseOpen = false;
567
599
  assistantStart = -1;
568
600
  currentAnswer = "";
601
+ plainAssistantMode = "undecided";
602
+ plainAssistantPending = "";
603
+ plainReasonBuffer = "";
604
+ plainReasonPrinted = 0;
569
605
  commandOpen = true;
570
606
  proc.stdin.write(input + "\n");
571
607
  generating = true;
@@ -575,6 +611,10 @@ async function main() {
575
611
  responseOpen = false;
576
612
  assistantStart = -1;
577
613
  currentAnswer = "";
614
+ plainAssistantMode = "undecided";
615
+ plainAssistantPending = "";
616
+ plainReasonBuffer = "";
617
+ plainReasonPrinted = 0;
578
618
  commandOpen = true;
579
619
  proc.stdin.write(input + "\n");
580
620
  generating = true;
@@ -593,6 +633,10 @@ async function main() {
593
633
  responseOpen = false;
594
634
  assistantStart = -1;
595
635
  currentAnswer = "";
636
+ plainAssistantMode = "undecided";
637
+ plainAssistantPending = "";
638
+ plainReasonBuffer = "";
639
+ plainReasonPrinted = 0;
596
640
  generating = true;
597
641
  proc.stdin.write(input + "\n");
598
642
  return true;
@@ -635,11 +679,73 @@ async function main() {
635
679
  return "{green-fg}{bold}◆{/bold}{/green-fg} " + raw;
636
680
  }
637
681
 
682
+ 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;
683
+
638
684
  function redrawAssistant() {
639
685
  if (assistantStart < 0 || !ui.supportsReplace) return;
640
686
  ui.replaceFrom(assistantStart, formatAssistant(currentAnswer));
641
687
  }
642
688
 
689
+ function streamPlainAssistant(text, final = false) {
690
+ if (ui.supportsReplace) return;
691
+ plainAssistantPending += text;
692
+ if (plainAssistantMode === "undecided") {
693
+ const start = plainAssistantPending.trimStart();
694
+ if (!start && !final) return;
695
+ if (start.startsWith("<think>") || reasonRe.test(start)) {
696
+ plainAssistantMode = "reason";
697
+ plainReasonBuffer = start.replace(/^<think>\s*/i, "");
698
+ plainReasonPrinted = 0;
699
+ plainAssistantPending = "";
700
+ ui.append(`${G}-${R} ${G}`);
701
+ } else if (final || start.length >= 12 || /\s/.test(start)) {
702
+ plainAssistantMode = "answer";
703
+ plainAssistantPending = start;
704
+ ui.append(`${GR}${B}◆${R} `);
705
+ } else {
706
+ return;
707
+ }
708
+ }
709
+
710
+ if (plainAssistantMode === "reason") {
711
+ plainReasonBuffer += plainAssistantPending;
712
+ plainAssistantPending = "";
713
+ const thinkEnd = plainReasonBuffer.indexOf("</think>");
714
+ const paraEnd = plainReasonBuffer.search(/\n\s*\n/);
715
+ const split = thinkEnd !== -1 ? thinkEnd : paraEnd;
716
+ if (split !== -1) {
717
+ const reason = plainReasonBuffer.slice(plainReasonPrinted, split).replace(/<\/think>/gi, "");
718
+ const restStart = thinkEnd !== -1 ? split + "</think>".length : split;
719
+ const rest = plainReasonBuffer.slice(restStart).trimStart();
720
+ if (reason) ui.append(reason);
721
+ ui.append(`${R}\n${GR}${B}◆${R} `);
722
+ plainAssistantMode = "answer";
723
+ plainAssistantPending = rest;
724
+ plainReasonBuffer = "";
725
+ plainReasonPrinted = 0;
726
+ } else {
727
+ const printableUntil = final ? plainReasonBuffer.length : Math.max(0, plainReasonBuffer.length - 2);
728
+ if (printableUntil > plainReasonPrinted) {
729
+ ui.append(plainReasonBuffer.slice(plainReasonPrinted, printableUntil));
730
+ plainReasonPrinted = printableUntil;
731
+ }
732
+ if (!final) return;
733
+ if (plainReasonPrinted < plainReasonBuffer.length) ui.append(plainReasonBuffer.slice(plainReasonPrinted));
734
+ plainReasonBuffer = "";
735
+ plainReasonPrinted = 0;
736
+ ui.append(R);
737
+ return;
738
+ }
739
+ }
740
+
741
+ if (plainAssistantMode === "answer") {
742
+ if (plainAssistantPending) {
743
+ ui.append(plainAssistantPending);
744
+ plainAssistantPending = "";
745
+ }
746
+ }
747
+ }
748
+
643
749
 
644
750
  proc.stdout.on("data", (chunk) => {
645
751
  buf += chunk;
@@ -684,16 +790,20 @@ async function main() {
684
790
  responseOpen = true;
685
791
  assistantStart = ui.supportsReplace ? ui.length() : 0;
686
792
  currentAnswer = "";
793
+ plainAssistantMode = "undecided";
794
+ plainAssistantPending = "";
795
+ plainReasonBuffer = "";
796
+ plainReasonPrinted = 0;
687
797
  }
688
798
 
689
799
  const di = buf.indexOf(DONE);
690
800
  if (di !== -1) {
691
801
  const pre = buf.slice(0, di);
692
- if (pre) { currentAnswer += pre; redrawAssistant(); }
802
+ if (pre) { currentAnswer += pre; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(pre); }
693
803
  buf = buf.slice(di + DONE.length);
694
804
  if (buf[0] === "\n") buf = buf.slice(1);
695
805
  if (ui.supportsReplace) redrawAssistant();
696
- else ui.print(formatAssistant(currentAnswer));
806
+ else streamPlainAssistant("", true);
697
807
  generating = false;
698
808
  responseOpen = false;
699
809
  if (ui.supportsReplace) ui.append("\n");
@@ -704,7 +814,7 @@ async function main() {
704
814
  const keep = DONE.length - 1;
705
815
  if (buf.length > keep) {
706
816
  const out = buf.slice(0, -keep);
707
- if (out) { currentAnswer += out; redrawAssistant(); }
817
+ if (out) { currentAnswer += out; if (ui.supportsReplace) redrawAssistant(); else streamPlainAssistant(out); }
708
818
  buf = buf.slice(-keep);
709
819
  }
710
820
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {