cumora 0.1.45 → 0.1.47
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/dist/cli.js +227 -23
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ../server/src/agents/computer/daemon.ts
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdir as mkdir2, writeFile as writeFile2, readFile, chmod } from "node:fs/promises";
|
|
5
6
|
import { homedir, hostname } from "node:os";
|
|
6
7
|
import { join as join2 } from "node:path";
|
|
@@ -56,8 +57,37 @@ async function exists(p) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
async function ensureCommonHome(home) {
|
|
60
|
+
await mkdir(join(home, "memory"), { recursive: true });
|
|
59
61
|
await mkdir(join(home, "notes"), { recursive: true });
|
|
60
62
|
await mkdir(join(home, "workspace"), { recursive: true });
|
|
63
|
+
const memoryIndex = join(home, "memory", "MEMORY.md");
|
|
64
|
+
if (!await exists(memoryIndex)) {
|
|
65
|
+
await writeFile(
|
|
66
|
+
memoryIndex,
|
|
67
|
+
"# Memory index\n\nOne line per durable fact, pointing at the file that holds it:\n`- [Title](file.md) \u2014 one-line hook`\n\nWrite the fact itself in its own `memory/<topic>.md` file; keep this index short.\n",
|
|
68
|
+
"utf8"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var MAX_FAILURE_LINES = 30;
|
|
73
|
+
var MAX_FAILURE_CHARS = 4e3;
|
|
74
|
+
var ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
75
|
+
function cleanLine(line) {
|
|
76
|
+
return line.replace(ANSI_RE, "").replace(/\r/g, "").trim();
|
|
77
|
+
}
|
|
78
|
+
function pushTail(lines, line) {
|
|
79
|
+
if (!line) return;
|
|
80
|
+
lines.push(line);
|
|
81
|
+
if (lines.length > MAX_FAILURE_LINES) lines.shift();
|
|
82
|
+
}
|
|
83
|
+
function failurePreview(args) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (args.stderr.length > 0) parts.push(args.stderr.join("\n"));
|
|
86
|
+
if (args.stdout.length > 0) parts.push(args.stdout.join("\n"));
|
|
87
|
+
const detail = parts.join("\n").trim();
|
|
88
|
+
const prefix = args.signalName ? `process terminated by ${args.signalName}` : `process exited with code ${args.exitCode}`;
|
|
89
|
+
return detail ? `${prefix}
|
|
90
|
+
${detail}`.slice(0, MAX_FAILURE_CHARS) : prefix;
|
|
61
91
|
}
|
|
62
92
|
function spawnEngine(bin, args, { home, env, onLog, signal }) {
|
|
63
93
|
return new Promise((resolve, reject) => {
|
|
@@ -66,20 +96,29 @@ function spawnEngine(bin, args, { home, env, onLog, signal }) {
|
|
|
66
96
|
child.kill("SIGTERM");
|
|
67
97
|
};
|
|
68
98
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
-
const
|
|
99
|
+
const stderrTail = [];
|
|
100
|
+
const stdoutTail = [];
|
|
101
|
+
const pump = (stream, buf) => {
|
|
70
102
|
for (const line of buf.toString("utf8").split("\n")) {
|
|
71
|
-
|
|
103
|
+
const cleaned = cleanLine(line);
|
|
104
|
+
if (!cleaned) continue;
|
|
105
|
+
pushTail(stream === "stderr" ? stderrTail : stdoutTail, cleaned);
|
|
106
|
+
onLog(cleaned);
|
|
72
107
|
}
|
|
73
108
|
};
|
|
74
|
-
child.stdout.on("data", pump);
|
|
75
|
-
child.stderr.on("data", pump);
|
|
109
|
+
child.stdout.on("data", (buf) => pump("stdout", buf));
|
|
110
|
+
child.stderr.on("data", (buf) => pump("stderr", buf));
|
|
76
111
|
child.on("error", (err) => {
|
|
77
112
|
signal.removeEventListener("abort", onAbort);
|
|
78
113
|
reject(err);
|
|
79
114
|
});
|
|
80
|
-
child.on("close", (code) => {
|
|
115
|
+
child.on("close", (code, signalName) => {
|
|
81
116
|
signal.removeEventListener("abort", onAbort);
|
|
82
|
-
|
|
117
|
+
const exitCode = code ?? (signalName ? 128 : 1);
|
|
118
|
+
resolve({
|
|
119
|
+
exitCode,
|
|
120
|
+
error: exitCode === 0 ? void 0 : failurePreview({ exitCode, signalName, stderr: stderrTail, stdout: stdoutTail })
|
|
121
|
+
});
|
|
83
122
|
});
|
|
84
123
|
});
|
|
85
124
|
}
|
|
@@ -92,7 +131,13 @@ var PERSONA_HEADER = (p) => `# ${p.name}${p.role ? ` \u2014 ${p.role}` : ""}
|
|
|
92
131
|
You are **${p.name}**, a member of a team that collaborates in Cumora (a team chat).
|
|
93
132
|
This directory is your private home and your working directory \u2014 it persists
|
|
94
133
|
across wakes and is yours alone. Its layout:
|
|
95
|
-
- \`CLAUDE.md\` (this file)
|
|
134
|
+
- \`CLAUDE.md\` (this file) \u2014 always loaded each wake; keep it short.
|
|
135
|
+
- \`memory/\` \u2014 your durable memory. There is NO hidden memory store: to remember
|
|
136
|
+
something across wakes you MUST write it to a file here (e.g. \`memory/<topic>.md\`)
|
|
137
|
+
and add a one-line pointer in \`memory/MEMORY.md\`. Saying "I'll remember" without
|
|
138
|
+
writing a file means you will NOT remember. At the start of each wake, read
|
|
139
|
+
\`memory/MEMORY.md\` (and the files it points to) to recall what you know.
|
|
140
|
+
- \`notes/\` \u2014 scratch notes and drafts.
|
|
96
141
|
- \`.claude/skills/\` \u2014 your skills.
|
|
97
142
|
- \`workspace/\` \u2014 **put all project files and scratch here**: git clones, builds,
|
|
98
143
|
downloads, temp files. Always \`cd workspace\` (or use \`workspace/\u2026\` paths) for
|
|
@@ -177,6 +222,7 @@ var DEFAULT_SERVER = process.env.CUMORA_SERVER_URL || "https://api.cumora.ai";
|
|
|
177
222
|
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
178
223
|
var AGENT_POLL_MS = 6e4;
|
|
179
224
|
var HEARTBEAT_MS = 3e4;
|
|
225
|
+
var MAX_VISIBLE_ERROR_CHARS = 900;
|
|
180
226
|
function parseArgs(argv) {
|
|
181
227
|
const out = {};
|
|
182
228
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -210,6 +256,49 @@ async function runtimeBest(serverUrl, path, token, body) {
|
|
|
210
256
|
return null;
|
|
211
257
|
}
|
|
212
258
|
}
|
|
259
|
+
async function runtimeGet(serverUrl, path, token) {
|
|
260
|
+
try {
|
|
261
|
+
const res = await fetch(`${serverUrl}/runtime${path}`, {
|
|
262
|
+
method: "GET",
|
|
263
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
264
|
+
});
|
|
265
|
+
return res.ok ? await res.json().catch(() => null) : null;
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function hashText(text) {
|
|
271
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 12);
|
|
272
|
+
}
|
|
273
|
+
function conciseError(text) {
|
|
274
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "").replace(/\r/g, "").trim().slice(0, MAX_VISIBLE_ERROR_CHARS);
|
|
275
|
+
}
|
|
276
|
+
function authFailureHint(engine, detail) {
|
|
277
|
+
if (!/(auth|login|token|quota|credit|billing|subscription|rate limit|usage limit|api key)/i.test(detail)) {
|
|
278
|
+
return "Check the daemon terminal for details, then wake the agent again.";
|
|
279
|
+
}
|
|
280
|
+
if (engine === "claude") {
|
|
281
|
+
return "Open Claude Code on that computer and sign in, refresh quota, or add credits, then wake the agent again.";
|
|
282
|
+
}
|
|
283
|
+
return "Open Codex on that computer and refresh its login or quota, then wake the agent again.";
|
|
284
|
+
}
|
|
285
|
+
function missingEngineMessage() {
|
|
286
|
+
return [
|
|
287
|
+
"no supported local agent engine found on PATH",
|
|
288
|
+
"",
|
|
289
|
+
"Install and sign in to at least one of:",
|
|
290
|
+
" - Claude Code: install the `claude` CLI, then run `claude` once to sign in",
|
|
291
|
+
" - Codex: install the `codex` CLI, then run `codex` once to sign in",
|
|
292
|
+
"",
|
|
293
|
+
"After that, rerun:",
|
|
294
|
+
" npx cumora agent computer --pair <code>"
|
|
295
|
+
].join("\n");
|
|
296
|
+
}
|
|
297
|
+
async function requireLocalEngine() {
|
|
298
|
+
const engines = await detectEngines();
|
|
299
|
+
if (engines.length === 0) throw new Error(missingEngineMessage());
|
|
300
|
+
return engines;
|
|
301
|
+
}
|
|
213
302
|
async function loadConfig() {
|
|
214
303
|
try {
|
|
215
304
|
return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
|
|
@@ -265,10 +354,7 @@ async function detectHostName() {
|
|
|
265
354
|
return base || "My computer";
|
|
266
355
|
}
|
|
267
356
|
async function doPair(code, serverUrl) {
|
|
268
|
-
const engines = await
|
|
269
|
-
if (engines.length === 0) {
|
|
270
|
-
console.warn("[computer] warning: no engine found on PATH (need `claude` or `codex`).");
|
|
271
|
-
}
|
|
357
|
+
const engines = await requireLocalEngine();
|
|
272
358
|
const paired = await api(
|
|
273
359
|
serverUrl,
|
|
274
360
|
"/api/computers/pair",
|
|
@@ -332,16 +418,93 @@ var AgentRunner = class {
|
|
|
332
418
|
CUMORA_AGENT_ID: this.agent.id
|
|
333
419
|
};
|
|
334
420
|
}
|
|
335
|
-
|
|
421
|
+
visibleEngineError(exitCode, detail) {
|
|
422
|
+
const raw = conciseError(detail || `process exited with code ${exitCode}`);
|
|
423
|
+
const redacted = raw.split(this.home).join("<agent home>").split(homedir()).join("~");
|
|
424
|
+
return `local ${this.adapter.id} failed (exit ${exitCode}): ${redacted}`;
|
|
425
|
+
}
|
|
426
|
+
async publishEngineFailure(args) {
|
|
427
|
+
if (args.runId) {
|
|
428
|
+
await runtimeBest(this.cfg.serverUrl, "/events", args.token, {
|
|
429
|
+
runId: args.runId,
|
|
430
|
+
kind: "engine.failed",
|
|
431
|
+
level: "error",
|
|
432
|
+
title: `${this.adapter.id} exited ${args.exitCode}`,
|
|
433
|
+
data: { error: args.error },
|
|
434
|
+
stage: "failed"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const hint = authFailureHint(this.adapter.id, args.error);
|
|
438
|
+
const conversationIds = await this.failureConversationIds(args.token, args.conversationId);
|
|
439
|
+
await Promise.all(conversationIds.map((conversationId) => runtimeBest(this.cfg.serverUrl, "/notices", args.token, {
|
|
440
|
+
conversationId,
|
|
441
|
+
agentId: this.agent.id,
|
|
442
|
+
noticeKind: "byoa_engine_failed",
|
|
443
|
+
text: `${this.agent.name} could not run on local ${this.adapter.id}: ${args.error}
|
|
444
|
+
${hint}`,
|
|
445
|
+
dedupeKey: `byoa_engine_failed:${this.agent.id}:${conversationId}:${hashText(args.error)}`,
|
|
446
|
+
dedupeTtlSec: 900
|
|
447
|
+
})));
|
|
448
|
+
}
|
|
449
|
+
async failureConversationIds(token, conversationId) {
|
|
450
|
+
if (conversationId) return [conversationId];
|
|
451
|
+
const inbox = await runtimeGet(this.cfg.serverUrl, "/inbox", token);
|
|
452
|
+
const ids = /* @__PURE__ */ new Set();
|
|
453
|
+
for (const row of inbox?.rows ?? []) {
|
|
454
|
+
if (typeof row.conversation_id === "string" && row.conversation_id) ids.add(row.conversation_id);
|
|
455
|
+
if (ids.size >= 5) break;
|
|
456
|
+
}
|
|
457
|
+
return [...ids];
|
|
458
|
+
}
|
|
459
|
+
/** Preload the memory index (memory/MEMORY.md) so it's ALWAYS in the wake
|
|
460
|
+
* prompt — the lightweight BYOA analog of the cloud agent's RAG memory
|
|
461
|
+
* preload. We inject only the small index; the agent opens detail files on
|
|
462
|
+
* demand. Capped so a runaway index can't blow up the prompt. */
|
|
463
|
+
async memoryDigest() {
|
|
464
|
+
try {
|
|
465
|
+
const txt = (await readFile(join2(this.home, "memory", "MEMORY.md"), "utf8")).trim();
|
|
466
|
+
if (!txt) return "";
|
|
467
|
+
return txt.length > 4e3 ? `${txt.slice(0, 4e3)}
|
|
468
|
+
\u2026(truncated \u2014 cat the file for the rest)` : txt;
|
|
469
|
+
} catch {
|
|
470
|
+
return "";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async inboxTriage(token) {
|
|
474
|
+
return runtimeGet(this.cfg.serverUrl, "/inbox-triage", token);
|
|
475
|
+
}
|
|
476
|
+
formatTriageNote(triage) {
|
|
477
|
+
if (!triage) return "";
|
|
478
|
+
const note = typeof triage?.promptNote === "string" ? triage.promptNote.trim() : "";
|
|
479
|
+
if (!note) return "";
|
|
480
|
+
const state = triage.actionable === false ? "not relevant" : "relevant";
|
|
481
|
+
const source = typeof triage.source === "string" ? triage.source : "server";
|
|
482
|
+
const reason = typeof triage.reason === "string" && triage.reason.trim() ? `
|
|
483
|
+
Reason: ${triage.reason.trim().slice(0, 500)}` : "";
|
|
484
|
+
return `Small-brain inbox triage (${state}, ${source}): ${note}${reason}`;
|
|
485
|
+
}
|
|
486
|
+
prompt(memoryDigest, triageNote) {
|
|
336
487
|
return `You've been woken because there's new activity in your Cumora conversations.
|
|
337
488
|
|
|
338
|
-
|
|
489
|
+
` + (triageNote ? `${triageNote}
|
|
490
|
+
|
|
491
|
+
` : ``) + `Use the \`cumora\` tool to catch up and respond:
|
|
339
492
|
1. \`cumora inbox\` \u2014 see what's unread
|
|
340
493
|
2. \`cumora messages <conversationId> --tail 30\` \u2014 read the relevant thread(s)
|
|
341
|
-
3. \`cumora
|
|
494
|
+
3. \`cumora glance <conversationId>\` \u2014 in group threads, read the room right before replying
|
|
495
|
+
4. \`cumora reply <conversationId> '<text>'\` \u2014 reply in your own voice
|
|
496
|
+
|
|
497
|
+
` + (memoryDigest ? `Your memory index (\`memory/MEMORY.md\`) \u2014 consult before replying; \`cat memory/<file>\` for any detail it points to:
|
|
498
|
+
${memoryDigest}
|
|
499
|
+
|
|
500
|
+
` : ``) + `Memory is your ONLY durable store. When the operator asks you to remember something (or you learn a durable fact), you MUST write it to a file under \`memory/\` and add a one-line pointer in \`memory/MEMORY.md\` \u2014 actually run the write. Saying "got it, I'll remember" does NOT persist anything.
|
|
342
501
|
|
|
343
502
|
Mentions: to notify or address a specific teammate, write @<their-id> using their participant id (the short id shown in \`cumora messages\` and \`cumora participants\`), NOT their display name. "@Alex P" does nothing; "@alex-3f9a" actually pings them. Run \`cumora participants\` if you need to look up an id.
|
|
344
503
|
|
|
504
|
+
When a HUMAN says \`@all\` in a group conversation, they addressed YOU too. Do not silently ack an @all just because it is simple. If it is a greeting like "hello @all" or "hi @all", reply briefly as yourself. If it asks every agent to introduce themselves, vote, count, or each give an answer, you should answer your part. Before replying in a group, run \`cumora glance <conversationId>\` so you do not duplicate a peer's identical answer.
|
|
505
|
+
|
|
506
|
+
Concrete task handoff: if the human asks for one concrete deliverable in a group (generate images, draft the email, summarize the thread, make the chart, fix X) and another teammate has clearly claimed or is already executing that exact task, stay quiet unless you have been specifically named or the result lands and needs your specific missing contribution. Do not add side commentary while a teammate is working.
|
|
507
|
+
|
|
345
508
|
Know when to stay quiet \u2014 this is how teams avoid endless back-and-forth:
|
|
346
509
|
- Read the WHOLE thread first, including your own past replies. If you've already made your point, don't repeat it. Saying nothing is better than echoing.
|
|
347
510
|
- Only reply where you genuinely add something the thread doesn't already have. Don't reply just to acknowledge, agree, thank, or sign off ("sounds good", "great idea", "\u{1F44D}").
|
|
@@ -367,24 +530,43 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
367
530
|
const token = this.token;
|
|
368
531
|
const convo = this.lastWakeConvo;
|
|
369
532
|
this.lastWakeConvo = null;
|
|
533
|
+
const triage = await this.inboxTriage(token);
|
|
534
|
+
if (triage?.actionable === false) {
|
|
535
|
+
const reason = typeof triage.reason === "string" ? triage.reason : "not relevant";
|
|
536
|
+
console.log(`[computer] ${this.agent.id} skipped by small-brain inbox triage: ${reason}`);
|
|
537
|
+
await runtimeBest(this.cfg.serverUrl, "/status", token, { status: "avail" });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
370
540
|
await runtimeBest(this.cfg.serverUrl, "/status", token, { status: "thinking" });
|
|
371
541
|
let typingTimer;
|
|
372
542
|
if (convo) {
|
|
373
543
|
const ping = () => {
|
|
374
544
|
void runtimeBest(this.cfg.serverUrl, "/typing", token, { conversationId: convo, done: false });
|
|
375
545
|
};
|
|
546
|
+
const thinkingPing = () => {
|
|
547
|
+
void runtimeBest(this.cfg.serverUrl, "/thinking/mark", token, { conversationIds: [convo], ttlSec: 60 });
|
|
548
|
+
};
|
|
376
549
|
ping();
|
|
377
|
-
|
|
550
|
+
thinkingPing();
|
|
551
|
+
typingTimer = setInterval(() => {
|
|
552
|
+
ping();
|
|
553
|
+
thinkingPing();
|
|
554
|
+
}, 6e3);
|
|
378
555
|
}
|
|
379
556
|
const run = await runtimeBest(this.cfg.serverUrl, "/runs", token, {
|
|
380
557
|
trigger: { source: "byoa", engine: this.adapter.id }
|
|
381
558
|
});
|
|
382
559
|
const controller = new AbortController();
|
|
383
560
|
let exitCode = 0;
|
|
561
|
+
let engineError = null;
|
|
384
562
|
try {
|
|
563
|
+
const [memoryDigest, triageNote] = await Promise.all([
|
|
564
|
+
this.memoryDigest(),
|
|
565
|
+
Promise.resolve(this.formatTriageNote(triage))
|
|
566
|
+
]);
|
|
385
567
|
const result = await this.adapter.run({
|
|
386
568
|
home: this.home,
|
|
387
|
-
prompt: this.prompt(),
|
|
569
|
+
prompt: this.prompt(memoryDigest, triageNote),
|
|
388
570
|
env: this.engineEnv(),
|
|
389
571
|
model: this.agent.model,
|
|
390
572
|
fastModel: this.agent.fastModel,
|
|
@@ -392,17 +574,32 @@ If nothing genuinely needs you, it's fine to do nothing and stop. When finished,
|
|
|
392
574
|
signal: controller.signal
|
|
393
575
|
});
|
|
394
576
|
exitCode = result.exitCode;
|
|
577
|
+
if (result.error) engineError = this.visibleEngineError(exitCode, result.error);
|
|
395
578
|
} catch (err) {
|
|
396
579
|
console.error(`[computer] ${this.agent.id} engine spawn failed:`, err instanceof Error ? err.message : err);
|
|
397
580
|
exitCode = 1;
|
|
581
|
+
engineError = this.visibleEngineError(exitCode, err instanceof Error ? err.stack || err.message : String(err));
|
|
398
582
|
} finally {
|
|
399
583
|
if (typingTimer) clearInterval(typingTimer);
|
|
400
|
-
if (convo)
|
|
584
|
+
if (convo) {
|
|
585
|
+
await runtimeBest(this.cfg.serverUrl, "/typing", token, { conversationId: convo, done: true });
|
|
586
|
+
await runtimeBest(this.cfg.serverUrl, "/thinking/unmark", token, { conversationIds: [convo] });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (engineError) {
|
|
590
|
+
await this.publishEngineFailure({
|
|
591
|
+
token,
|
|
592
|
+
runId: run?.runId,
|
|
593
|
+
conversationId: convo,
|
|
594
|
+
error: engineError,
|
|
595
|
+
exitCode
|
|
596
|
+
});
|
|
401
597
|
}
|
|
402
598
|
if (run?.runId) {
|
|
403
599
|
await runtimeBest(this.cfg.serverUrl, `/runs/${run.runId}/finish`, token, {
|
|
404
600
|
status: exitCode === 0 ? "completed" : "failed",
|
|
405
|
-
summary: `byoa ${this.adapter.id} run (exit ${exitCode})
|
|
601
|
+
summary: engineError ?? `byoa ${this.adapter.id} run (exit ${exitCode})`,
|
|
602
|
+
error: engineError
|
|
406
603
|
});
|
|
407
604
|
}
|
|
408
605
|
await runtimeBest(this.cfg.serverUrl, "/status", token, { status: "avail" });
|
|
@@ -453,10 +650,12 @@ async function doRun(serverOverride) {
|
|
|
453
650
|
return;
|
|
454
651
|
}
|
|
455
652
|
if (serverOverride) cfg.serverUrl = serverOverride;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
653
|
+
let available;
|
|
654
|
+
try {
|
|
655
|
+
available = await requireLocalEngine();
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.error(`[computer] ${err instanceof Error ? err.message : String(err)}`);
|
|
658
|
+
process.exitCode = 70;
|
|
460
659
|
return;
|
|
461
660
|
}
|
|
462
661
|
console.log(`[computer] starting ${cfg.computerId} @ ${cfg.serverUrl} (engines: ${available.join(", ")})`);
|
|
@@ -546,4 +745,9 @@ async function main() {
|
|
|
546
745
|
);
|
|
547
746
|
process.exit(argv.length ? 1 : 0);
|
|
548
747
|
}
|
|
549
|
-
void main()
|
|
748
|
+
void main().catch((err) => {
|
|
749
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
750
|
+
process.stderr.write(`cumora: ${message}
|
|
751
|
+
`);
|
|
752
|
+
process.exit(70);
|
|
753
|
+
});
|