@cello-protocol/cli 0.0.44 → 0.0.45

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.
@@ -0,0 +1,641 @@
1
+ /**
2
+ * DOD-CLI-PARITY-1 §4 — the command REGISTRY: the single source of truth for the `cello` CLI.
3
+ *
4
+ * Each entry carries { name, summary, help, flags, run }. Everything derives from this one table:
5
+ * - dispatch (src/bin/cello.ts) — no switch to keep in sync,
6
+ * - the `cello --help` described `Commands:` table — rendered from each entry's `summary`
7
+ * (this is DOD-ONBOARD-HELP-1's remaining gap; the old surface was a pipe-delimited blob),
8
+ * - per-command `cello <cmd> --help` — the pre-existing help text, moved here VERBATIM,
9
+ * - the recognized-flag set used to reject unknown flags before dispatch.
10
+ *
11
+ * Consequence, and the reason for the refactor: the help table, per-command help, and dispatch
12
+ * CANNOT DRIFT, and adding a command FORCES adding its one-line summary.
13
+ */
14
+ import { MONIKER_RE } from "@cello-protocol/protocol-types";
15
+ import { login, logout, status, register, createAgent, removeAgent, refreshShares, relayReceipts, sessions, settingsGet, settingsSet, monikerSet, telegramSetToken, } from "./commands.js";
16
+ import { splitAgentFlag } from "./arg-parse.js";
17
+ import { IPC_METHODS, contactAdd, contactRemove, contactList, contactSetTier, contactSetAway, listAgents, startAgent, stopAgent, useAgent, inbox, transcript, contactSetMoniker, sealedReceipt, initiate, send, receive, receiveSession, closeSession, awaitSession, } from "./parity-commands.js";
18
+ /** Read the whole of stdin — `cello send <id> --stdin` for message text with newlines/quotes. */
19
+ async function readStdin() {
20
+ const chunks = [];
21
+ for await (const chunk of process.stdin)
22
+ chunks.push(Buffer.from(chunk));
23
+ return Buffer.concat(chunks).toString("utf8");
24
+ }
25
+ /** Parse the parity commands' shared flags out of argv (`--agent`, `--pretty`, and value flags). */
26
+ function parityOpts(args) {
27
+ const { agent, positional } = splitAgentFlag(args);
28
+ const pretty = positional.includes("--pretty");
29
+ return { agent, pretty, positional: positional.filter((a) => a !== "--pretty") };
30
+ }
31
+ /** Read `--flag <value>` out of a positional list, returning the value and the remaining args. */
32
+ function takeValueFlag(args, flag) {
33
+ const i = args.indexOf(flag);
34
+ if (i === -1)
35
+ return { rest: args };
36
+ return { value: args[i + 1], rest: args.filter((_, j) => j !== i && j !== i + 1) };
37
+ }
38
+ /**
39
+ * Review F5 — a numeric flag given a NON-NUMERIC value must fail loud, never be silently dropped.
40
+ *
41
+ * `--since-seq abc` used to parse to undefined, which `defined()` then removed, which turned a
42
+ * stateless CATCH-UP into a 30-second BLOCKING live wait that returns `content: null` — so a script
43
+ * asking "what did I miss?" was answered "nothing new" to a question it never asked. Silently
44
+ * changing the meaning of a command is worse than refusing it.
45
+ *
46
+ * Throws a BadFlagValue, which run() converts to a structured error + exit 1.
47
+ */
48
+ class BadFlagValue extends Error {
49
+ flag;
50
+ value;
51
+ constructor(flag, value) {
52
+ super(`${flag} expects a number, got '${value}'`);
53
+ this.flag = flag;
54
+ this.value = value;
55
+ }
56
+ }
57
+ function numberOrUndefined(raw, flag) {
58
+ if (raw === undefined)
59
+ return undefined;
60
+ const n = Number(raw);
61
+ if (!Number.isFinite(n))
62
+ throw new BadFlagValue(flag, raw);
63
+ return n;
64
+ }
65
+ /** Turn a BadFlagValue into the §3 structured error; rethrow anything else. */
66
+ function flagError(err) {
67
+ if (err instanceof BadFlagValue) {
68
+ return {
69
+ stdout: "",
70
+ stderr: JSON.stringify({
71
+ ok: false,
72
+ reason: "invalid_flag_value",
73
+ flag: err.flag,
74
+ value: err.value,
75
+ guidance: `${err.flag} expects a number. Got '${err.value}'. The command was NOT run — a dropped flag would have silently changed what it does.`,
76
+ }),
77
+ exitCode: 1,
78
+ };
79
+ }
80
+ throw err;
81
+ }
82
+ /** Adapt a legacy CommandResult (single `output` string, always stdout) to the CliOutput triple. */
83
+ function legacy(result) {
84
+ return { stdout: result.output, stderr: "", exitCode: result.exitCode };
85
+ }
86
+ /**
87
+ * `--agent <name>` — recognized by every agent-scoped command.
88
+ *
89
+ * `consumesValue: false` is deliberate and is what checkArgs PARITY requires: the pre-existing
90
+ * checkArgs never skipped --agent's value (only `--limit` did), and flipping this to true would
91
+ * change `cello contact list --agent --bogus` from a fail-loud unknown_flag into a silently
92
+ * accepted agent literally named "--bogus". The value is claimed by splitAgentFlag (arg-parse.ts),
93
+ * which owns --agent parsing; checkArgs only needs to know the FLAG is legal. Same for install's
94
+ * --agent / --hermes-home below.
95
+ */
96
+ const AGENT_FLAG = [{ name: "--agent", consumesValue: false }];
97
+ /** Agent-scoped parity commands also take --pretty (granted automatically via `jsonOut`). */
98
+ const AGENT_AND_TIMEOUT = [
99
+ { name: "--agent", consumesValue: false },
100
+ { name: "--timeout-ms", consumesValue: true },
101
+ ];
102
+ export const COMMANDS = [
103
+ {
104
+ name: "login",
105
+ summary: "Start the local daemon (or connect to a running one) and bring your agents online.",
106
+ help: "Usage: cello login — start the daemon (or connect to an existing one).",
107
+ async run(ctx) {
108
+ return legacy(await login(ctx.celloDir, ctx.daemonBin, ctx.logger));
109
+ },
110
+ },
111
+ {
112
+ name: "logout",
113
+ summary: "Stop the running daemon (waits until it is actually gone).",
114
+ help: "Usage: cello logout — send shutdown to the running daemon.",
115
+ async run(ctx) {
116
+ // DOD-LOGOUT-WAIT-1: logout WAITS for the daemon to actually die before claiming
117
+ // "Daemon stopped." — the immediate progress line tells the operator the command
118
+ // activated and the short pause is expected.
119
+ return legacy(await logout(ctx.celloDir, ctx.onProgress));
120
+ },
121
+ },
122
+ {
123
+ name: "status",
124
+ summary: "Show daemon + agent state as structured JSON.",
125
+ help: "Usage: cello status — query the daemon and print the structured status JSON.",
126
+ async run(ctx) {
127
+ return legacy(await status(ctx.celloDir));
128
+ },
129
+ },
130
+ {
131
+ name: "register",
132
+ summary: "Register a local agent with the directory using a pre-auth token.",
133
+ help: "Usage: cello register <agent> <pre-auth-token> — register a LOCAL agent with the directory.\n" +
134
+ " The two-step onboarding: (1) 'cello create-agent <name>' makes the local identity; (2) 'cello register <name> <token>' registers it with the directory.\n" +
135
+ " The token is a single-use pre-authorization ticket from the CELLO Operations Agent on Telegram, format 'CELLO-' + 33 characters, valid 24h.\n" +
136
+ " Example: cello register alice CELLO-3xY7...\n" +
137
+ " Env-var form (avoids retyping): CELLO_PREAUTH_TOKEN=CELLO-3xY7... cello register alice\n" +
138
+ " Quoting is only needed if a value contains spaces (agent names and tokens never do).",
139
+ async run(ctx, args) {
140
+ // cello register <agent> [preAuthToken] (token falls back to CELLO_PREAUTH_TOKEN so it need
141
+ // not appear in shell history). Optional phone stub follows.
142
+ const agent = args[0] ?? "";
143
+ const preAuthToken = args[1] ?? process.env.CELLO_PREAUTH_TOKEN ?? "";
144
+ const phoneStub = args[2] ?? "";
145
+ return legacy(await register(ctx.celloDir, agent, preAuthToken, phoneStub));
146
+ },
147
+ },
148
+ {
149
+ name: "create-agent",
150
+ summary: "Create a new local agent identity (does not touch the directory).",
151
+ help: "Usage: cello create-agent <name> — create a new LOCAL agent identity (does not touch the directory).\n" +
152
+ // MONIKER-0 AC2: the regex text is DERIVED from the shared constant, never hand-typed.
153
+ ` Name rule: 1–64 characters, letters/digits/'-'/'_' only, no spaces (regex ${MONIKER_RE.source}).\n` +
154
+ " Next step: 'cello register <name> <pre-auth-token>' to register it with the directory.",
155
+ async run(ctx, args) {
156
+ return legacy(await createAgent(ctx.celloDir, args[0] ?? ""));
157
+ },
158
+ },
159
+ {
160
+ name: "remove-agent",
161
+ summary: "Retire a local agent (one-way) and free its name.",
162
+ help: "Usage: cello remove-agent <name> — retires a local agent (one-way) and frees its name.",
163
+ async run(ctx, args) {
164
+ return legacy(await removeAgent(ctx.celloDir, args[0] ?? ""));
165
+ },
166
+ },
167
+ {
168
+ name: "refresh",
169
+ summary: "Refresh an agent's threshold shares (new epoch).",
170
+ help: "Usage: cello refresh <name> — proactively refresh the agent's threshold shares (new epoch).",
171
+ async run(ctx, args) {
172
+ return legacy(await refreshShares(ctx.celloDir, args[0] ?? ""));
173
+ },
174
+ },
175
+ {
176
+ name: "receipts",
177
+ summary: "List an agent's stored relay ordering receipts.",
178
+ help: "Usage: cello receipts <name> — list the agent's stored relay ordering receipts.",
179
+ async run(ctx, args) {
180
+ return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
181
+ },
182
+ },
183
+ {
184
+ name: "sessions",
185
+ summary: "List session history (open by default; --all for everything).",
186
+ help: "Usage: cello sessions [--open|--closed|--failed|--all] [--limit N] — list session history (defaults to open).",
187
+ flags: [
188
+ { name: "--open" },
189
+ { name: "--closed" },
190
+ { name: "--failed" },
191
+ { name: "--all" },
192
+ { name: "--limit", consumesValue: true },
193
+ ],
194
+ async run(ctx, args) {
195
+ let filter;
196
+ if (args.includes("--all"))
197
+ filter = "all";
198
+ else if (args.includes("--closed"))
199
+ filter = "closed";
200
+ else if (args.includes("--failed"))
201
+ filter = "failed";
202
+ else if (args.includes("--open"))
203
+ filter = "open";
204
+ const limitIdx = args.indexOf("--limit");
205
+ let limit;
206
+ if (limitIdx !== -1 && args[limitIdx + 1] !== undefined) {
207
+ const n = Number(args[limitIdx + 1]);
208
+ if (Number.isFinite(n) && n > 0)
209
+ limit = Math.floor(n);
210
+ }
211
+ return legacy(await sessions(ctx.celloDir, { filter, limit }));
212
+ },
213
+ },
214
+ {
215
+ name: "contact",
216
+ summary: "Manage the per-agent address book (add, remove, list, set-tier, set-away, set-moniker).",
217
+ help: "Usage: cello contact add <pubkey> [--agent <name>] | cello contact remove <pubkey> [--agent <name>] | cello contact list [--agent <name>]\n" +
218
+ " cello contact set-tier <pubkey> <0..4> [--agent <name>] — trust tier (unknown|known|whitelisted|vip)\n" +
219
+ " cello contact set-away <pubkey> <message…> [--agent <name>] — per-contact away text (empty clears it)\n" +
220
+ " cello contact set-moniker <pubkey> <moniker> [--agent <name>] — YOUR pet name for THEM (empty clears it)\n" +
221
+ " Per-agent contact whitelist (M8C-CONTACT-1). --agent defaults to the current/sole-online agent.\n" +
222
+ " Contacts are added automatically too: initiating a session to X, or accepting X's inbound request, adds X.\n" +
223
+ " Note: 'set-moniker' is the name YOU give a CONTACT. The top-level 'cello moniker' is your OWN outbound name.\n" +
224
+ " ('tier' and 'away' remain accepted as aliases of set-tier / set-away.)\n" +
225
+ " Example: cello contact list --agent alice",
226
+ flags: AGENT_FLAG,
227
+ jsonOut: true, // review F3: the WHOLE address book honors §3 — one command, one contract
228
+ async run(ctx, args) {
229
+ const { agent, pretty, positional } = parityOpts(args);
230
+ const o = { agent, pretty };
231
+ const [sub, pubkey, valueArg] = positional;
232
+ if (sub === "add" && pubkey)
233
+ return contactAdd(ctx.celloDir, pubkey, o);
234
+ if (sub === "remove" && pubkey)
235
+ return contactRemove(ctx.celloDir, pubkey, o);
236
+ if (sub === "list")
237
+ return contactList(ctx.celloDir, o);
238
+ // set-tier / set-away are the DOD-CLI-PARITY-1 names; tier / away are the pre-existing verbs,
239
+ // kept as aliases so no existing script or muscle-memory breaks.
240
+ if ((sub === "set-tier" || sub === "tier") && pubkey && valueArg !== undefined) {
241
+ // Daemon validates the value; a non-numeric arg surfaces as its invalid_tier verdict.
242
+ return contactSetTier(ctx.celloDir, pubkey, Number(valueArg), o);
243
+ }
244
+ if ((sub === "set-away" || sub === "away") && pubkey) {
245
+ // The rest of the args form the away text; empty → clear.
246
+ const message = positional.slice(2).join(" ");
247
+ return contactSetAway(ctx.celloDir, pubkey, message.length > 0 ? message : null, o);
248
+ }
249
+ if (sub === "set-moniker" && pubkey) {
250
+ // DOD-CLI-PARITY-1: the per-CONTACT pet name (cello_contact_set_moniker) — was MCP-only.
251
+ // Empty → null clears it, mirroring the tool.
252
+ const moniker = positional.slice(2).join(" ");
253
+ return contactSetMoniker(ctx.celloDir, pubkey, moniker.length > 0 ? moniker : null, o);
254
+ }
255
+ return {
256
+ stdout: helpForSpec("contact"),
257
+ stderr: "",
258
+ exitCode: 1,
259
+ };
260
+ },
261
+ },
262
+ {
263
+ name: "settings",
264
+ summary: "Get or set an agent's reachability policy (session/byte bounds, away text).",
265
+ help: "Usage: cello settings get [key] [--agent <name>] | cello settings set <key> <value> [--agent <name>]\n" +
266
+ " Per-agent reachability policy (DOD-SETTINGS-1). Keys: bounds.<tier>.max_sessions, bounds.<tier>.max_bytes\n" +
267
+ " (tier = unknown|known|whitelisted|vip; a finite positive integer), away.default, away.tier.<tier> (away text).\n" +
268
+ " An unset key uses the built-in default. Example: cello settings set bounds.known.max_sessions 8 --agent alice",
269
+ flags: AGENT_FLAG,
270
+ async run(ctx, args) {
271
+ const { agent, positional } = splitAgentFlag(args);
272
+ const [sub, key, value] = positional;
273
+ if (sub === "get")
274
+ return legacy(await settingsGet(ctx.celloDir, key, agent)); // key optional → all
275
+ if (sub === "set" && key && value !== undefined) {
276
+ return legacy(await settingsSet(ctx.celloDir, key, value, agent));
277
+ }
278
+ return {
279
+ stdout: "Usage: cello settings get [key] [--agent <name>] | cello settings set <key> <value> [--agent <name>]",
280
+ stderr: "",
281
+ exitCode: 1,
282
+ };
283
+ },
284
+ },
285
+ {
286
+ name: "moniker",
287
+ summary: "Set or clear the agent's OWN outbound display name (what a counterparty sees).",
288
+ help: "Usage: cello moniker set <name> [--agent <agent>] | cello moniker clear [--agent <agent>]\n" +
289
+ " The agent's OUTBOUND name — what a counterparty's doorbell shows (MONIKER-1). Defaults to the agent name; 'set' stores an override, 'clear' restores the default.\n" +
290
+ // MONIKER-0 AC2: the regex text is DERIVED from the shared constant, never hand-typed.
291
+ ` Name rule: 1–64 characters, letters/digits/'-'/'_' only, no spaces (regex ${MONIKER_RE.source}).\n` +
292
+ " Local-only: never sent to the directory; the receiver treats it as an unverified hint (like caller ID).\n" +
293
+ " Example: cello moniker set Wonderland_Alice --agent alice",
294
+ flags: AGENT_FLAG,
295
+ async run(ctx, args) {
296
+ const { agent, positional } = splitAgentFlag(args);
297
+ const [sub, name] = positional;
298
+ if (sub === "set" && name)
299
+ return legacy(await monikerSet(ctx.celloDir, name, agent));
300
+ if (sub === "clear" && !name)
301
+ return legacy(await monikerSet(ctx.celloDir, null, agent));
302
+ return { stdout: helpForSpec("moniker"), stderr: "", exitCode: 1 };
303
+ },
304
+ },
305
+ {
306
+ name: "telegram",
307
+ summary: "Configure the daemon-owned Telegram doorbell.",
308
+ help: "Usage: cello telegram set-token <bot_token> <allowlisted_chat_id> — configure the daemon-owned Telegram doorbell (M8C-TGDOOR-1).\n" +
309
+ " Starts a single long-lived poller immediately; the operator chat given is the ONLY one that ever receives doorbell events.",
310
+ async run(ctx, args) {
311
+ const [sub, botToken, chatId] = args;
312
+ if (sub === "set-token" && botToken && chatId) {
313
+ return legacy(await telegramSetToken(ctx.celloDir, botToken, chatId));
314
+ }
315
+ return { stdout: "Usage: cello telegram set-token <bot_token> <allowlisted_chat_id>", stderr: "", exitCode: 1 };
316
+ },
317
+ },
318
+ {
319
+ name: "install",
320
+ summary: "Wire the local CELLO daemon into a Hermes Agent installation.",
321
+ help: "Usage: cello install hermes --agent <name> [--hermes-home <path>] — wire the local CELLO daemon into a Hermes Agent installation.\n" +
322
+ " Scaffolds the CELLO platform-adapter plugin into the Hermes home (default ~/.hermes), binds CELLO_AGENT_NAME in its .env,\n" +
323
+ " and registers via 'hermes plugins enable cello' + 'hermes mcp add cello'. Idempotent — re-run to upgrade.\n" +
324
+ " After installing, restart the gateway: hermes gateway restart",
325
+ flags: [{ name: "--agent" }, { name: "--hermes-home" }],
326
+ async run(_ctx, args) {
327
+ const agentIdx = args.indexOf("--agent");
328
+ const homeIdx = args.indexOf("--hermes-home");
329
+ // Find the target positional, excluding both flags AND their values — so
330
+ // `cello install --agent alice hermes` still resolves target=hermes.
331
+ const target = args.find((a, i) => !a.startsWith("-") &&
332
+ !(agentIdx !== -1 && i === agentIdx + 1) &&
333
+ !(homeIdx !== -1 && i === homeIdx + 1));
334
+ if (target !== "hermes") {
335
+ return { stdout: helpForSpec("install"), stderr: "", exitCode: 1 };
336
+ }
337
+ const { installHermes } = await import("./hermes/install-hermes.js");
338
+ return legacy(await installHermes({
339
+ agentName: agentIdx !== -1 ? (args[agentIdx + 1] ?? "") : "",
340
+ hermesHome: homeIdx !== -1 ? args[homeIdx + 1] : undefined,
341
+ }));
342
+ },
343
+ },
344
+ // ═══ DOD-CLI-PARITY-1 — the MCP-only capabilities, now reachable from bash ══════════════════
345
+ // Each honors the §3 contract (jsonOut) and calls the SAME daemon handler as its cello_* MCP
346
+ // tool (ipcMethod). Group A = operator control + address book; Group B = live conversation.
347
+ {
348
+ name: "agents",
349
+ summary: "List every loaded agent and whether it is online.",
350
+ help: "Usage: cello agents [--pretty] — list all loaded agents (name, state).\n" +
351
+ " The CLI twin of the cello_list_agents MCP tool. Prints JSON; use --pretty for humans.",
352
+ ipcMethod: IPC_METHODS.agents,
353
+ jsonOut: true,
354
+ async run(ctx, args) {
355
+ const { pretty } = parityOpts(args);
356
+ return listAgents(ctx.celloDir, { pretty });
357
+ },
358
+ },
359
+ {
360
+ name: "start-agent",
361
+ summary: "Bring an agent online (without selecting it as current).",
362
+ help: "Usage: cello start-agent <name> [--pretty] — bring a registered agent ONLINE.\n" +
363
+ " Does NOT select it as the current agent — use 'cello use-agent <name>' for that.\n" +
364
+ " Idempotent: starting an already-online agent is safe.",
365
+ ipcMethod: IPC_METHODS["start-agent"],
366
+ jsonOut: true,
367
+ async run(ctx, args) {
368
+ const { pretty, positional } = parityOpts(args);
369
+ return startAgent(ctx.celloDir, positional[0] ?? "", { pretty });
370
+ },
371
+ },
372
+ {
373
+ name: "stop-agent",
374
+ summary: "Take an agent offline.",
375
+ help: "Usage: cello stop-agent <name> [--pretty] — take an agent offline.",
376
+ ipcMethod: IPC_METHODS["stop-agent"],
377
+ jsonOut: true,
378
+ async run(ctx, args) {
379
+ const { pretty, positional } = parityOpts(args);
380
+ return stopAgent(ctx.celloDir, positional[0] ?? "", { pretty });
381
+ },
382
+ },
383
+ {
384
+ name: "use-agent",
385
+ summary: "Select the agent that later commands act as (auto-starts it; persists).",
386
+ help: "Usage: cello use-agent <name> [--pretty] — select the CURRENT agent for later commands.\n" +
387
+ " Auto-starts the agent if it is offline (AUTOSTART-1).\n" +
388
+ " The selection PERSISTS across invocations (recorded in <cello-dir>/current-agent), because\n" +
389
+ " each CLI command opens its own daemon connection — a selection that lived only on the socket\n" +
390
+ " would vanish the moment the command exited. Override per-command with '--agent <name>'.\n" +
391
+ " A selection the daemon rejects is not recorded.",
392
+ ipcMethod: IPC_METHODS["use-agent"],
393
+ jsonOut: true,
394
+ async run(ctx, args) {
395
+ const { pretty, positional } = parityOpts(args);
396
+ return useAgent(ctx.celloDir, positional[0] ?? "", { pretty });
397
+ },
398
+ },
399
+ {
400
+ name: "inbox",
401
+ summary: "Check pending session requests and unread counts (the push-loss reconciler).",
402
+ help: "Usage: cello inbox [--scope current|all] [--agent <name>] [--pretty] — poll for what you missed.\n" +
403
+ " Content-free: pending session requests + unread message counts. Non-destructive (it does not\n" +
404
+ " drain anything — 'cello await-session' owns that). --scope all covers every loaded agent.",
405
+ flags: [
406
+ { name: "--agent", consumesValue: false },
407
+ { name: "--scope", consumesValue: true },
408
+ ],
409
+ ipcMethod: IPC_METHODS.inbox,
410
+ jsonOut: true,
411
+ async run(ctx, args) {
412
+ const { agent, pretty, positional } = parityOpts(args);
413
+ const { value } = takeValueFlag(positional, "--scope");
414
+ // Review F6: an UNRECOGNIZED scope must not silently become the default. A typo'd
415
+ // `--scope all` (e.g. "al") would have answered with `current`'s data and exit 0 — the
416
+ // operator reads "no notifications" while another agent's inbox is full.
417
+ if (value !== undefined && value !== "all" && value !== "current") {
418
+ return {
419
+ stdout: "",
420
+ stderr: JSON.stringify({
421
+ ok: false,
422
+ reason: "invalid_flag_value",
423
+ flag: "--scope",
424
+ value,
425
+ guidance: "--scope must be 'current' or 'all'. The command was NOT run — answering a different question than the one asked is worse than refusing.",
426
+ }),
427
+ exitCode: 1,
428
+ };
429
+ }
430
+ return inbox(ctx.celloDir, { agent, pretty, scope: value });
431
+ },
432
+ },
433
+ {
434
+ name: "transcript",
435
+ summary: "Print a session's durable conversation transcript (sent + received).",
436
+ help: "Usage: cello transcript <session-id> [--agent <name>] [--pretty] — the durable transcript.\n" +
437
+ " Sent AND received messages in order; survives a daemon restart. This is also how you satisfy\n" +
438
+ " read-before-write after being away: read it, then 'cello send' is accepted.",
439
+ flags: AGENT_FLAG,
440
+ ipcMethod: IPC_METHODS.transcript,
441
+ jsonOut: true,
442
+ async run(ctx, args) {
443
+ const { agent, pretty, positional } = parityOpts(args);
444
+ return transcript(ctx.celloDir, positional[0] ?? "", { agent, pretty });
445
+ },
446
+ },
447
+ {
448
+ name: "sealed-receipt",
449
+ summary: "Print a closed session's notarized bilateral seal receipt.",
450
+ help: "Usage: cello sealed-receipt <session-id> [--agent <name>] [--pretty] — the NOTARIZED receipt.\n" +
451
+ " The artifact the bilateral close ceremony produces: per-party content frontiers and the sealed\n" +
452
+ " root both sides agree on. It attests RECEIPT, never assent (implies_assent: false) — an\n" +
453
+ " unanswered final message reads as delivered-but-unanswered, never as agreement.\n" +
454
+ " Distinct from 'cello receipts <name>', which lists RELAY ORDERING receipts (a different thing).",
455
+ flags: AGENT_FLAG,
456
+ ipcMethod: IPC_METHODS["sealed-receipt"],
457
+ jsonOut: true,
458
+ async run(ctx, args) {
459
+ const { agent, pretty, positional } = parityOpts(args);
460
+ return sealedReceipt(ctx.celloDir, positional[0] ?? "", { agent, pretty });
461
+ },
462
+ },
463
+ // ─── Group B: live conversation ───────────────────────────────────────────────────────────
464
+ {
465
+ name: "initiate",
466
+ summary: "Start a session with a target agent (by pubkey). Prints the session_id.",
467
+ help: "Usage: cello initiate <target-pubkey> [--agent <name>] [--pretty] — open a session.\n" +
468
+ " <target-pubkey> is the counterparty's hex public key. Prints the session_id you then pass to\n" +
469
+ " 'cello send' / 'cello receive' / 'cello close'. Adds the counterparty to your address book.",
470
+ flags: AGENT_FLAG,
471
+ ipcMethod: IPC_METHODS.initiate,
472
+ jsonOut: true,
473
+ async run(ctx, args) {
474
+ const { agent, pretty, positional } = parityOpts(args);
475
+ return initiate(ctx.celloDir, positional[0] ?? "", { agent, pretty });
476
+ },
477
+ },
478
+ {
479
+ name: "send",
480
+ summary: "Send a message in a session (honors read-before-write).",
481
+ help: "Usage: cello send <session-id> <message…> [--stdin] [--agent <name>] [--pretty]\n" +
482
+ " The message is the remaining arguments, or the whole of stdin with --stdin (for text with\n" +
483
+ " newlines/quotes). READ-BEFORE-WRITE: if the counterparty has spoken since you last read, the\n" +
484
+ " daemon rejects the send with session_not_current and its cursor — that verdict is printed\n" +
485
+ " verbatim and NOT auto-fixed. Catch up with 'cello transcript <session-id>', then resend.",
486
+ flags: [
487
+ { name: "--agent", consumesValue: false },
488
+ { name: "--stdin", consumesValue: false },
489
+ ],
490
+ ipcMethod: IPC_METHODS.send,
491
+ jsonOut: true,
492
+ async run(ctx, args) {
493
+ const { agent, pretty, positional } = parityOpts(args);
494
+ const useStdin = positional.includes("--stdin");
495
+ const rest = positional.filter((a) => a !== "--stdin");
496
+ const sessionId = rest[0] ?? "";
497
+ const content = useStdin ? await readStdin() : rest.slice(1).join(" ");
498
+ return send(ctx.celloDir, sessionId, content, { agent, pretty });
499
+ },
500
+ },
501
+ {
502
+ name: "receive",
503
+ summary: "Receive the next message, or catch up in a batch with --since-seq.",
504
+ help: "Usage: cello receive <session-id> [--since-seq N] [--timeout-ms N] [--agent <name>] [--pretty]\n" +
505
+ " Default: BLOCKS for the next live message (up to --timeout-ms, default 30000).\n" +
506
+ " With --since-seq N: stateless CATCH-UP — returns every message after sequence N as a batch,\n" +
507
+ " immediately (no replay race, --timeout-ms ignored). Mirrors cello_receive exactly.",
508
+ flags: [
509
+ { name: "--agent", consumesValue: false },
510
+ { name: "--timeout-ms", consumesValue: true },
511
+ { name: "--since-seq", consumesValue: true },
512
+ ],
513
+ ipcMethod: IPC_METHODS.receive,
514
+ jsonOut: true,
515
+ async run(ctx, args) {
516
+ const { agent, pretty, positional } = parityOpts(args);
517
+ const since = takeValueFlag(positional, "--since-seq");
518
+ const timeout = takeValueFlag(since.rest, "--timeout-ms");
519
+ try {
520
+ return await receive(ctx.celloDir, timeout.rest[0] ?? "", {
521
+ agent,
522
+ pretty,
523
+ sinceSeq: numberOrUndefined(since.value, "--since-seq"),
524
+ timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms"),
525
+ });
526
+ }
527
+ catch (err) {
528
+ return flagError(err);
529
+ }
530
+ },
531
+ },
532
+ {
533
+ name: "receive-session",
534
+ summary: "Accept / join an inbound session request.",
535
+ help: "Usage: cello receive-session <session-id> [--timeout-ms N] [--agent <name>] [--pretty]\n" +
536
+ " Joins an inbound session (the one 'cello await-session' told you about).",
537
+ flags: AGENT_AND_TIMEOUT,
538
+ ipcMethod: IPC_METHODS["receive-session"],
539
+ jsonOut: true,
540
+ async run(ctx, args) {
541
+ const { agent, pretty, positional } = parityOpts(args);
542
+ const timeout = takeValueFlag(positional, "--timeout-ms");
543
+ try {
544
+ return await receiveSession(ctx.celloDir, timeout.rest[0] ?? "", {
545
+ agent,
546
+ pretty,
547
+ timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms"),
548
+ });
549
+ }
550
+ catch (err) {
551
+ return flagError(err);
552
+ }
553
+ },
554
+ },
555
+ {
556
+ name: "close",
557
+ summary: "Close a session — triggers the bilateral seal ceremony.",
558
+ help: "Usage: cello close <session-id> [--force] [--agent <name>] [--pretty]\n" +
559
+ " Normally runs the bilateral SEAL ceremony: both parties get a notarized receipt.\n" +
560
+ " --force abandons a half-open session that can never be sealed (a handshake the counterparty\n" +
561
+ " never joined). It FORFEITS the receipt — never use it on a healthy session.",
562
+ flags: [
563
+ { name: "--agent", consumesValue: false },
564
+ { name: "--force", consumesValue: false },
565
+ ],
566
+ ipcMethod: IPC_METHODS.close,
567
+ jsonOut: true,
568
+ async run(ctx, args) {
569
+ const { agent, pretty, positional } = parityOpts(args);
570
+ const force = positional.includes("--force");
571
+ const rest = positional.filter((a) => a !== "--force");
572
+ return closeSession(ctx.celloDir, rest[0] ?? "", { agent, pretty, force });
573
+ },
574
+ },
575
+ {
576
+ name: "await-session",
577
+ summary: "Block until an inbound session request arrives (the doorbell).",
578
+ help: "Usage: cello await-session [--timeout-ms N] [--agent <name>] [--pretty]\n" +
579
+ " BLOCKS until someone opens a session with you (default 30000ms), then prints the request.\n" +
580
+ " On expiry it returns {\"type\":\"timeout\"} and exits 0 — a timeout is a normal answer, not an\n" +
581
+ " error (this mirrors cello_await_session exactly). Branch on .type in scripts.",
582
+ flags: AGENT_AND_TIMEOUT,
583
+ ipcMethod: IPC_METHODS["await-session"],
584
+ jsonOut: true,
585
+ async run(ctx, args) {
586
+ const { agent, pretty, positional } = parityOpts(args);
587
+ const timeout = takeValueFlag(positional, "--timeout-ms");
588
+ try {
589
+ return await awaitSession(ctx.celloDir, { agent, pretty, timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms") });
590
+ }
591
+ catch (err) {
592
+ return flagError(err);
593
+ }
594
+ },
595
+ },
596
+ ];
597
+ export function commandNames() {
598
+ return COMMANDS.map((c) => c.name);
599
+ }
600
+ export function findCommand(name) {
601
+ return COMMANDS.find((c) => c.name === name);
602
+ }
603
+ /**
604
+ * Internal: a spec's help by name, for commands that print their own usage on bad input.
605
+ *
606
+ * Called with hardcoded names that MUST resolve, so a miss is a programmer error (a rename typo),
607
+ * not a runtime condition. Throwing beats the old `?? ""` default, which would have printed an
608
+ * EMPTY string with exit 1 — silently swallowing the operator's only guidance, and doing it in the
609
+ * one code path whose entire job is to explain what went wrong.
610
+ */
611
+ function helpForSpec(name) {
612
+ const spec = findCommand(name);
613
+ if (!spec)
614
+ throw new Error(`registry: no command '${name}' (a hardcoded help lookup is out of sync)`);
615
+ return spec.help;
616
+ }
617
+ /**
618
+ * The flags a command recognizes, derived from its registry entry. `--pretty` is granted
619
+ * automatically to every command honoring the §3 JSON contract, so it can never be forgotten.
620
+ */
621
+ export function flagsFor(name) {
622
+ const spec = findCommand(name);
623
+ const map = new Map();
624
+ if (!spec)
625
+ return map;
626
+ for (const f of spec.flags ?? [])
627
+ map.set(f.name, f);
628
+ if (spec.jsonOut)
629
+ map.set("--pretty", { name: "--pretty" });
630
+ return map;
631
+ }
632
+ /**
633
+ * DOD-ONBOARD-HELP-1: render the described `Commands:` table — each command on its own line with
634
+ * its one-line summary (git / `claude --help` style). Arguments stay in per-command `--help`.
635
+ */
636
+ export function renderCommandsTable() {
637
+ const width = Math.max(...COMMANDS.map((c) => c.name.length));
638
+ const rows = COMMANDS.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
639
+ return `Commands:\n${rows.join("\n")}`;
640
+ }
641
+ //# sourceMappingURL=registry.js.map