@runravel/ravel 0.1.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +202 -0
  3. package/README.md +68 -0
  4. package/bin/ravel.mjs +6 -0
  5. package/examples/acme/agent.md +14 -0
  6. package/examples/acme/growth/agent.md +19 -0
  7. package/examples/acme/growth/copywriter/agent.md +13 -0
  8. package/examples/acme/growth/copywriter/tools.json +11 -0
  9. package/examples/acme/growth/researcher/agent.md +11 -0
  10. package/examples/acme/processes/prospect-outreach.process.md +24 -0
  11. package/examples/harbor/agent.md +31 -0
  12. package/examples/harbor/processes/new-client-quote.process.md +31 -0
  13. package/examples/harbor/processes/resolve-ticket-batch.process.md +33 -0
  14. package/examples/harbor/sales/agent.md +29 -0
  15. package/examples/harbor/sales/sdr/agent.md +24 -0
  16. package/examples/harbor/sales/solutions/agent.md +29 -0
  17. package/examples/harbor/sales/solutions/tools.json +16 -0
  18. package/examples/harbor/support/agent.md +31 -0
  19. package/examples/harbor/support/kb-writer/agent.md +25 -0
  20. package/examples/harbor/support/kb-writer/tools.json +10 -0
  21. package/examples/harbor/support/qa-reviewer/agent.md +23 -0
  22. package/examples/harbor/support/tools.json +16 -0
  23. package/examples/harbor/support/triage/agent.md +24 -0
  24. package/examples/harbor/support/triage/tools.json +10 -0
  25. package/examples/plugin-demo/agent.md +15 -0
  26. package/examples/plugin-demo/processes/jot.process.md +21 -0
  27. package/examples/plugin-demo/scribe/agent.md +15 -0
  28. package/examples/plugin-demo/scribe/plugin.ts +53 -0
  29. package/examples/plugin-demo/scribe/tools.json +9 -0
  30. package/package.json +65 -0
  31. package/src/cli/main.ts +428 -0
  32. package/src/control-plane/registry.ts +294 -0
  33. package/src/control-plane/watcher.ts +132 -0
  34. package/src/domain/ids.ts +17 -0
  35. package/src/domain/pricing.ts +51 -0
  36. package/src/domain/types.ts +168 -0
  37. package/src/index.ts +35 -0
  38. package/src/memory/genericTools.ts +276 -0
  39. package/src/memory/kv.ts +52 -0
  40. package/src/memory/store.ts +76 -0
  41. package/src/messaging/bus.ts +143 -0
  42. package/src/messaging/inbox.ts +119 -0
  43. package/src/orchestrator/orchestrator.ts +270 -0
  44. package/src/orchestrator/planner.ts +218 -0
  45. package/src/platform/app.ts +287 -0
  46. package/src/plugins/loader.ts +86 -0
  47. package/src/plugins/server.ts +41 -0
  48. package/src/plugins/types.ts +96 -0
  49. package/src/runtime/agent.ts +488 -0
  50. package/src/runtime/engine.ts +84 -0
  51. package/src/runtime/fakeEngine.ts +75 -0
  52. package/src/runtime/lifecycle.ts +85 -0
  53. package/src/runtime/officeActions.ts +58 -0
  54. package/src/runtime/officeTools.ts +42 -0
  55. package/src/runtime/sdkEngine.ts +213 -0
  56. package/src/schemas/agent.ts +47 -0
  57. package/src/schemas/common.ts +52 -0
  58. package/src/schemas/frontmatter.ts +36 -0
  59. package/src/schemas/process.ts +55 -0
  60. package/src/schemas/tools.ts +83 -0
  61. package/src/secrets/store.ts +131 -0
  62. package/src/service/scheduler.ts +377 -0
  63. package/src/service/server.ts +554 -0
  64. package/src/trust/approval.ts +180 -0
  65. package/src/trust/audit.ts +195 -0
  66. package/src/trust/budget.ts +64 -0
  67. package/src/trust/emittingAudit.ts +32 -0
  68. package/src/trust/executor.ts +73 -0
  69. package/src/trust/killswitch.ts +73 -0
  70. package/src/trust/observability.ts +97 -0
  71. package/src/trust/proposals.ts +114 -0
  72. package/ui/dist/assets/index-C6CxDaPS.js +44 -0
  73. package/ui/dist/assets/index-CD-lhs0Z.css +1 -0
  74. package/ui/dist/index.html +13 -0
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ import { parseArgs } from "node:util";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promises as fs } from "node:fs";
6
+ import readline from "node:readline/promises";
7
+ import { stdin, stdout } from "node:process";
8
+ import { App } from "../platform/app.js";
9
+ import { SdkEngine } from "../runtime/sdkEngine.js";
10
+ import { createServer } from "../service/server.js";
11
+ import { EmittingAudit } from "../trust/emittingAudit.js";
12
+ import { JsonlAudit } from "../trust/audit.js";
13
+ import { compileRegistry } from "../control-plane/registry.js";
14
+ import { parseDotEnv } from "../secrets/store.js";
15
+ import type { ApprovalRequest, Usage } from "../domain/types.js";
16
+ import { totalTokens } from "../domain/types.js";
17
+
18
+ const HELP = `Ravel — run an agentic team defined as a folder tree.
19
+
20
+ Usage:
21
+ ravel create <name>
22
+ ravel validate [--dir <org>]
23
+ ravel run <process-name> [--dir <org>] [--dry-run] [--sync] [--input k=v]... [--file <path>]...
24
+ ravel chat <node-id> <message...> [--dir <org>]
25
+ ravel proposals [list|approve <id>|reject <id>] [--dir <org>]
26
+ ravel dashboard [--dir <org>]
27
+ ravel watch [--dir <org>]
28
+ ravel serve [--dir <org>] [--port 4317] [--host 127.0.0.1] [--state-dir <path>] [--read-only-config]
29
+
30
+ Options:
31
+ --dir <path> Org root folder (default: current directory)
32
+ --host <addr> Interface to bind (default: 127.0.0.1 — loopback only.
33
+ The API has no auth; use 0.0.0.0 only behind a gateway)
34
+ --state-dir <path> Where runtime state (memory, audit, runs) lives
35
+ (default: <org>/.ravel)
36
+ --read-only-config Disable config/secret writes over HTTP (PUT /api/files,
37
+ PUT /api/secrets) — for workers whose config comes from git
38
+ --dry-run Agents produce intended actions but execute no tools
39
+ --sync Block on consequential actions with an interactive y/N prompt
40
+ (default is async: actions queue as proposals to approve later)
41
+ --input k=v Run input passed to the process (repeatable)
42
+ --file <path> Source file staged into each dispatched worker's workdir (repeatable)
43
+ -v, --verbose Stream the audit trail (turns, dispatches, tools, proposals) to stderr
44
+
45
+ Example:
46
+ ravel create my-team && ravel serve --dir my-team
47
+ ravel run "Resolve Ticket Batch" --dir examples/harbor \\
48
+ --file ./tickets.json --input product="Acme CRM"
49
+ `;
50
+
51
+ /**
52
+ * Load `.env` files into process.env without clobbering already-exported vars
53
+ * (existing environment wins, per dotenv convention). Tries the current dir and
54
+ * the org `--dir`, so credentials can live next to the org folder.
55
+ */
56
+ async function loadEnvFiles(orgDir: string): Promise<void> {
57
+ const seen = new Set<string>();
58
+ for (const dir of [process.cwd(), path.resolve(orgDir)]) {
59
+ const file = path.join(dir, ".env");
60
+ if (seen.has(file)) continue;
61
+ seen.add(file);
62
+ let content: string;
63
+ try {
64
+ content = await fs.readFile(file, "utf8");
65
+ } catch {
66
+ continue; // no .env here — fine
67
+ }
68
+ // Global base layer: cwd + org-root .env into process.env (existing env wins).
69
+ for (const [key, value] of Object.entries(parseDotEnv(content))) {
70
+ if (process.env[key] === undefined) process.env[key] = value;
71
+ }
72
+ }
73
+ }
74
+
75
+ function fmtUsage(u: Usage): string {
76
+ const total = totalTokens(u);
77
+ const cached = u.cacheReadTokens;
78
+ const hitRate = total > 0 ? Math.round((cached / total) * 100) : 0;
79
+ return `${total} tok ($${u.usd.toFixed(4)}) · ${hitRate}% cache hit (${cached} read / ${u.cacheCreationTokens} written)`;
80
+ }
81
+
82
+ /** Wire the approval queue to an interactive y/n prompt on the terminal. */
83
+ function attachInteractiveApprovals(app: App): () => void {
84
+ const rl = readline.createInterface({ input: stdin, output: stdout });
85
+ const handler = async (req: ApprovalRequest) => {
86
+ stdout.write(`\n⚠️ Approval needed — agent "${req.nodeId}" wants to use "${req.toolName}"\n`);
87
+ stdout.write(` input: ${JSON.stringify(req.input)}\n`);
88
+ if (req.rationale) stdout.write(` why: ${req.rationale}\n`);
89
+ const answer = (await rl.question(" allow? [y/N] ")).trim().toLowerCase();
90
+ await app.resolveApproval(req.id, answer === "y" || answer === "yes" ? "allow" : "deny");
91
+ };
92
+ app.approvals.on("requested", handler);
93
+ return () => {
94
+ app.approvals.off("requested", handler);
95
+ rl.close();
96
+ };
97
+ }
98
+
99
+ /** Resolve when the process is asked to shut down (Ctrl-C or a container stop). */
100
+ function shutdownSignal(): Promise<void> {
101
+ return new Promise<void>((resolve) => {
102
+ process.once("SIGINT", () => resolve());
103
+ process.once("SIGTERM", () => resolve());
104
+ });
105
+ }
106
+
107
+ /** Boot the App + HTTP service and run until interrupted. */
108
+ async function runServe(
109
+ root: string,
110
+ port: number,
111
+ verbose: boolean,
112
+ opts: { stateDir?: string; readOnlyConfig?: boolean; host?: string } = {},
113
+ ): Promise<number> {
114
+ // A single run's abort (budget/turn cap, kill switch, or the Agent SDK's own
115
+ // detached internals) must never take the whole long-running service down. The
116
+ // SDK can surface an AbortError asynchronously after we've already handled the
117
+ // run; swallow those and log anything else, but keep serving — crashing would
118
+ // lose every in-flight run and the operator's queue.
119
+ const guard = (kind: string) => (err: unknown) => {
120
+ const e = err as { name?: string; message?: string } | undefined;
121
+ const msg = e?.message ?? String(err);
122
+ if (e?.name === "AbortError" || /abort/i.test(msg)) {
123
+ if (verbose) process.stderr.write(`· ${kind} ignored (a run was aborted): ${msg}\n`);
124
+ return;
125
+ }
126
+ process.stderr.write(`· ${kind} (service stays up): ${msg}\n`);
127
+ };
128
+ process.on("unhandledRejection", guard("unhandledRejection"));
129
+ process.on("uncaughtException", guard("uncaughtException"));
130
+
131
+ const runtimeDir = opts.stateDir ? path.resolve(opts.stateDir) : path.join(root, ".ravel");
132
+ // EmittingAudit feeds the SSE stream; with -v, App also wraps it in a
133
+ // LoggingAudit that tees each event to stderr (append flows through both).
134
+ const events = new EmittingAudit(new JsonlAudit(path.join(runtimeDir, "audit.jsonl")));
135
+ const app = new App({
136
+ root,
137
+ engine: new SdkEngine(),
138
+ audit: events,
139
+ runtimeDir,
140
+ ...(verbose ? { verbose: (line: string) => process.stderr.write(`${line}\n`) } : {}),
141
+ });
142
+ await app.start();
143
+
144
+ // Serve the built operator console (ui/dist, shipped with the package) from
145
+ // the same port. Absent a build, the API still serves — console devs use the
146
+ // Vite dev server (`cd ui && npm run dev`) for HMR instead.
147
+ const uiDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "ui", "dist");
148
+ const hasUi = await fs.access(path.join(uiDir, "index.html")).then(() => true).catch(() => false);
149
+ const server = createServer({
150
+ app,
151
+ events,
152
+ ...(hasUi ? { uiDir } : {}),
153
+ ...(opts.readOnlyConfig ? { readOnlyConfig: true } : {}),
154
+ });
155
+ // Loopback by default: the API is deliberately auth-free (single local
156
+ // operator), so exposing it on other interfaces is an explicit opt-in.
157
+ const host = opts.host ?? "127.0.0.1";
158
+ // A bind failure (e.g. EADDRINUSE) is a fatal boot error: reject so the
159
+ // process exits non-zero instead of being swallowed by the run-error guard
160
+ // and hanging — the worker contract promises a clear failure, not a timeout.
161
+ await new Promise<void>((resolve, reject) => {
162
+ server.once("error", reject);
163
+ server.listen(port, host, () => {
164
+ server.off("error", reject);
165
+ resolve();
166
+ });
167
+ });
168
+ stdout.write(`Ravel service on http://${host}:${port} (org: ${root})\n`);
169
+ if (hasUi) stdout.write(`Operator console: http://localhost:${port}/\n`);
170
+ else stdout.write(`No built console found — \`cd ui && npm run build\` to serve it from this port.\n`);
171
+ stdout.write(`Proposals queue is async — review at /api/proposals or in the console. Ctrl-C to stop.\n`);
172
+
173
+ await shutdownSignal();
174
+ await new Promise<void>((resolve) => server.close(() => resolve()));
175
+ await app.stop();
176
+ return 0;
177
+ }
178
+
179
+ async function main(): Promise<number> {
180
+ const { values, positionals } = parseArgs({
181
+ allowPositionals: true,
182
+ options: {
183
+ dir: { type: "string" },
184
+ "dry-run": { type: "boolean" },
185
+ input: { type: "string", multiple: true },
186
+ file: { type: "string", multiple: true },
187
+ verbose: { type: "boolean", short: "v" },
188
+ sync: { type: "boolean" },
189
+ port: { type: "string" },
190
+ host: { type: "string" },
191
+ "state-dir": { type: "string" },
192
+ "read-only-config": { type: "boolean" },
193
+ help: { type: "boolean", short: "h" },
194
+ },
195
+ });
196
+
197
+ const command = positionals[0];
198
+ const root = values.dir ?? process.cwd();
199
+
200
+ if (values.help || !command) {
201
+ stdout.write(HELP);
202
+ return 0;
203
+ }
204
+
205
+ if (command === "validate") {
206
+ const result = await compileRegistry(root, 1);
207
+ if (result.ok && result.snapshot) {
208
+ stdout.write(`✓ valid — ${result.snapshot.nodes.size} agent(s), ${result.snapshot.processes.length} process(es)\n`);
209
+ for (const id of result.snapshot.nodes.keys()) stdout.write(` • ${id || "(root)"}\n`);
210
+ return 0;
211
+ }
212
+ stdout.write("✗ invalid:\n");
213
+ for (const d of result.diagnostics) stdout.write(` • ${d.where}: ${d.message}\n`);
214
+ return 1;
215
+ }
216
+
217
+ if (command === "create") {
218
+ const name = positionals[1];
219
+ if (!name) {
220
+ stdout.write("error: `create` requires a team name (e.g. `ravel create my-team`)\n");
221
+ return 1;
222
+ }
223
+ const dir = path.resolve(name);
224
+ if (await fs.access(dir).then(() => true).catch(() => false)) {
225
+ stdout.write(`error: "${name}" already exists\n`);
226
+ return 1;
227
+ }
228
+ await scaffoldTeam(dir, path.basename(dir));
229
+ stdout.write(`✓ created team "${name}"\n\n cd ${name} && ravel serve --dir .\n`);
230
+ return 0;
231
+ }
232
+
233
+ // Load .env so ANTHROPIC_API_KEY (and friends) reach the SDK.
234
+ await loadEnvFiles(root);
235
+ if (process.env["ANTHROPIC_API_KEY"] === undefined && process.env["ANTHROPIC_AUTH_TOKEN"] === undefined) {
236
+ stdout.write(
237
+ "warning: no ANTHROPIC_API_KEY found (checked env and .env in the current dir and --dir). " +
238
+ "Set it in .env or export it, or run `ant auth login`.\n",
239
+ );
240
+ }
241
+
242
+ if (command === "serve") {
243
+ return runServe(root, values.port ? Number(values.port) : 4317, Boolean(values.verbose), {
244
+ ...(values.host ? { host: values.host } : {}),
245
+ ...(values["state-dir"] ? { stateDir: values["state-dir"] } : {}),
246
+ ...(values["read-only-config"] ? { readOnlyConfig: true } : {}),
247
+ });
248
+ }
249
+
250
+ const app = new App({
251
+ root,
252
+ engine: new SdkEngine(),
253
+ ...(values["state-dir"] ? { runtimeDir: path.resolve(values["state-dir"]) } : {}),
254
+ ...(values["dry-run"] ? { dryRun: true } : {}),
255
+ ...(values.sync ? { approvals: "sync" as const } : {}),
256
+ ...(values.verbose ? { verbose: (line: string) => process.stderr.write(`${line}\n`) } : {}),
257
+ });
258
+ await app.start();
259
+
260
+ try {
261
+ switch (command) {
262
+ case "run": {
263
+ const processName = positionals[1];
264
+ if (!processName) {
265
+ stdout.write("error: `run` requires a process name\n");
266
+ return 1;
267
+ }
268
+ const inputs: Record<string, string> = {};
269
+ for (const pair of values.input ?? []) {
270
+ const eq = pair.indexOf("=");
271
+ if (eq === -1) {
272
+ stdout.write(`error: --input must be key=value (got "${pair}")\n`);
273
+ return 1;
274
+ }
275
+ inputs[pair.slice(0, eq)] = pair.slice(eq + 1);
276
+ }
277
+ const files = (values.file ?? []).map((f) => path.resolve(f));
278
+ const detach = values.sync ? attachInteractiveApprovals(app) : () => {};
279
+ try {
280
+ const result = await app.runProcess(processName, {
281
+ ...(Object.keys(inputs).length ? { inputs } : {}),
282
+ ...(files.length ? { files } : {}),
283
+ });
284
+ stdout.write(`\n[${result.status}] ${result.processName} (${result.turns} turn(s), ${fmtUsage(result.usage)})\n`);
285
+ stdout.write(`${result.summary}\n`);
286
+ if (result.workspaceDir) {
287
+ stdout.write(`\nDeliverables (shared workspace): ${result.workspaceDir}\n`);
288
+ try {
289
+ const entries = await fs.readdir(result.workspaceDir);
290
+ for (const e of entries.sort()) stdout.write(` • ${e}\n`);
291
+ } catch {
292
+ /* dir may not exist if nothing was written */
293
+ }
294
+ }
295
+ const pending = app.pendingProposals();
296
+ if (pending.length) {
297
+ stdout.write(`\n${pending.length} action(s) awaiting approval (\`ravel proposals\` to review):\n`);
298
+ for (const p of pending) stdout.write(` • ${p.id} ${p.action} ${JSON.stringify(p.input)}\n`);
299
+ }
300
+ } finally {
301
+ detach();
302
+ }
303
+ return 0;
304
+ }
305
+ case "proposals": {
306
+ const sub = positionals[1] ?? "list";
307
+ if (sub === "list") {
308
+ const pending = app.pendingProposals();
309
+ if (!pending.length) stdout.write("No pending proposals.\n");
310
+ for (const p of pending) {
311
+ stdout.write(`• ${p.id} [${p.nodeId || "(root)"}] ${p.action} ${JSON.stringify(p.input)}\n`);
312
+ }
313
+ return 0;
314
+ }
315
+ if (sub === "approve" || sub === "reject") {
316
+ const id = positionals[2];
317
+ if (!id) {
318
+ stdout.write(`error: \`proposals ${sub}\` requires a proposal id\n`);
319
+ return 1;
320
+ }
321
+ const updated = await app.resolveProposal(id, sub === "approve" ? "approve" : "reject");
322
+ if (!updated) {
323
+ stdout.write(`No pending proposal with id "${id}".\n`);
324
+ return 1;
325
+ }
326
+ stdout.write(`${updated.action} → ${updated.status}${updated.error ? ` (${updated.error})` : ""}\n`);
327
+ return 0;
328
+ }
329
+ stdout.write(`unknown proposals subcommand "${sub}" (use list|approve <id>|reject <id>)\n`);
330
+ return 1;
331
+ }
332
+ case "chat": {
333
+ const nodeId = positionals[1] ?? "";
334
+ const message = positionals.slice(2).join(" ");
335
+ if (!message) {
336
+ stdout.write("error: `chat` requires a message\n");
337
+ return 1;
338
+ }
339
+ const detach = values.sync ? attachInteractiveApprovals(app) : () => {};
340
+ try {
341
+ stdout.write(`${await app.chat(nodeId, message)}\n`);
342
+ } finally {
343
+ detach();
344
+ }
345
+ return 0;
346
+ }
347
+ case "dashboard": {
348
+ const d = app.dashboard();
349
+ stdout.write(`Total spend: ${fmtUsage(d.totalUsage)} · ${d.eventCount} events · ${d.pendingProposals} pending proposal(s)\n`);
350
+ for (const a of d.agents) {
351
+ stdout.write(` • ${a.nodeId || "(root)"} [${a.role}] ${a.state} — ${a.tasksRun} task(s), ${fmtUsage(a.usage)}\n`);
352
+ }
353
+ return 0;
354
+ }
355
+ case "watch": {
356
+ attachInteractiveApprovals(app);
357
+ stdout.write(`Watching ${root}. Edit the folder to reshape the org. Ctrl-C to stop.\n`);
358
+ await shutdownSignal();
359
+ return 0;
360
+ }
361
+ default:
362
+ stdout.write(`unknown command "${command}"\n\n${HELP}`);
363
+ return 1;
364
+ }
365
+ } finally {
366
+ await app.stop();
367
+ }
368
+ }
369
+
370
+ /** Scaffold a minimal, valid starter team folder (a lead + one assistant + a process). */
371
+ async function scaffoldTeam(dir: string, name: string): Promise<void> {
372
+ const write = async (rel: string, content: string) => {
373
+ const abs = path.join(dir, rel);
374
+ await fs.mkdir(path.dirname(abs), { recursive: true });
375
+ await fs.writeFile(abs, content, "utf8");
376
+ };
377
+ await write(
378
+ "ravel.json",
379
+ JSON.stringify({ name, runtimeVersion: "^0.1", description: `The ${name} team.` }, null, 2) + "\n",
380
+ );
381
+ await write(
382
+ "agent.md",
383
+ `---\nname: ${name} Lead\nrole: lead\nmodel: sonnet\nautonomy: orchestrated\nbudget:\n usd: 2\n turns: 6\n---\n` +
384
+ `You lead the **${name}** team. You own the **Hello** process and delegate to your\n` +
385
+ `report, the **assistant**. Dispatch the assistant to do the work, then mark the\n` +
386
+ `process done with a one-line summary.\n`,
387
+ );
388
+ await write(
389
+ "assistant/agent.md",
390
+ `---\nname: Assistant\nrole: assistant\nmodel: sonnet\nautonomy: orchestrated\n---\n` +
391
+ `You carry out the task you're given and report back in one line. Use \`mem_text_set\`\n` +
392
+ `to record anything the team should remember.\n`,
393
+ );
394
+ await write(
395
+ "assistant/tools.json",
396
+ JSON.stringify(
397
+ {
398
+ defaultPolicy: "ask",
399
+ builtins: "none",
400
+ tools: [
401
+ { name: "mem_text_get", policy: "auto", description: "Read a team memory value." },
402
+ { name: "mem_text_set", policy: "auto", description: "Write a team memory value." },
403
+ ],
404
+ mcpServers: {},
405
+ },
406
+ null,
407
+ 2,
408
+ ) + "\n",
409
+ );
410
+ await write(
411
+ "processes/hello.process.md",
412
+ `---\nname: Hello\nowner: lead\nparticipants: [assistant]\ntrigger:\n type: manual\ndefinitionOfDone: >\n` +
413
+ ` The assistant recorded a greeting to team memory and reported done.\nbudget:\n usd: 1\n turns: 4\n---\n` +
414
+ `Dispatch the **assistant** to write a friendly greeting (from the run input \`who\`,\n` +
415
+ `defaulting to "world") to team memory via \`mem_text_set\`, then mark the process done.\n`,
416
+ );
417
+ await write(
418
+ ".gitignore",
419
+ `node_modules/\n.env\n.env.local\n.DS_Store\n\n# Runtime state (memory, audit, queues) — created at runtime\n.ravel/\n`,
420
+ );
421
+ }
422
+
423
+ main()
424
+ .then((code) => process.exit(code))
425
+ .catch((err: unknown) => {
426
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
427
+ process.exit(1);
428
+ });