@lzhzzzzwill/cofos 1.2.4 → 1.3.0
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 +16 -0
- package/bin/cofos.js +147 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ 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`.
|
|
10
|
+
|
|
9
11
|
## Runtime Model
|
|
10
12
|
|
|
11
13
|
The standalone npm package includes a minimal Python runtime under `runtime/`:
|
|
@@ -69,6 +71,18 @@ Disable RAG for a session:
|
|
|
69
71
|
npx @lzhzzzzwill/cofos --no-rag
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
Ask once and exit, useful for scripts and smoke tests:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @lzhzzzzwill/cofos -p "What ROS does TT-T-COF generate under visible-light photocatalysis?"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use the experimental fixed-input TUI:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx @lzhzzzzwill/cofos --tui
|
|
84
|
+
```
|
|
85
|
+
|
|
72
86
|
## Startup Behavior
|
|
73
87
|
|
|
74
88
|
On startup, the CLI does the following:
|
|
@@ -118,6 +132,8 @@ cofos \
|
|
|
118
132
|
| `--cache-dir` | Folder for model, KG/BM25, and parsed PDF cache; defaults to `./.cofos` |
|
|
119
133
|
| `--rebuild-pdf-index` | Force PDF parsing and BM25 rebuild |
|
|
120
134
|
| `--top-k` | Retrieval top-k |
|
|
135
|
+
| `-p`, `--prompt` | Ask one question and exit |
|
|
136
|
+
| `--tui` | Use the experimental fixed-input full-screen TUI |
|
|
121
137
|
| `--max-new-tokens` | Override generation length |
|
|
122
138
|
| `--no-rag` | Run without KG/BM25 retrieval |
|
|
123
139
|
|
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
|
|
3
|
+
import { createInterface } 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";
|
|
@@ -154,12 +154,15 @@ function ensureDeps(py, log = console.log) {
|
|
|
154
154
|
|
|
155
155
|
/* ─── TUI ─── */
|
|
156
156
|
|
|
157
|
-
function createTui() {
|
|
157
|
+
async function createTui() {
|
|
158
|
+
const { default: blessed } = await import("blessed");
|
|
158
159
|
const screen = blessed.screen({
|
|
159
160
|
smartCSR: true,
|
|
160
161
|
fullUnicode: true,
|
|
161
162
|
title: "COFOS",
|
|
163
|
+
mouse: false,
|
|
162
164
|
});
|
|
165
|
+
screen.program.disableMouse();
|
|
163
166
|
|
|
164
167
|
const history = blessed.box({
|
|
165
168
|
top: 0,
|
|
@@ -170,7 +173,7 @@ function createTui() {
|
|
|
170
173
|
alwaysScroll: true,
|
|
171
174
|
keys: true,
|
|
172
175
|
vi: true,
|
|
173
|
-
mouse:
|
|
176
|
+
mouse: false,
|
|
174
177
|
tags: true,
|
|
175
178
|
scrollbar: { ch: " ", track: { bg: "black" }, style: { bg: "cyan" } },
|
|
176
179
|
});
|
|
@@ -213,6 +216,12 @@ function createTui() {
|
|
|
213
216
|
let suggestionMatches = [];
|
|
214
217
|
let selectedSuggestion = 0;
|
|
215
218
|
|
|
219
|
+
function renderBuffer(wasAtBottom = true) {
|
|
220
|
+
history.setContent(buffer);
|
|
221
|
+
if (wasAtBottom) history.setScrollPerc(100);
|
|
222
|
+
screen.render();
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
function append(text = "") {
|
|
217
226
|
const wasAtBottom = history.getScrollPerc() >= 98;
|
|
218
227
|
const data = String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
@@ -226,9 +235,7 @@ function createTui() {
|
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
if (buffer.length > 250000) buffer = buffer.slice(-220000);
|
|
229
|
-
|
|
230
|
-
if (wasAtBottom) history.setScrollPerc(100);
|
|
231
|
-
screen.render();
|
|
238
|
+
renderBuffer(wasAtBottom);
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
function hideSuggestions() {
|
|
@@ -326,25 +333,75 @@ function createTui() {
|
|
|
326
333
|
screen.render();
|
|
327
334
|
|
|
328
335
|
return {
|
|
336
|
+
supportsReplace: true,
|
|
329
337
|
screen,
|
|
330
338
|
input,
|
|
331
339
|
append,
|
|
332
340
|
print: (text = "") => append(String(text) + "\n"),
|
|
333
|
-
clear: () => { buffer = "";
|
|
341
|
+
clear: () => { buffer = ""; renderBuffer(true); },
|
|
342
|
+
length: () => buffer.length,
|
|
343
|
+
replaceFrom: (index, text = "") => { buffer = buffer.slice(0, Math.max(0, index)) + String(text); renderBuffer(true); },
|
|
334
344
|
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
335
345
|
setExitHandler: (fn) => { onExit = fn; },
|
|
336
346
|
focus: () => { input.focus(); screen.render(); },
|
|
337
347
|
setPrompt: (label) => { input.setLabel(` ${label} `); screen.render(); },
|
|
338
|
-
destroy: () =>
|
|
348
|
+
destroy: () => {
|
|
349
|
+
try { screen.program.disableMouse(); } catch (_) {}
|
|
350
|
+
screen.destroy();
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function stripBlessedTags(text) {
|
|
356
|
+
return String(text)
|
|
357
|
+
.replace(/\{\/?(?:bold|cyan-fg|yellow-fg|green-fg|gray-fg|red-fg|black-fg|yellow-bg)\}/g, "")
|
|
358
|
+
.replace(/[{}]/g, "");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createPlainUi() {
|
|
362
|
+
const rl = createInterface({
|
|
363
|
+
input: process.stdin,
|
|
364
|
+
output: process.stdout,
|
|
365
|
+
terminal: true,
|
|
366
|
+
completer: (line) => slashCompletions(line),
|
|
367
|
+
});
|
|
368
|
+
let onSubmit = () => {};
|
|
369
|
+
let onExit = () => {};
|
|
370
|
+
let prompt = "cofos";
|
|
371
|
+
|
|
372
|
+
function setPrompt(label) {
|
|
373
|
+
prompt = label;
|
|
374
|
+
rl.setPrompt(`${C}${prompt}${R} `);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
setPrompt(prompt);
|
|
378
|
+
rl.on("line", (line) => {
|
|
379
|
+
onSubmit(line);
|
|
380
|
+
});
|
|
381
|
+
rl.on("SIGINT", () => onExit());
|
|
382
|
+
rl.on("close", () => onExit());
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
supportsReplace: false,
|
|
386
|
+
append: (text = "") => process.stdout.write(stripBlessedTags(text)),
|
|
387
|
+
print: (text = "") => console.log(stripBlessedTags(text)),
|
|
388
|
+
clear: () => console.clear(),
|
|
389
|
+
length: () => 0,
|
|
390
|
+
replaceFrom: (_index, text = "") => process.stdout.write(stripBlessedTags(text)),
|
|
391
|
+
setSubmitHandler: (fn) => { onSubmit = fn; },
|
|
392
|
+
setExitHandler: (fn) => { onExit = fn; },
|
|
393
|
+
focus: () => rl.prompt(),
|
|
394
|
+
setPrompt,
|
|
395
|
+
destroy: () => { try { rl.close(); } catch (_) {} },
|
|
339
396
|
};
|
|
340
397
|
}
|
|
341
398
|
|
|
342
399
|
/* ─── main ─── */
|
|
343
400
|
|
|
344
|
-
function main() {
|
|
401
|
+
async function main() {
|
|
345
402
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
346
403
|
console.log("\n COFOS CLI — 9B + RAG chat (Willlzh/COFOS)\n");
|
|
347
|
-
console.log(" Usage: cofos [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
404
|
+
console.log(" Usage: cofos [--tui] [-p QUESTION] [--model PATH] [--config PATH] [--pdf-dir DIR] [--cache-dir DIR] [--top-k N] [--no-rag]\n");
|
|
348
405
|
console.log(" Commands in chat:");
|
|
349
406
|
console.log(" /help Show this help");
|
|
350
407
|
console.log(" /exit Quit");
|
|
@@ -356,6 +413,9 @@ function main() {
|
|
|
356
413
|
console.log(" Startup options:");
|
|
357
414
|
console.log(" --pdf-dir DIR Add PDFs from DIR as extra BM25 evidence");
|
|
358
415
|
console.log(" --cache-dir DIR Store model/data cache here (default: ./.cofos)");
|
|
416
|
+
console.log(" --max-new-tokens N Override generation length");
|
|
417
|
+
console.log(" --tui Use the experimental fixed-input TUI");
|
|
418
|
+
console.log(" -p, --prompt TEXT Ask once and exit");
|
|
359
419
|
console.log(" --rebuild-pdf-index Force reparsing PDFs and rebuilding BM25\n");
|
|
360
420
|
console.log(" Multi-line: end a line with \\ to continue on the next line.\n");
|
|
361
421
|
process.exit(0);
|
|
@@ -368,8 +428,11 @@ function main() {
|
|
|
368
428
|
let configPath = DEFAULT_CONFIG;
|
|
369
429
|
let topK = "5";
|
|
370
430
|
let pdfDir = null;
|
|
431
|
+
let maxNewTokens = null;
|
|
371
432
|
let cacheDir = process.env.COFOS_CACHE_DIR ? resolve(process.env.COFOS_CACHE_DIR) : resolve(process.cwd(), ".cofos");
|
|
372
433
|
let rebuildPdfIndex = false;
|
|
434
|
+
let useTui = false;
|
|
435
|
+
let singlePrompt = null;
|
|
373
436
|
const backendArgs = [chatScript];
|
|
374
437
|
|
|
375
438
|
const die = (msg) => { console.error("[cofos] " + msg); process.exit(1); };
|
|
@@ -393,10 +456,17 @@ function main() {
|
|
|
393
456
|
} else if (arg === "--pdf-dir") {
|
|
394
457
|
pdfDir = resolve(value(++i, arg));
|
|
395
458
|
if (!existsSync(pdfDir)) die(`--pdf-dir does not exist: ${pdfDir}`);
|
|
459
|
+
} else if (arg === "--max-new-tokens") {
|
|
460
|
+
maxNewTokens = value(++i, arg);
|
|
461
|
+
if (!/^\d+$/.test(maxNewTokens) || Number(maxNewTokens) < 1) die(`--max-new-tokens must be a positive integer, got "${maxNewTokens}"`);
|
|
396
462
|
} else if (arg === "--cache-dir") {
|
|
397
463
|
cacheDir = resolve(value(++i, arg));
|
|
398
464
|
} else if (arg === "--rebuild-pdf-index") {
|
|
399
465
|
rebuildPdfIndex = true;
|
|
466
|
+
} else if (arg === "--tui") {
|
|
467
|
+
useTui = true;
|
|
468
|
+
} else if (arg === "-p" || arg === "--prompt") {
|
|
469
|
+
singlePrompt = value(++i, arg);
|
|
400
470
|
} else if (arg === "--no-rag") {
|
|
401
471
|
backendArgs.push("--no-rag");
|
|
402
472
|
} else {
|
|
@@ -405,14 +475,15 @@ function main() {
|
|
|
405
475
|
}
|
|
406
476
|
backendArgs.push("--merged-model-path", modelPath, "--config", configPath, "--top-k", topK);
|
|
407
477
|
if (pdfDir) backendArgs.push("--pdf-dir", pdfDir);
|
|
478
|
+
if (maxNewTokens) backendArgs.push("--max-new-tokens", maxNewTokens);
|
|
408
479
|
if (rebuildPdfIndex) backendArgs.push("--rebuild-pdf-index");
|
|
409
480
|
|
|
410
481
|
const py = findPy();
|
|
411
482
|
if (!py) die("Python not found. Install Python 3.9+ and re-run.");
|
|
412
483
|
if (!existsSync(chatScript)) die(`Missing script: ${chatScript}`);
|
|
413
484
|
|
|
414
|
-
const ui = createTui();
|
|
415
|
-
ui.print(plain(render({ modelPath, cacheDir, pdfDir })));
|
|
485
|
+
const ui = useTui ? await createTui() : createPlainUi();
|
|
486
|
+
ui.print(useTui ? plain(render({ modelPath, cacheDir, pdfDir })) : render({ modelPath, cacheDir, pdfDir }));
|
|
416
487
|
ensureDeps(py, (msg) => ui.print(msg));
|
|
417
488
|
ui.print(" Loading COFOS 9B + RAG backend from Hugging Face … first run may take a while");
|
|
418
489
|
if (pdfDir) ui.print(" Local PDF RAG: enabled for this session");
|
|
@@ -439,7 +510,7 @@ function main() {
|
|
|
439
510
|
proc.stderr.setEncoding("utf-8");
|
|
440
511
|
|
|
441
512
|
let buf = "", ready = false, generating = false, responseOpen = false, commandOpen = false, closed = false;
|
|
442
|
-
let
|
|
513
|
+
let currentAnswer = "", assistantStart = -1;
|
|
443
514
|
const inputQ = [];
|
|
444
515
|
let mlBuf = [];
|
|
445
516
|
|
|
@@ -449,6 +520,7 @@ function main() {
|
|
|
449
520
|
try { proc.stdin.end(); } catch (_) {}
|
|
450
521
|
try { proc.kill("SIGTERM"); } catch (_) {}
|
|
451
522
|
ui.destroy();
|
|
523
|
+
process.exit(0);
|
|
452
524
|
}
|
|
453
525
|
|
|
454
526
|
ui.setExitHandler(shutdown);
|
|
@@ -479,7 +551,7 @@ function main() {
|
|
|
479
551
|
input = input.trim();
|
|
480
552
|
if (!input) return false;
|
|
481
553
|
|
|
482
|
-
if (/^\/(exit|quit)$/.test(input)) {
|
|
554
|
+
if (/^\/(exit|quit)$/.test(input)) { shutdown(); return true; }
|
|
483
555
|
if (/^\/(help)$/.test(input)) {
|
|
484
556
|
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.");
|
|
485
557
|
return false;
|
|
@@ -492,13 +564,17 @@ function main() {
|
|
|
492
564
|
ui.clear();
|
|
493
565
|
mlBuf = [];
|
|
494
566
|
responseOpen = false;
|
|
567
|
+
assistantStart = -1;
|
|
568
|
+
currentAnswer = "";
|
|
495
569
|
commandOpen = true;
|
|
496
570
|
proc.stdin.write(input + "\n");
|
|
497
571
|
generating = true;
|
|
498
572
|
return true;
|
|
499
573
|
}
|
|
500
|
-
if (/^\/(topk)(?:\s+\d+)?$/.test(input) || /^\/rag\s+(?:on|off)$/.test(input)) {
|
|
574
|
+
if (/^\/(save)$/.test(input) || /^\/(topk)(?:\s+\d+)?$/.test(input) || /^\/rag\s+(?:on|off)$/.test(input)) {
|
|
501
575
|
responseOpen = false;
|
|
576
|
+
assistantStart = -1;
|
|
577
|
+
currentAnswer = "";
|
|
502
578
|
commandOpen = true;
|
|
503
579
|
proc.stdin.write(input + "\n");
|
|
504
580
|
generating = true;
|
|
@@ -515,6 +591,8 @@ function main() {
|
|
|
515
591
|
|
|
516
592
|
ui.print("{yellow-fg}●{/yellow-fg} " + input);
|
|
517
593
|
responseOpen = false;
|
|
594
|
+
assistantStart = -1;
|
|
595
|
+
currentAnswer = "";
|
|
518
596
|
generating = true;
|
|
519
597
|
proc.stdin.write(input + "\n");
|
|
520
598
|
return true;
|
|
@@ -527,39 +605,42 @@ function main() {
|
|
|
527
605
|
|
|
528
606
|
proc.stderr.on("data", (chunk) => ui.append(chunk));
|
|
529
607
|
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
responseBuffer = "";
|
|
545
|
-
return;
|
|
608
|
+
function escapeTags(text) {
|
|
609
|
+
return String(text).replace(/[{}]/g, "");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatAssistant(answer) {
|
|
613
|
+
const raw = escapeTags(answer).trimStart();
|
|
614
|
+
if (!raw) return "";
|
|
615
|
+
if (raw.includes("</think>")) {
|
|
616
|
+
const [reason, ...answerParts] = raw.split("</think>");
|
|
617
|
+
const cleanReason = reason.replace(/^<think>\s*/i, "").trim();
|
|
618
|
+
const finalAnswer = answerParts.join("</think>").trimStart();
|
|
619
|
+
return finalAnswer
|
|
620
|
+
? "{gray-fg}- " + cleanReason + "{/gray-fg}\n{green-fg}{bold}◆{/bold}{/green-fg} " + finalAnswer
|
|
621
|
+
: "{gray-fg}- " + cleanReason + "{/gray-fg}";
|
|
546
622
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return;
|
|
623
|
+
|
|
624
|
+
const paragraphs = raw.split(/\n\s*\n/);
|
|
625
|
+
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;
|
|
626
|
+
let reasonCount = 0;
|
|
627
|
+
while (reasonCount < paragraphs.length && reasonRe.test(paragraphs[reasonCount].trim())) reasonCount += 1;
|
|
628
|
+
if (reasonCount > 0) {
|
|
629
|
+
const reason = paragraphs.slice(0, reasonCount).join("\n\n").trim();
|
|
630
|
+
const finalAnswer = paragraphs.slice(reasonCount).join("\n\n").trimStart();
|
|
631
|
+
return finalAnswer
|
|
632
|
+
? "{gray-fg}- " + reason + "{/gray-fg}\n{green-fg}{bold}◆{/bold}{/green-fg} " + finalAnswer
|
|
633
|
+
: "{gray-fg}- " + reason + "{/gray-fg}";
|
|
559
634
|
}
|
|
560
|
-
|
|
635
|
+
return "{green-fg}{bold}◆{/bold}{/green-fg} " + raw;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function redrawAssistant() {
|
|
639
|
+
if (assistantStart < 0 || !ui.supportsReplace) return;
|
|
640
|
+
ui.replaceFrom(assistantStart, formatAssistant(currentAnswer));
|
|
561
641
|
}
|
|
562
642
|
|
|
643
|
+
|
|
563
644
|
proc.stdout.on("data", (chunk) => {
|
|
564
645
|
buf += chunk;
|
|
565
646
|
if (!ready) {
|
|
@@ -568,6 +649,8 @@ function main() {
|
|
|
568
649
|
ready = true;
|
|
569
650
|
buf = buf.slice(i + READY.length);
|
|
570
651
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
652
|
+
if (singlePrompt) inputQ.push(singlePrompt);
|
|
653
|
+
else ui.focus();
|
|
571
654
|
drain();
|
|
572
655
|
}
|
|
573
656
|
return;
|
|
@@ -583,7 +666,8 @@ function main() {
|
|
|
583
666
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
584
667
|
generating = false;
|
|
585
668
|
commandOpen = false;
|
|
586
|
-
|
|
669
|
+
if (singlePrompt && inputQ.length === 0) shutdown();
|
|
670
|
+
else ui.focus();
|
|
587
671
|
drain();
|
|
588
672
|
return;
|
|
589
673
|
}
|
|
@@ -598,29 +682,29 @@ function main() {
|
|
|
598
682
|
buf = buf.slice(ri + RESPONSE.length);
|
|
599
683
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
600
684
|
responseOpen = true;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
responseBuffer = "";
|
|
604
|
-
ui.append("{green-fg}{bold}◆{/bold}{/green-fg} ");
|
|
685
|
+
assistantStart = ui.supportsReplace ? ui.length() : 0;
|
|
686
|
+
currentAnswer = "";
|
|
605
687
|
}
|
|
606
688
|
|
|
607
689
|
const di = buf.indexOf(DONE);
|
|
608
690
|
if (di !== -1) {
|
|
609
691
|
const pre = buf.slice(0, di);
|
|
610
|
-
if (pre)
|
|
692
|
+
if (pre) { currentAnswer += pre; redrawAssistant(); }
|
|
611
693
|
buf = buf.slice(di + DONE.length);
|
|
612
694
|
if (buf[0] === "\n") buf = buf.slice(1);
|
|
613
|
-
if (
|
|
695
|
+
if (ui.supportsReplace) redrawAssistant();
|
|
696
|
+
else ui.print(formatAssistant(currentAnswer));
|
|
614
697
|
generating = false;
|
|
615
698
|
responseOpen = false;
|
|
616
|
-
ui.append("\n");
|
|
617
|
-
|
|
699
|
+
if (ui.supportsReplace) ui.append("\n");
|
|
700
|
+
if (singlePrompt && inputQ.length === 0) shutdown();
|
|
701
|
+
else ui.focus();
|
|
618
702
|
drain();
|
|
619
703
|
} else {
|
|
620
704
|
const keep = DONE.length - 1;
|
|
621
705
|
if (buf.length > keep) {
|
|
622
706
|
const out = buf.slice(0, -keep);
|
|
623
|
-
if (out)
|
|
707
|
+
if (out) { currentAnswer += out; redrawAssistant(); }
|
|
624
708
|
buf = buf.slice(-keep);
|
|
625
709
|
}
|
|
626
710
|
}
|
|
@@ -628,12 +712,18 @@ function main() {
|
|
|
628
712
|
|
|
629
713
|
proc.on("error", (e) => { ui.print("[cofos] Python error: " + e.message); shutdown(); });
|
|
630
714
|
proc.on("exit", (c) => {
|
|
631
|
-
closed
|
|
632
|
-
|
|
633
|
-
ui.
|
|
715
|
+
if (closed) return;
|
|
716
|
+
ui.print(c && c !== 0 ? "[cofos] Python exited with code " + c : "[cofos] Python backend exited.");
|
|
717
|
+
ui.print("Press Ctrl+C to close.");
|
|
718
|
+
ui.focus();
|
|
634
719
|
process.exitCode = c ?? 0;
|
|
635
720
|
});
|
|
636
721
|
|
|
637
722
|
}
|
|
638
723
|
|
|
724
|
+
process.on("uncaughtException", (err) => {
|
|
725
|
+
try { process.stderr.write(`[cofos] Uncaught error: ${err?.stack || err}\n`); } catch (_) {}
|
|
726
|
+
process.exit(1);
|
|
727
|
+
});
|
|
728
|
+
|
|
639
729
|
main();
|