@lzhzzzzwill/cofos 1.2.2 → 1.2.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.
Files changed (2) hide show
  1. package/bin/cofos.js +96 -10
  2. package/package.json +1 -1
package/bin/cofos.js CHANGED
@@ -106,7 +106,8 @@ function render({ modelPath, cacheDir, pdfDir }) {
106
106
  const SLASH_COMMANDS = [
107
107
  { cmd: "/help", desc: "Show help" },
108
108
  { cmd: "/exit", desc: "Quit" },
109
- { cmd: "/clear", desc: "Clear conversation history" },
109
+ { cmd: "/clear", desc: "Clear page and conversation history" },
110
+ { cmd: "/cls", desc: "Clear page only" },
110
111
  { cmd: "/save", desc: "Save this session history" },
111
112
  { cmd: "/info", desc: "Show model and RAG info" },
112
113
  { cmd: "/topk 5", desc: "Set retrieval top-k" },
@@ -169,8 +170,8 @@ function createTui() {
169
170
  alwaysScroll: true,
170
171
  keys: true,
171
172
  vi: true,
172
- mouse: false,
173
- tags: false,
173
+ mouse: true,
174
+ tags: true,
174
175
  scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
175
176
  });
176
177
 
@@ -207,10 +208,13 @@ function createTui() {
207
208
 
208
209
  let buffer = "";
209
210
  let onSubmit = () => {};
211
+ let onExit = () => {};
212
+ let submitting = false;
210
213
  let suggestionMatches = [];
211
214
  let selectedSuggestion = 0;
212
215
 
213
216
  function append(text = "") {
217
+ const wasAtBottom = history.getScrollPerc() >= 98;
214
218
  const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
215
219
  for (const part of data.split("\r")) {
216
220
  if (part === "") continue;
@@ -223,7 +227,7 @@ function createTui() {
223
227
  }
224
228
  if (buffer.length > 250000) buffer = buffer.slice(-220000);
225
229
  history.setContent(buffer);
226
- history.setScrollPerc(100);
230
+ if (wasAtBottom) history.setScrollPerc(100);
227
231
  screen.render();
228
232
  }
229
233
 
@@ -268,6 +272,8 @@ function createTui() {
268
272
  }
269
273
 
270
274
  function submitInput() {
275
+ if (submitting) return;
276
+ submitting = true;
271
277
  let value = input.getValue();
272
278
  if (!suggestions.hidden && selectedCommand()) value = selectedCommand();
273
279
  hideSuggestions();
@@ -275,9 +281,11 @@ function createTui() {
275
281
  input.focus();
276
282
  screen.render();
277
283
  onSubmit(String(value || ""));
284
+ setTimeout(() => { submitting = false; }, 0);
278
285
  }
279
286
 
280
287
  input.key(["enter"], submitInput);
288
+ screen.key(["enter"], submitInput);
281
289
 
282
290
  input.on("keypress", (_ch, key = {}) => {
283
291
  if (["up", "down", "tab", "enter", "escape"].includes(key.name)) return;
@@ -308,7 +316,11 @@ function createTui() {
308
316
  });
309
317
 
310
318
  screen.key(["escape"], hideSuggestions);
311
- screen.key(["C-c"], () => process.emit("SIGINT"));
319
+ screen.key(["pageup"], () => { history.scroll(-Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
320
+ screen.key(["pagedown"], () => { history.scroll(Math.max(3, Math.floor(screen.height * 0.8))); screen.render(); });
321
+ screen.key(["home"], () => { history.setScrollPerc(0); screen.render(); });
322
+ screen.key(["end"], () => { history.setScrollPerc(100); screen.render(); });
323
+ screen.key(["C-c"], () => onExit());
312
324
 
313
325
  input.focus();
314
326
  screen.render();
@@ -318,7 +330,10 @@ function createTui() {
318
330
  input,
319
331
  append,
320
332
  print: (text = "") => append(String(text) + "\n"),
333
+ clear: () => { buffer = ""; history.setContent(""); history.setScrollPerc(100); screen.render(); },
321
334
  setSubmitHandler: (fn) => { onSubmit = fn; },
335
+ setExitHandler: (fn) => { onExit = fn; },
336
+ focus: () => { input.focus(); screen.render(); },
322
337
  setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
323
338
  destroy: () => screen.destroy(),
324
339
  };
@@ -423,7 +438,8 @@ function main() {
423
438
  proc.stdout.setEncoding("utf-8");
424
439
  proc.stderr.setEncoding("utf-8");
425
440
 
426
- let buf = "", ready = false, generating = false, responseOpen = false, closed = false;
441
+ let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
442
+ let responseMaybeReasoning = false, responseReasoning = false, responseBuffer = "";
427
443
  const inputQ = [];
428
444
  let mlBuf = [];
429
445
 
@@ -435,6 +451,7 @@ function main() {
435
451
  ui.destroy();
436
452
  }
437
453
 
454
+ ui.setExitHandler(shutdown);
438
455
  process.on("SIGTERM", shutdown);
439
456
  process.on("SIGINT", shutdown);
440
457
 
@@ -467,9 +484,22 @@ function main() {
467
484
  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.");
468
485
  return false;
469
486
  }
487
+ if (/^\/(cls)$/.test(input)) {
488
+ ui.clear();
489
+ return false;
490
+ }
470
491
  if (/^\/(clear)$/.test(input)) {
492
+ ui.clear();
471
493
  mlBuf = [];
472
494
  responseOpen = false;
495
+ commandOpen = true;
496
+ proc.stdin.write(input + "\n");
497
+ generating = true;
498
+ return true;
499
+ }
500
+ if (/^\/(topk)(?:\s+\d+)?$/.test(input) || /^\/rag\s+(?:on|off)$/.test(input)) {
501
+ responseOpen = false;
502
+ commandOpen = true;
473
503
  proc.stdin.write(input + "\n");
474
504
  generating = true;
475
505
  return true;
@@ -478,8 +508,12 @@ function main() {
478
508
  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 Hist: ~/.cofos_history.jsonl\n`);
479
509
  return false;
480
510
  }
511
+ if (input.startsWith("/")) {
512
+ ui.print(`{red-fg}•{/red-fg} Unknown command: ${input}. Press / then Tab for commands, or run /help.`);
513
+ return false;
514
+ }
481
515
 
482
- ui.print(Y + "● You" + R + "\n" + input);
516
+ ui.print("{yellow-fg}{/yellow-fg} " + input);
483
517
  responseOpen = false;
484
518
  generating = true;
485
519
  proc.stdin.write(input + "\n");
@@ -493,6 +527,39 @@ function main() {
493
527
 
494
528
  proc.stderr.on("data", (chunk) => ui.append(chunk));
495
529
 
530
+ function appendAssistant(text) {
531
+ if (!text) return;
532
+ if (responseMaybeReasoning) {
533
+ responseBuffer += text;
534
+ const enough = responseBuffer.length > 80 || responseBuffer.includes("\n\n") || responseBuffer.includes("。") || responseBuffer.includes(".");
535
+ if (!enough) return;
536
+ const start = responseBuffer.trimStart();
537
+ responseReasoning = /^(The user|Okay[,,]|I need|We need|Based on the provided information[,,]? I can|用户)/i.test(start);
538
+ responseMaybeReasoning = false;
539
+ if (responseReasoning) {
540
+ ui.append("{gray-fg}- " + responseBuffer + "{/gray-fg}");
541
+ } else {
542
+ ui.append(responseBuffer);
543
+ }
544
+ responseBuffer = "";
545
+ return;
546
+ }
547
+ if (responseReasoning) {
548
+ const end = text.indexOf("\n\n");
549
+ if (end !== -1) {
550
+ const reason = text.slice(0, end);
551
+ const answer = text.slice(end).replace(/^\s+/, "");
552
+ if (reason) ui.append("{gray-fg}" + reason + "{/gray-fg}");
553
+ responseReasoning = false;
554
+ if (answer) ui.append("\n{green-fg}{bold}◆{/bold}{/green-fg} " + answer);
555
+ } else {
556
+ ui.append("{gray-fg}" + text + "{/gray-fg}");
557
+ }
558
+ return;
559
+ }
560
+ ui.append(text);
561
+ }
562
+
496
563
  proc.stdout.on("data", (chunk) => {
497
564
  buf += chunk;
498
565
  if (!ready) {
@@ -507,6 +574,20 @@ function main() {
507
574
  }
508
575
  if (!generating) { buf = ""; return; }
509
576
 
577
+ if (commandOpen) {
578
+ const di = buf.indexOf(DONE);
579
+ if (di === -1) return;
580
+ const pre = buf.slice(0, di).trim();
581
+ if (pre) ui.print("{gray-fg}•{/gray-fg} " + pre);
582
+ buf = buf.slice(di + DONE.length);
583
+ if (buf[0] === "\n") buf = buf.slice(1);
584
+ generating = false;
585
+ commandOpen = false;
586
+ ui.focus();
587
+ drain();
588
+ return;
589
+ }
590
+
510
591
  if (!responseOpen) {
511
592
  const ri = buf.indexOf(RESPONSE);
512
593
  if (ri === -1) {
@@ -517,24 +598,29 @@ function main() {
517
598
  buf = buf.slice(ri + RESPONSE.length);
518
599
  if (buf[0] === "\n") buf = buf.slice(1);
519
600
  responseOpen = true;
520
- ui.append(GR + "◆ COFOS" + R + "\n");
601
+ responseMaybeReasoning = true;
602
+ responseReasoning = false;
603
+ responseBuffer = "";
604
+ ui.append("{green-fg}{bold}◆{/bold}{/green-fg} ");
521
605
  }
522
606
 
523
607
  const di = buf.indexOf(DONE);
524
608
  if (di !== -1) {
525
609
  const pre = buf.slice(0, di);
526
- if (pre) ui.append(pre);
610
+ if (pre) appendAssistant(pre);
527
611
  buf = buf.slice(di + DONE.length);
528
612
  if (buf[0] === "\n") buf = buf.slice(1);
613
+ if (responseBuffer) { appendAssistant("\n"); }
529
614
  generating = false;
530
615
  responseOpen = false;
531
616
  ui.append("\n");
617
+ ui.focus();
532
618
  drain();
533
619
  } else {
534
620
  const keep = DONE.length - 1;
535
621
  if (buf.length > keep) {
536
622
  const out = buf.slice(0, -keep);
537
- if (out) ui.append(out);
623
+ if (out) appendAssistant(out);
538
624
  buf = buf.slice(-keep);
539
625
  }
540
626
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {