@cello-protocol/cli 0.0.45 → 0.0.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/registry.js CHANGED
@@ -14,7 +14,7 @@
14
14
  import { MONIKER_RE } from "@cello-protocol/protocol-types";
15
15
  import { login, logout, status, register, createAgent, removeAgent, refreshShares, relayReceipts, sessions, settingsGet, settingsSet, monikerSet, telegramSetToken, } from "./commands.js";
16
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";
17
+ import { IPC_METHODS, contactAdd, contactRemove, contactList, contactSetTier, contactSetAway, listAgents, startAgent, stopAgent, useAgent, inbox, transcript, contactSetMoniker, sealedReceipt, initiate, send, receive, closeSession, awaitSession, } from "./parity-commands.js";
18
18
  /** Read the whole of stdin — `cello send <id> --stdin` for message text with newlines/quotes. */
19
19
  async function readStdin() {
20
20
  const chunks = [];
@@ -22,6 +22,19 @@ async function readStdin() {
22
22
  chunks.push(Buffer.from(chunk));
23
23
  return Buffer.concat(chunks).toString("utf8");
24
24
  }
25
+ /**
26
+ * DOD-ONBOARD-HELP-1 §1 — the help is GROUPED, not alphabetical, and the groups render in this
27
+ * order. A new user reads it top-to-bottom as the order they will actually do things: get set up,
28
+ * bring an agent online, hold a conversation, then look at what it produced.
29
+ */
30
+ export const GROUP_ORDER = [
31
+ "Setup",
32
+ "Agents",
33
+ "Messaging",
34
+ "Sessions & receipts",
35
+ "Contacts",
36
+ "Other",
37
+ ];
25
38
  /** Parse the parity commands' shared flags out of argv (`--agent`, `--pretty`, and value flags). */
26
39
  function parityOpts(args) {
27
40
  const { agent, positional } = splitAgentFlag(args);
@@ -88,7 +101,7 @@ function legacy(result) {
88
101
  *
89
102
  * `consumesValue: false` is deliberate and is what checkArgs PARITY requires: the pre-existing
90
103
  * 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
104
+ * change `cello contacts --agent --bogus` from a fail-loud unknown_flag into a silently
92
105
  * accepted agent literally named "--bogus". The value is claimed by splitAgentFlag (arg-parse.ts),
93
106
  * which owns --agent parsing; checkArgs only needs to know the FLAG is legal. Same for install's
94
107
  * --agent / --hermes-home below.
@@ -100,9 +113,11 @@ const AGENT_AND_TIMEOUT = [
100
113
  { name: "--timeout-ms", consumesValue: true },
101
114
  ];
102
115
  export const COMMANDS = [
116
+ // ═══ Setup — get a working agent, in the order you actually do it ═══════════════════════════
103
117
  {
104
118
  name: "login",
105
- summary: "Start the local daemon (or connect to a running one) and bring your agents online.",
119
+ group: "Setup",
120
+ summary: "Start the local CELLO daemon and bring your agents online.",
106
121
  help: "Usage: cello login — start the daemon (or connect to an existing one).",
107
122
  async run(ctx) {
108
123
  return legacy(await login(ctx.celloDir, ctx.daemonBin, ctx.logger));
@@ -110,7 +125,8 @@ export const COMMANDS = [
110
125
  },
111
126
  {
112
127
  name: "logout",
113
- summary: "Stop the running daemon (waits until it is actually gone).",
128
+ group: "Setup",
129
+ summary: "Stop the daemon. Waits until it has actually exited.",
114
130
  help: "Usage: cello logout — send shutdown to the running daemon.",
115
131
  async run(ctx) {
116
132
  // DOD-LOGOUT-WAIT-1: logout WAITS for the daemon to actually die before claiming
@@ -121,24 +137,38 @@ export const COMMANDS = [
121
137
  },
122
138
  {
123
139
  name: "status",
124
- summary: "Show daemon + agent state as structured JSON.",
140
+ group: "Setup",
141
+ summary: "Show whether the daemon is running and which agents are online.",
125
142
  help: "Usage: cello status — query the daemon and print the structured status JSON.",
126
143
  async run(ctx) {
127
144
  return legacy(await status(ctx.celloDir));
128
145
  },
129
146
  },
130
147
  {
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" +
148
+ name: "create-agent",
149
+ group: "Setup",
150
+ summary: "Create a new agent on this machine. Step 1 of 2.",
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-agent <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: "register-agent",
161
+ group: "Setup",
162
+ summary: "Publish an agent to the directory so others can reach it. Step 2 of 2.",
163
+ help: "Usage: cello register-agent <agent> <pre-auth-token> — register a LOCAL agent with the directory.\n" +
164
+ " The two-step onboarding: (1) 'cello create-agent <name>' makes the identity on this machine; (2) 'cello register-agent <name> <token>' publishes it to the directory so others can find and reach it.\n" +
135
165
  " 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" +
166
+ " Example: cello register-agent alice CELLO-3xY7...\n" +
167
+ " Env-var form (avoids retyping): CELLO_PREAUTH_TOKEN=CELLO-3xY7... cello register-agent alice\n" +
138
168
  " Quoting is only needed if a value contains spaces (agent names and tokens never do).",
139
169
  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.
170
+ // cello register-agent <agent> [preAuthToken] (token falls back to CELLO_PREAUTH_TOKEN so it
171
+ // need not appear in shell history). Optional phone stub follows.
142
172
  const agent = args[0] ?? "";
143
173
  const preAuthToken = args[1] ?? process.env.CELLO_PREAUTH_TOKEN ?? "";
144
174
  const phoneStub = args[2] ?? "";
@@ -146,43 +176,249 @@ export const COMMANDS = [
146
176
  },
147
177
  },
148
178
  {
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.",
179
+ name: "remove-agent",
180
+ group: "Setup",
181
+ summary: "Retire an agent permanently and free its name. Cannot be undone.",
182
+ help: "Usage: cello remove-agent <name> — retires a local agent (one-way) and frees its name.",
155
183
  async run(ctx, args) {
156
- return legacy(await createAgent(ctx.celloDir, args[0] ?? ""));
184
+ return legacy(await removeAgent(ctx.celloDir, args[0] ?? ""));
157
185
  },
158
186
  },
187
+ // ═══ Agents — day-to-day control of who is online and who you are acting as ═════════════════
159
188
  {
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.",
189
+ name: "agents",
190
+ group: "Agents",
191
+ summary: "List your agents and whether each one is online.",
192
+ help: "Usage: cello agents [--pretty] — list all loaded agents (name, state).\n" +
193
+ " The CLI twin of the cello_agents MCP tool. Prints JSON; use --pretty for humans.",
194
+ ipcMethod: IPC_METHODS.agents,
195
+ jsonOut: true,
163
196
  async run(ctx, args) {
164
- return legacy(await removeAgent(ctx.celloDir, args[0] ?? ""));
197
+ const { pretty } = parityOpts(args);
198
+ return listAgents(ctx.celloDir, { pretty });
199
+ },
200
+ },
201
+ {
202
+ name: "start-agent",
203
+ group: "Agents",
204
+ summary: "Bring an agent online so it can be reached.",
205
+ help: "Usage: cello start-agent <name> [--pretty] — bring a registered agent ONLINE.\n" +
206
+ " Does NOT select it as the current agent — use 'cello use-agent <name>' for that.\n" +
207
+ " Idempotent: starting an already-online agent is safe.",
208
+ ipcMethod: IPC_METHODS["start-agent"],
209
+ jsonOut: true,
210
+ async run(ctx, args) {
211
+ const { pretty, positional } = parityOpts(args);
212
+ return startAgent(ctx.celloDir, positional[0] ?? "", { pretty });
213
+ },
214
+ },
215
+ {
216
+ name: "use-agent",
217
+ group: "Agents",
218
+ summary: "Select the agent that later commands operate through.",
219
+ help: "Usage: cello use-agent <name> [--pretty] — select the CURRENT agent for later commands.\n" +
220
+ " Brings the agent online first if it is offline (AUTOSTART-1).\n" +
221
+ " The selection PERSISTS across invocations (recorded in <cello-dir>/current-agent), because\n" +
222
+ " each CLI command opens its own daemon connection — a selection that lived only on the socket\n" +
223
+ " would vanish the moment the command exited. Override per-command with '--agent <name>'.\n" +
224
+ " A selection the daemon rejects is not recorded.",
225
+ ipcMethod: IPC_METHODS["use-agent"],
226
+ jsonOut: true,
227
+ async run(ctx, args) {
228
+ const { pretty, positional } = parityOpts(args);
229
+ return useAgent(ctx.celloDir, positional[0] ?? "", { pretty });
230
+ },
231
+ },
232
+ {
233
+ name: "stop-agent",
234
+ group: "Agents",
235
+ summary: "Take an agent offline. It stops accepting anything until restarted.",
236
+ help: "Usage: cello stop-agent <name> [--pretty] — take an agent offline.",
237
+ ipcMethod: IPC_METHODS["stop-agent"],
238
+ jsonOut: true,
239
+ async run(ctx, args) {
240
+ const { pretty, positional } = parityOpts(args);
241
+ return stopAgent(ctx.celloDir, positional[0] ?? "", { pretty });
165
242
  },
166
243
  },
167
244
  {
168
245
  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).",
246
+ group: "Agents",
247
+ // VERIFIED against the handler (cello_refresh_shares → runAgentRefresh), not guessed. It runs a
248
+ // resharing ceremony with the directory nodes and moves the agent to a NEW key epoch. Routine
249
+ // key hygiene — nothing is re-registered and the agent's public identity does not change.
250
+ summary: "Rotate an agent's signing-key shares to a fresh epoch (routine key hygiene).",
251
+ help: "Usage: cello refresh <name> — rotate the agent's split signing-key shares to a new epoch.\n" +
252
+ " CELLO never holds your whole signing key in one place — it is split into shares held with the\n" +
253
+ " directory nodes. This runs a ceremony that replaces every share with a fresh one. Your public\n" +
254
+ " identity does NOT change and you do not re-register; old shares simply stop being usable.\n" +
255
+ " Requires the directory to be reachable (the agent must be online and connected).\n" +
256
+ " Occasional hygiene, not something you need day to day.",
171
257
  async run(ctx, args) {
172
258
  return legacy(await refreshShares(ctx.celloDir, args[0] ?? ""));
173
259
  },
174
260
  },
261
+ // ═══ Messaging — the conversation itself ════════════════════════════════════════════════════
175
262
  {
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.",
263
+ name: "initiate-session",
264
+ group: "Messaging",
265
+ summary: "Open a session with someone (by public key). Prints the session id.",
266
+ help: "Usage: cello initiate-session <target-pubkey> [--agent <name>] [--pretty] — open a session.\n" +
267
+ " <target-pubkey> is the counterparty's hex public key. Prints the session_id you then pass to\n" +
268
+ " 'cello send' / 'cello receive' / 'cello close-session'. Adds them to your address book.",
269
+ flags: AGENT_FLAG,
270
+ ipcMethod: IPC_METHODS["initiate-session"],
271
+ jsonOut: true,
179
272
  async run(ctx, args) {
180
- return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
273
+ const { agent, pretty, positional } = parityOpts(args);
274
+ return initiate(ctx.celloDir, positional[0] ?? "", { agent, pretty });
275
+ },
276
+ },
277
+ {
278
+ name: "await-session",
279
+ group: "Messaging",
280
+ summary: "Wait for someone to open a session with you.",
281
+ help: "Usage: cello await-session [--timeout-ms N] [--agent <name>] [--pretty]\n" +
282
+ " BLOCKS until someone opens a session with you (default 30000ms), then prints the request.\n" +
283
+ " On expiry it returns {\"type\":\"timeout\"} and exits 0 — a timeout is a normal answer, not an\n" +
284
+ " error (this mirrors cello_await_session exactly). Branch on .type in scripts.",
285
+ flags: AGENT_AND_TIMEOUT,
286
+ ipcMethod: IPC_METHODS["await-session"],
287
+ jsonOut: true,
288
+ async run(ctx, args) {
289
+ const { agent, pretty, positional } = parityOpts(args);
290
+ const timeout = takeValueFlag(positional, "--timeout-ms");
291
+ try {
292
+ return await awaitSession(ctx.celloDir, { agent, pretty, timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms") });
293
+ }
294
+ catch (err) {
295
+ return flagError(err);
296
+ }
297
+ },
298
+ },
299
+ {
300
+ name: "close-session",
301
+ group: "Messaging",
302
+ summary: "End a session. Both sides sign off and get a tamper-proof receipt.",
303
+ help: "Usage: cello close-session <session-id> [--force] [--agent <name>] [--pretty]\n" +
304
+ " Both parties sign off on the whole conversation and each gets a notarized receipt\n" +
305
+ " ('cello sealed-receipt <session-id>' prints it).\n" +
306
+ " --force abandons a half-open session that can never be sealed (a handshake the counterparty\n" +
307
+ " never joined). It FORFEITS the receipt — never use it on a healthy session.",
308
+ flags: [
309
+ { name: "--agent", consumesValue: false },
310
+ { name: "--force", consumesValue: false },
311
+ ],
312
+ ipcMethod: IPC_METHODS["close-session"],
313
+ jsonOut: true,
314
+ async run(ctx, args) {
315
+ const { agent, pretty, positional } = parityOpts(args);
316
+ const force = positional.includes("--force");
317
+ const rest = positional.filter((a) => a !== "--force");
318
+ return closeSession(ctx.celloDir, rest[0] ?? "", { agent, pretty, force });
181
319
  },
182
320
  },
321
+ {
322
+ name: "send",
323
+ group: "Messaging",
324
+ // §4: "honors read-before-write" was jargon for a rule the operator meets as a REFUSAL. Say the
325
+ // rule, and say that the tool will tell you.
326
+ summary: "Send a message. Any unread messages must be read first — you'll be told, and blocked until you do.",
327
+ help: "Usage: cello send <session-id> <message…> [--stdin] [--agent <name>] [--pretty]\n" +
328
+ " The message is the remaining arguments, or the whole of stdin with --stdin (for text with\n" +
329
+ " newlines/quotes).\n" +
330
+ " If the other side has said something you have not read, the send is REFUSED and tells you how\n" +
331
+ " many messages are waiting. Read them ('cello receive <session-id>', or 'cello transcript\n" +
332
+ " <session-id>' for the whole conversation) and send again. This is deliberate: you cannot\n" +
333
+ " reply to something you never saw. The refusal is printed verbatim and never auto-fixed.",
334
+ flags: [
335
+ { name: "--agent", consumesValue: false },
336
+ { name: "--stdin", consumesValue: false },
337
+ ],
338
+ ipcMethod: IPC_METHODS.send,
339
+ jsonOut: true,
340
+ async run(ctx, args) {
341
+ const { agent, pretty, positional } = parityOpts(args);
342
+ const useStdin = positional.includes("--stdin");
343
+ const rest = positional.filter((a) => a !== "--stdin");
344
+ const sessionId = rest[0] ?? "";
345
+ const content = useStdin ? await readStdin() : rest.slice(1).join(" ");
346
+ return send(ctx.celloDir, sessionId, content, { agent, pretty });
347
+ },
348
+ },
349
+ {
350
+ name: "receive",
351
+ group: "Messaging",
352
+ summary: "Read the next message, or catch up on everything you missed with --since-seq.",
353
+ help: "Usage: cello receive <session-id> [--since-seq N] [--timeout-ms N] [--agent <name>] [--pretty]\n" +
354
+ " Default: WAITS for the next message (up to --timeout-ms, default 30000).\n" +
355
+ " With --since-seq N: returns every message after number N at once, immediately, without\n" +
356
+ " waiting — this is how you catch up after being away. Mirrors cello_receive exactly.",
357
+ flags: [
358
+ { name: "--agent", consumesValue: false },
359
+ { name: "--timeout-ms", consumesValue: true },
360
+ { name: "--since-seq", consumesValue: true },
361
+ ],
362
+ ipcMethod: IPC_METHODS.receive,
363
+ jsonOut: true,
364
+ async run(ctx, args) {
365
+ const { agent, pretty, positional } = parityOpts(args);
366
+ const since = takeValueFlag(positional, "--since-seq");
367
+ const timeout = takeValueFlag(since.rest, "--timeout-ms");
368
+ try {
369
+ return await receive(ctx.celloDir, timeout.rest[0] ?? "", {
370
+ agent,
371
+ pretty,
372
+ sinceSeq: numberOrUndefined(since.value, "--since-seq"),
373
+ timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms"),
374
+ });
375
+ }
376
+ catch (err) {
377
+ return flagError(err);
378
+ }
379
+ },
380
+ },
381
+ {
382
+ name: "inbox",
383
+ group: "Messaging",
384
+ summary: "See who tried to reach you and what is unread, without reading anything.",
385
+ help: "Usage: cello inbox [--scope current|all] [--agent <name>] [--pretty] — what did I miss?\n" +
386
+ " Shows pending session requests and unread message COUNTS — never message content, and it\n" +
387
+ " does not mark anything as read ('cello receive' does that). Use it after being away.\n" +
388
+ " --scope all covers every agent you have, not just the current one.",
389
+ flags: [
390
+ { name: "--agent", consumesValue: false },
391
+ { name: "--scope", consumesValue: true },
392
+ ],
393
+ ipcMethod: IPC_METHODS.inbox,
394
+ jsonOut: true,
395
+ async run(ctx, args) {
396
+ const { agent, pretty, positional } = parityOpts(args);
397
+ const { value } = takeValueFlag(positional, "--scope");
398
+ // Review F6: an UNRECOGNIZED scope must not silently become the default. A typo'd
399
+ // `--scope all` (e.g. "al") would have answered with `current`'s data and exit 0 — the
400
+ // operator reads "no notifications" while another agent's inbox is full.
401
+ if (value !== undefined && value !== "all" && value !== "current") {
402
+ return {
403
+ stdout: "",
404
+ stderr: JSON.stringify({
405
+ ok: false,
406
+ reason: "invalid_flag_value",
407
+ flag: "--scope",
408
+ value,
409
+ guidance: "--scope must be 'current' or 'all'. The command was NOT run — answering a different question than the one asked is worse than refusing.",
410
+ }),
411
+ exitCode: 1,
412
+ };
413
+ }
414
+ return inbox(ctx.celloDir, { agent, pretty, scope: value });
415
+ },
416
+ },
417
+ // ═══ Sessions & receipts — what the conversations left behind ═══════════════════════════════
183
418
  {
184
419
  name: "sessions",
185
- summary: "List session history (open by default; --all for everything).",
420
+ group: "Sessions & receipts",
421
+ summary: "List your sessions (open by default; --all/--closed/--failed to filter).",
186
422
  help: "Usage: cello sessions [--open|--closed|--failed|--all] [--limit N] — list session history (defaults to open).",
187
423
  flags: [
188
424
  { name: "--open" },
@@ -211,57 +447,132 @@ export const COMMANDS = [
211
447
  return legacy(await sessions(ctx.celloDir, { filter, limit }));
212
448
  },
213
449
  },
450
+ {
451
+ name: "transcript",
452
+ group: "Sessions & receipts",
453
+ summary: "Print the full conversation for a session — everything sent and received.",
454
+ help: "Usage: cello transcript <session-id> [--agent <name>] [--pretty] — the whole conversation.\n" +
455
+ " Sent AND received messages, in order. Stored on disk, so it survives a daemon restart.\n" +
456
+ " Reading it also catches you up, which un-blocks 'cello send' after you have been away.",
457
+ flags: AGENT_FLAG,
458
+ ipcMethod: IPC_METHODS.transcript,
459
+ jsonOut: true,
460
+ async run(ctx, args) {
461
+ const { agent, pretty, positional } = parityOpts(args);
462
+ return transcript(ctx.celloDir, positional[0] ?? "", { agent, pretty });
463
+ },
464
+ },
465
+ {
466
+ name: "sealed-receipt",
467
+ group: "Sessions & receipts",
468
+ // THE one users want. Named and described so it cannot be confused with relay-receipts.
469
+ summary: "Print a closed session's notarized receipt — proof both sides signed off on the conversation.",
470
+ help: "Usage: cello sealed-receipt <session-id> [--agent <name>] [--pretty] — the NOTARIZED receipt.\n" +
471
+ " This is the proof CELLO exists to produce: when a session closes, both parties sign off on\n" +
472
+ " the whole conversation and the directory notarizes it. The receipt is tamper-evident — if a\n" +
473
+ " single message were altered, added or dropped, it would no longer match.\n" +
474
+ " It attests RECEIPT, never agreement (implies_assent: false) — an unanswered last message\n" +
475
+ " reads as delivered-but-unanswered, never as consent.\n" +
476
+ " NOT the same as 'cello relay-receipts', which is a low-level delivery-plumbing artifact.",
477
+ flags: AGENT_FLAG,
478
+ ipcMethod: IPC_METHODS["sealed-receipt"],
479
+ jsonOut: true,
480
+ async run(ctx, args) {
481
+ const { agent, pretty, positional } = parityOpts(args);
482
+ return sealedReceipt(ctx.celloDir, positional[0] ?? "", { agent, pretty });
483
+ },
484
+ },
485
+ {
486
+ name: "relay-receipts",
487
+ group: "Sessions & receipts",
488
+ // VERIFIED against the handler (cello_get_relay_receipts → getRelayReceipts): per-MESSAGE
489
+ // signatures from a RELAY attesting it handled and ordered that message. Renamed from
490
+ // `receipts` because a name that differed from `sealed-receipt` by one plural could not be
491
+ // rescued by any description — Andre could not tell them apart, and he wrote the protocol.
492
+ summary: "Advanced/debug: per-message proofs signed by a relay. Not the session receipt — see 'sealed-receipt'.",
493
+ help: "Usage: cello relay-receipts <name> — ADVANCED / DEBUG. You almost certainly want\n" +
494
+ " 'cello sealed-receipt <session-id>' instead.\n" +
495
+ " When a message cannot go directly to the other agent (they are offline, or the network is in\n" +
496
+ " the way), it goes via a relay. The relay signs a small receipt saying it handled that message\n" +
497
+ " and where it fell in the order. This lists those — a plumbing artifact for diagnosing\n" +
498
+ " delivery, one per message.\n" +
499
+ " It says NOTHING about the conversation being agreed or sealed. That is 'cello sealed-receipt'.",
500
+ async run(ctx, args) {
501
+ return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
502
+ },
503
+ },
504
+ // ═══ Contacts — the address book (plural) and one contact (singular) ════════════════════════
505
+ {
506
+ name: "contacts",
507
+ group: "Contacts",
508
+ summary: "List your address book — everyone this agent knows, and how much they're trusted.",
509
+ help: "Usage: cello contacts [--agent <name>] [--pretty] — list the whole address book.\n" +
510
+ " Contacts are added automatically when you open a session with someone, or accept theirs.\n" +
511
+ " To act on ONE contact, use 'cello contact <pubkey> <operation>'.\n" +
512
+ " --agent defaults to the current agent (or the only online one).",
513
+ flags: AGENT_FLAG,
514
+ jsonOut: true,
515
+ ipcMethod: IPC_METHODS.contacts,
516
+ async run(ctx, args) {
517
+ const { agent, pretty } = parityOpts(args);
518
+ return contactList(ctx.celloDir, { agent, pretty });
519
+ },
520
+ },
214
521
  {
215
522
  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",
523
+ group: "Contacts",
524
+ summary: "Act on ONE contact: add, remove, set-tier, set-away, set-moniker.",
525
+ help: "Usage: cello contact <pubkey> <operation> [args] [--agent <name>] [--pretty]\n" +
526
+ "\n" +
527
+ " Operations:\n" +
528
+ " add add this peer to the address book\n" +
529
+ " remove remove them (they go back to being a stranger)\n" +
530
+ " set-tier <0..4> how much they're trusted: 0=blocked, 1=stranger, 2=known,\n" +
531
+ " 3=trusted (reaches you even when you're away), 4=vip.\n" +
532
+ " A higher tier RAISES their limits; it never removes screening.\n" +
533
+ " set-away <message…> what THIS person hears when you're away (empty clears it)\n" +
534
+ " set-moniker <name> YOUR pet name for THEM (empty clears it). Always wins over the\n" +
535
+ " name they offer — the one thing they cannot spoof.\n" +
536
+ "\n" +
537
+ " To list the whole book, use 'cello contacts'.\n" +
538
+ " Note: 'set-moniker' names a CONTACT. 'cello moniker' sets your OWN outbound name.\n" +
539
+ " Example: cello contact 178d420b… set-tier 3 --agent alice",
226
540
  flags: AGENT_FLAG,
227
541
  jsonOut: true, // review F3: the WHOLE address book honors §3 — one command, one contract
228
542
  async run(ctx, args) {
229
543
  const { agent, pretty, positional } = parityOpts(args);
230
544
  const o = { agent, pretty };
231
- const [sub, pubkey, valueArg] = positional;
232
- if (sub === "add" && pubkey)
545
+ // §3 SHAPE: `contact <pubkey> <op>` — the subject first, then what to do to them. (The old
546
+ // shape was `contact <op> <pubkey>`, which read like a verb table rather than an address book.)
547
+ const [pubkey, op, valueArg] = positional;
548
+ if (!pubkey || !op)
549
+ return { stdout: helpForSpec("contact"), stderr: "", exitCode: 1 };
550
+ if (op === "add")
233
551
  return contactAdd(ctx.celloDir, pubkey, o);
234
- if (sub === "remove" && pubkey)
552
+ if (op === "remove")
235
553
  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) {
554
+ if (op === "set-tier" && valueArg !== undefined) {
241
555
  // Daemon validates the value; a non-numeric arg surfaces as its invalid_tier verdict.
242
556
  return contactSetTier(ctx.celloDir, pubkey, Number(valueArg), o);
243
557
  }
244
- if ((sub === "set-away" || sub === "away") && pubkey) {
558
+ if (op === "set-away") {
245
559
  // The rest of the args form the away text; empty → clear.
246
560
  const message = positional.slice(2).join(" ");
247
561
  return contactSetAway(ctx.celloDir, pubkey, message.length > 0 ? message : null, o);
248
562
  }
249
- if (sub === "set-moniker" && pubkey) {
250
- // DOD-CLI-PARITY-1: the per-CONTACT pet name (cello_contact_set_moniker) — was MCP-only.
563
+ if (op === "set-moniker") {
251
564
  // Empty → null clears it, mirroring the tool.
252
565
  const moniker = positional.slice(2).join(" ");
253
566
  return contactSetMoniker(ctx.celloDir, pubkey, moniker.length > 0 ? moniker : null, o);
254
567
  }
255
- return {
256
- stdout: helpForSpec("contact"),
257
- stderr: "",
258
- exitCode: 1,
259
- };
568
+ return { stdout: helpForSpec("contact"), stderr: "", exitCode: 1 };
260
569
  },
261
570
  },
571
+ // ═══ Other ══════════════════════════════════════════════════════════════════════════════════
262
572
  {
263
573
  name: "settings",
264
- summary: "Get or set an agent's reachability policy (session/byte bounds, away text).",
574
+ group: "Other",
575
+ summary: "Get or set how reachable an agent is (limits per trust tier, away messages).",
265
576
  help: "Usage: cello settings get [key] [--agent <name>] | cello settings set <key> <value> [--agent <name>]\n" +
266
577
  " Per-agent reachability policy (DOD-SETTINGS-1). Keys: bounds.<tier>.max_sessions, bounds.<tier>.max_bytes\n" +
267
578
  " (tier = unknown|known|whitelisted|vip; a finite positive integer), away.default, away.tier.<tier> (away text).\n" +
@@ -284,12 +595,15 @@ export const COMMANDS = [
284
595
  },
285
596
  {
286
597
  name: "moniker",
287
- summary: "Set or clear the agent's OWN outbound display name (what a counterparty sees).",
598
+ group: "Other",
599
+ summary: "Set the name OTHERS see when this agent contacts them (like caller ID).",
288
600
  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" +
601
+ " Your OUTBOUND name — what shows up on the counterparty's screen when you reach them.\n" +
602
+ " Defaults to the agent name; 'set' overrides it, 'clear' restores the default.\n" +
290
603
  // MONIKER-0 AC2: the regex text is DERIVED from the shared constant, never hand-typed.
291
604
  ` 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" +
605
+ " It is a HINT, not proof — like caller ID, the receiver is shown it as self-declared and can\n" +
606
+ " override it with their own pet name for you. Never sent to the directory.\n" +
293
607
  " Example: cello moniker set Wonderland_Alice --agent alice",
294
608
  flags: AGENT_FLAG,
295
609
  async run(ctx, args) {
@@ -304,9 +618,12 @@ export const COMMANDS = [
304
618
  },
305
619
  {
306
620
  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.",
621
+ group: "Other",
622
+ summary: "Connect a Telegram bot to your daemon for notifications, status updates, etc.",
623
+ help: "Usage: cello telegram set-token <bot_token> <allowlisted_chat_id>\n" +
624
+ " Connects a Telegram bot to your daemon so you get notified there (someone reaching you,\n" +
625
+ " status updates, and more over time). Starts polling immediately.\n" +
626
+ " The chat id you give is the ONLY chat that ever receives anything.",
310
627
  async run(ctx, args) {
311
628
  const [sub, botToken, chatId] = args;
312
629
  if (sub === "set-token" && botToken && chatId) {
@@ -316,23 +633,32 @@ export const COMMANDS = [
316
633
  },
317
634
  },
318
635
  {
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",
636
+ name: "bridge",
637
+ group: "Other",
638
+ // Renamed from `install`, which read as "install CELLO itself" and hardcoded Hermes — the
639
+ // runtime is a PARAMETER. More runtimes are coming; the description must not claim otherwise.
640
+ summary: "Bridge CELLO into a third-party agent runtime (Hermes, OpenClaw, …).",
641
+ help: "Usage: cello bridge <runtime> --agent <name> [--hermes-home <path>]\n" +
642
+ " Wires the local CELLO daemon into a third-party agent runtime so that agent can use CELLO.\n" +
643
+ " Supported runtimes: hermes (more coming).\n" +
644
+ "\n" +
645
+ " hermes: scaffolds the CELLO plugin into the Hermes home (default ~/.hermes), binds\n" +
646
+ " CELLO_AGENT_NAME in its .env, and registers via 'hermes plugins enable cello' +\n" +
647
+ " 'hermes mcp add cello'. Idempotent — re-run to upgrade.\n" +
648
+ " Afterwards, restart the gateway: hermes gateway restart\n" +
649
+ "\n" +
650
+ " Example: cello bridge hermes --agent alice",
325
651
  flags: [{ name: "--agent" }, { name: "--hermes-home" }],
326
652
  async run(_ctx, args) {
327
653
  const agentIdx = args.indexOf("--agent");
328
654
  const homeIdx = args.indexOf("--hermes-home");
329
655
  // Find the target positional, excluding both flags AND their values — so
330
- // `cello install --agent alice hermes` still resolves target=hermes.
656
+ // `cello bridge --agent alice hermes` still resolves target=hermes.
331
657
  const target = args.find((a, i) => !a.startsWith("-") &&
332
658
  !(agentIdx !== -1 && i === agentIdx + 1) &&
333
659
  !(homeIdx !== -1 && i === homeIdx + 1));
334
660
  if (target !== "hermes") {
335
- return { stdout: helpForSpec("install"), stderr: "", exitCode: 1 };
661
+ return { stdout: helpForSpec("bridge"), stderr: "", exitCode: 1 };
336
662
  }
337
663
  const { installHermes } = await import("./hermes/install-hermes.js");
338
664
  return legacy(await installHermes({
@@ -341,258 +667,6 @@ export const COMMANDS = [
341
667
  }));
342
668
  },
343
669
  },
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
670
  ];
597
671
  export function commandNames() {
598
672
  return COMMANDS.map((c) => c.name);
@@ -630,12 +704,19 @@ export function flagsFor(name) {
630
704
  return map;
631
705
  }
632
706
  /**
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`.
707
+ * DOD-ONBOARD-HELP-1 §1: render the `Commands:` table GROUPED and in logical order.
708
+ *
709
+ * Flat-and-arbitrary was the reopen: `register` appeared before `create-agent`, so the table
710
+ * literally listed step 2 above step 1. Sections in GROUP_ORDER, commands in declaration order
711
+ * within each — the order a reader would actually do them. Name column is padded across the WHOLE
712
+ * table (not per group) so the summaries line up as one column down the page.
635
713
  */
636
714
  export function renderCommandsTable() {
637
715
  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")}`;
716
+ const sections = GROUP_ORDER.map((group) => {
717
+ const rows = COMMANDS.filter((c) => c.group === group).map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
718
+ return rows.length === 0 ? null : `${group}:\n${rows.join("\n")}`;
719
+ }).filter((s) => s !== null);
720
+ return sections.join("\n\n");
640
721
  }
641
722
  //# sourceMappingURL=registry.js.map