@lzhzzzzwill/cofos 1.1.14 → 1.1.19

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 +98 -41
  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, emitKeypressEvents } from "node:readline";
3
+ import { clearLine, createInterface, cursorTo } 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";
@@ -16,6 +16,7 @@ const DEFAULT_CONFIG = process.env.COFOS_CONFIG || "config/config.yaml";
16
16
  /* ─── ANSI ─── */
17
17
  const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
18
18
  const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m", Y = "\x1b[93m";
19
+ const M = "\x1b[95m", BL = "\x1b[94m", GR = "\x1b[92m", A = "\x1b[38;5;208m";
19
20
 
20
21
  /* ─── Protocol markers ─── */
21
22
  const READY = "===READY===", RESPONSE = "===RESPONSE===", DONE = "===DONE===";
@@ -30,9 +31,16 @@ const ART_W = ART[0].length;
30
31
 
31
32
  function vis(t) { return t.replace(/\x1b\[[0-9;]*m/g, "").length; }
32
33
  function fw() { return Math.min(Math.max((process.stdout.columns || 110) - 2, 82), 150); }
33
- function top(w) { return "╭" + "─".repeat(w) + ""; }
34
- function bot(w) { return "╰" + "".repeat(w) + "╯"; }
35
- function sep(w) { return "├" + "─".repeat(w) + "┤"; }
34
+ function plain(t) { return t.replace(/\x1b\[[0-9;]*m/g, ""); }
35
+ function topTitle(w, left, right = "") {
36
+ const l = ` ${C}${B}${left}${R} `;
37
+ const r = right ? ` ${G}${right}${R} ` : "";
38
+ const room = Math.max(0, w - vis(l) - vis(r));
39
+ return C + "╭" + R + l + C + "─".repeat(room) + R + r + C + "╮" + R;
40
+ }
41
+ function top(w) { return C + "╭" + "─".repeat(w) + "╮" + R; }
42
+ function bot(w) { return C + "╰" + "─".repeat(w) + "╯" + R; }
43
+ function sep(w) { return C + "├" + "─".repeat(w) + "┤" + R; }
36
44
  function vpad(t, w) { return t + " ".repeat(Math.max(0, w - vis(t))); }
37
45
  function clip(t, w) {
38
46
  const plain = t.replace(/\x1b\[[0-9;]*m/g, "");
@@ -41,12 +49,12 @@ function clip(t, w) {
41
49
  return plain.slice(0, w - 1) + "…";
42
50
  }
43
51
  function cell(t, w) { return vpad(clip(t, w), w); }
44
- function row(left, right, lw, rw) { return "│ " + cell(left, lw) + " │ " + cell(right, rw) + " │"; }
45
- function full(t, w) { return "│ " + cell(t, w - 2) + " │"; }
52
+ function row(left, right, lw, rw) { return C + "│ " + R + cell(left, lw) + C + " │ " + R + cell(right, rw) + C + " │" + R; }
53
+ function full(t, w) { return C + "│ " + R + cell(t, w - 2) + C + " │" + R; }
46
54
  function empty(w) { return full("", w); }
47
55
 
48
56
  function logoLines(width) {
49
- if (width >= ART_W) return ART.map((line) => C + B + line + R);
57
+ if (width >= ART_W) return ART.map((line, i) => (i < 2 ? C : i < 4 ? BL : M) + B + line + R);
50
58
  return [C + B + "COFOS" + R];
51
59
  }
52
60
 
@@ -60,25 +68,26 @@ function render({ modelPath, cacheDir, pdfDir }) {
60
68
  ...logoLines(Math.floor((w - 5) * 0.48)),
61
69
  "",
62
70
  G + "Scientific RAG CLI" + R,
63
- W + "Model" + R + " " + modelPath,
64
- W + "Params" + R + " 9B",
65
- W + "Path" + R + " " + cwd,
66
- W + "Cache" + R + " " + cacheDir,
71
+ Y + "Model" + R + " " + W + modelPath + R,
72
+ Y + "Params" + R + " " + W + "9B" + R,
73
+ Y + "Path" + R + " " + W + cwd + R,
74
+ Y + "Cache" + R + " " + W + cacheDir + R,
67
75
  ];
68
76
  const right = [
69
- W + B + "What COFOS Does" + R,
77
+ GR + B + "What COFOS Does" + R,
70
78
  "COFOS answers scientific questions about covalent organic frameworks,",
71
79
  "photocatalysis, reactive oxygen species, and oxygen-reduction products.",
72
80
  rag,
73
81
  "",
74
- W + B + "Suggested Commands" + R,
75
- "/help show commands and PDF-RAG usage",
76
- "/topk 5 change retrieval depth",
77
- "/rag off answer without retrieval",
78
- "cofos --pdf-dir ./new_pdfs add local papers",
82
+ A + B + "Suggested Commands" + R,
83
+ C + "/help" + R + " show commands and PDF-RAG usage",
84
+ C + "/topk 5" + R + " change retrieval depth",
85
+ C + "/rag off" + R + " answer without retrieval",
86
+ M + "cofos --pdf-dir ./new_pdfs" + R + " add local papers",
79
87
  ];
80
88
 
81
- const lines = [top(w)];
89
+ const titleLeft = `COFOS CLI v${pkg.version}`;
90
+ const lines = [topTitle(w, titleLeft)];
82
91
  if (w >= minSplit) {
83
92
  const inner = w - 5;
84
93
  const lw = Math.max(46, Math.floor(inner * 0.48));
@@ -114,7 +123,7 @@ function slashMatches(line) {
114
123
  function slashCompletions(line) {
115
124
  if (!line.startsWith("/")) return [[], line];
116
125
  const hits = slashMatches(line).map(({ cmd }) => cmd);
117
- return [hits.length ? hits : SLASH_COMMANDS.map(({ cmd }) => cmd), line];
126
+ return [hits, line];
118
127
  }
119
128
 
120
129
  function slashHint(line = "/") {
@@ -247,6 +256,11 @@ function main() {
247
256
  const inputQ = [];
248
257
  const mainPrompt = C + " cofos " + R;
249
258
  const contPrompt = G + " ... " + R;
259
+ const docked = Boolean(process.stdout.isTTY && process.stdin.isTTY);
260
+ let dockRows = process.stdout.rows || 24;
261
+ let dockCols = process.stdout.columns || 80;
262
+ let historyRow = Math.max(0, dockRows - 2);
263
+ let historyCol = 0;
250
264
  const rl = createInterface({
251
265
  input: process.stdin,
252
266
  output: process.stdout,
@@ -254,32 +268,72 @@ function main() {
254
268
  prompt: mainPrompt,
255
269
  completer: slashCompletions,
256
270
  });
257
- let lastSlashHint = "";
258
271
  rl.on("line", (l) => {
259
- lastSlashHint = "";
260
272
  if (!ready || generating) inputQ.push(l);
261
273
  else if (!handle(l)) ask();
262
274
  });
263
- if (process.stdin.isTTY) {
264
- emitKeypressEvents(process.stdin, rl);
265
- process.stdin.on("keypress", () => {
266
- if (!ready || generating || closed) return;
267
- const line = rl.line || "";
268
- if (!line.startsWith("/")) {
269
- lastSlashHint = "";
270
- return;
271
- }
272
- const key = line.trim();
273
- if (key && key !== lastSlashHint) {
274
- lastSlashHint = key;
275
- rl.output.write(slashHint(line));
276
- rl.prompt(true);
275
+
276
+ function resetDock() {
277
+ if (docked) process.stdout.write("\x1b[r");
278
+ }
279
+
280
+ function setupDock() {
281
+ if (!docked) return;
282
+ dockRows = process.stdout.rows || dockRows;
283
+ dockCols = process.stdout.columns || dockCols;
284
+ historyRow = Math.max(0, dockRows - 2);
285
+ historyCol = Math.min(historyCol, Math.max(0, dockCols - 1));
286
+ process.stdout.write(`\x1b[1;${Math.max(1, dockRows - 1)}r`);
287
+ cursorTo(process.stdout, 0, dockRows - 1);
288
+ clearLine(process.stdout, 0);
289
+ }
290
+
291
+ function redrawInput() {
292
+ if (!docked || closed) return;
293
+ cursorTo(rl.output, 0, dockRows - 1);
294
+ clearLine(rl.output, 0);
295
+ rl.prompt(true);
296
+ }
297
+
298
+ function writeHistory(data) {
299
+ if (!docked) {
300
+ process.stdout.write(data);
301
+ return;
302
+ }
303
+ cursorTo(process.stdout, historyCol, historyRow);
304
+ for (const ch of data) {
305
+ process.stdout.write(ch);
306
+ if (ch === "\n") {
307
+ historyCol = 0;
308
+ } else if (ch === "\r") {
309
+ historyCol = 0;
310
+ } else {
311
+ historyCol += 1;
312
+ if (historyCol >= dockCols) historyCol = 0;
277
313
  }
278
- });
314
+ }
315
+ redrawInput();
279
316
  }
280
317
 
281
- // Forward Python stderr (status / progress messages) to our stderr.
282
- proc.stderr.on("data", (chunk) => { process.stderr.write(chunk); });
318
+ function preserveInputWrite(stream, chunk) {
319
+ const data = String(chunk);
320
+ if (!rl.terminal || closed) {
321
+ stream.write(data);
322
+ return;
323
+ }
324
+ writeHistory(data);
325
+ }
326
+
327
+ setupDock();
328
+ process.stdout.on("resize", () => { setupDock(); redrawInput(); });
329
+
330
+ // Let users type while the Python backend is still loading. Submitted lines
331
+ // are queued until READY, while status logs are written above and the input
332
+ // line is restored afterward.
333
+ ask();
334
+
335
+ // Forward Python stderr (status / progress messages) without destroying the input line.
336
+ proc.stderr.on("data", (chunk) => { preserveInputWrite(process.stderr, chunk); });
283
337
 
284
338
  proc.stdout.on("data", (chunk) => {
285
339
  buf += chunk;
@@ -314,19 +368,19 @@ function main() {
314
368
  const di = buf.indexOf(DONE);
315
369
  if (di !== -1) {
316
370
  const pre = buf.slice(0, di);
317
- if (pre) process.stdout.write(pre);
371
+ if (pre) preserveInputWrite(process.stdout, pre);
318
372
  buf = buf.slice(di + DONE.length);
319
373
  if (buf[0] === "\n") buf = buf.slice(1);
320
374
  generating = false;
321
375
  responseOpen = false;
322
- console.log("");
376
+ preserveInputWrite(process.stdout, "\n");
323
377
  drain();
324
378
  } else {
325
379
  // Stream buffered content, keeping enough tail to catch a partial DONE marker.
326
380
  const keep = DONE.length - 1;
327
381
  if (buf.length > keep) {
328
382
  const out = buf.slice(0, -keep);
329
- if (out) process.stdout.write(out);
383
+ if (out) preserveInputWrite(process.stdout, out);
330
384
  buf = buf.slice(-keep);
331
385
  }
332
386
  }
@@ -335,6 +389,7 @@ function main() {
335
389
  proc.on("error", (e) => { console.error("[cofos] Python error:", e.message); process.exit(1); });
336
390
  proc.on("exit", (c) => {
337
391
  closed = true;
392
+ resetDock();
338
393
  if (c && c !== 0) console.error("[cofos] Python exited with code", c);
339
394
  try { rl.close(); } catch (_) {}
340
395
  // Let queued stdout writes flush before tearing the process down.
@@ -344,6 +399,7 @@ function main() {
344
399
  function shutdown() {
345
400
  if (closed) return;
346
401
  closed = true;
402
+ resetDock();
347
403
  try { proc.stdin.end(); } catch (_) {}
348
404
  try { rl.close(); } catch (_) {}
349
405
  proc.kill("SIGTERM");
@@ -353,6 +409,7 @@ function main() {
353
409
  rl.on("SIGINT", () => { console.log("\n[cofos] Interrupted."); shutdown(); });
354
410
  rl.on("close", () => { if (!closed) shutdown(); });
355
411
  process.on("SIGTERM", shutdown);
412
+ process.on("exit", resetDock);
356
413
 
357
414
  function drain() {
358
415
  while (inputQ.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzhzzzzwill/cofos",
3
- "version": "1.1.14",
3
+ "version": "1.1.19",
4
4
  "description": "COFOS 9B + RAG chat CLI with optional local PDF ingestion.",
5
5
  "type": "module",
6
6
  "bin": {