@cello-protocol/cli 0.0.45 → 0.0.46

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
@@ -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,287 +137,287 @@ 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] ?? "";
145
175
  return legacy(await register(ctx.celloDir, agent, preAuthToken, phoneStub));
146
176
  },
147
177
  },
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
178
  {
160
179
  name: "remove-agent",
161
- summary: "Retire a local agent (one-way) and free its name.",
180
+ group: "Setup",
181
+ summary: "Retire an agent permanently and free its name. Cannot be undone.",
162
182
  help: "Usage: cello remove-agent <name> — retires a local agent (one-way) and frees its name.",
163
183
  async run(ctx, args) {
164
184
  return legacy(await removeAgent(ctx.celloDir, args[0] ?? ""));
165
185
  },
166
186
  },
187
+ // ═══ Agents — day-to-day control of who is online and who you are acting as ═════════════════
167
188
  {
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).",
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,
171
196
  async run(ctx, args) {
172
- return legacy(await refreshShares(ctx.celloDir, args[0] ?? ""));
197
+ const { pretty } = parityOpts(args);
198
+ return listAgents(ctx.celloDir, { pretty });
173
199
  },
174
200
  },
175
201
  {
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.",
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,
179
210
  async run(ctx, args) {
180
- return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
211
+ const { pretty, positional } = parityOpts(args);
212
+ return startAgent(ctx.celloDir, positional[0] ?? "", { pretty });
181
213
  },
182
214
  },
183
215
  {
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
- ],
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,
194
227
  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 }));
228
+ const { pretty, positional } = parityOpts(args);
229
+ return useAgent(ctx.celloDir, positional[0] ?? "", { pretty });
212
230
  },
213
231
  },
214
232
  {
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
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,
228
239
  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
- };
240
+ const { pretty, positional } = parityOpts(args);
241
+ return stopAgent(ctx.celloDir, positional[0] ?? "", { pretty });
260
242
  },
261
243
  },
262
244
  {
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,
245
+ name: "refresh",
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.",
270
257
  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
- };
258
+ return legacy(await refreshShares(ctx.celloDir, args[0] ?? ""));
283
259
  },
284
260
  },
261
+ // ═══ Messaging — the conversation itself ════════════════════════════════════════════════════
285
262
  {
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",
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.",
294
269
  flags: AGENT_FLAG,
270
+ ipcMethod: IPC_METHODS["initiate-session"],
271
+ jsonOut: true,
295
272
  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 };
273
+ const { agent, pretty, positional } = parityOpts(args);
274
+ return initiate(ctx.celloDir, positional[0] ?? "", { agent, pretty });
303
275
  },
304
276
  },
305
277
  {
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.",
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,
310
288
  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));
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") });
314
293
  }
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 };
294
+ catch (err) {
295
+ return flagError(err);
336
296
  }
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
297
  },
343
298
  },
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
299
  {
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,
300
+ name: "receive-session",
301
+ group: "Messaging",
302
+ // TRUTH, not the old claim. The daemon registers the SAME handler for cello_receive_session and
303
+ // cello_receive (`handlers.set("cello_receive_session", handleReceive)`) — it does not accept or
304
+ // join anything; inbound sessions are auto-accepted by the standing receiver. The old summary
305
+ // ("Accept / join an inbound session request") described a step that does not exist. Slated for
306
+ // deletion under the no-aliases doctrine; until then it says what it is.
307
+ summary: "Alias of 'receive' — same behavior, no separate accept step. Prefer 'cello receive'.",
308
+ help: "Usage: cello receive-session <session-id> [--timeout-ms N] [--agent <name>] [--pretty]\n" +
309
+ " An ALIAS of 'cello receive' — the daemon runs the identical handler for both. It does NOT\n" +
310
+ " 'accept' or 'join' anything: an inbound session is auto-accepted for you, so there is no\n" +
311
+ " separate accept step to run. Use 'cello receive'; this exists only for backward parity with\n" +
312
+ " the cello_receive_session MCP tool and is expected to be removed.",
313
+ flags: AGENT_AND_TIMEOUT,
314
+ ipcMethod: IPC_METHODS["receive-session"],
353
315
  jsonOut: true,
354
316
  async run(ctx, args) {
355
- const { pretty } = parityOpts(args);
356
- return listAgents(ctx.celloDir, { pretty });
317
+ const { agent, pretty, positional } = parityOpts(args);
318
+ const timeout = takeValueFlag(positional, "--timeout-ms");
319
+ try {
320
+ return await receiveSession(ctx.celloDir, timeout.rest[0] ?? "", {
321
+ agent,
322
+ pretty,
323
+ timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms"),
324
+ });
325
+ }
326
+ catch (err) {
327
+ return flagError(err);
328
+ }
357
329
  },
358
330
  },
359
331
  {
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"],
332
+ name: "close-session",
333
+ group: "Messaging",
334
+ summary: "End a session. Both sides sign off and get a tamper-proof receipt.",
335
+ help: "Usage: cello close-session <session-id> [--force] [--agent <name>] [--pretty]\n" +
336
+ " Both parties sign off on the whole conversation and each gets a notarized receipt\n" +
337
+ " ('cello sealed-receipt <session-id>' prints it).\n" +
338
+ " --force abandons a half-open session that can never be sealed (a handshake the counterparty\n" +
339
+ " never joined). It FORFEITS the receipt — never use it on a healthy session.",
340
+ flags: [
341
+ { name: "--agent", consumesValue: false },
342
+ { name: "--force", consumesValue: false },
343
+ ],
344
+ ipcMethod: IPC_METHODS["close-session"],
366
345
  jsonOut: true,
367
346
  async run(ctx, args) {
368
- const { pretty, positional } = parityOpts(args);
369
- return startAgent(ctx.celloDir, positional[0] ?? "", { pretty });
347
+ const { agent, pretty, positional } = parityOpts(args);
348
+ const force = positional.includes("--force");
349
+ const rest = positional.filter((a) => a !== "--force");
350
+ return closeSession(ctx.celloDir, rest[0] ?? "", { agent, pretty, force });
370
351
  },
371
352
  },
372
353
  {
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"],
354
+ name: "send",
355
+ group: "Messaging",
356
+ // §4: "honors read-before-write" was jargon for a rule the operator meets as a REFUSAL. Say the
357
+ // rule, and say that the tool will tell you.
358
+ summary: "Send a message. Any unread messages must be read first — you'll be told, and blocked until you do.",
359
+ help: "Usage: cello send <session-id> <message…> [--stdin] [--agent <name>] [--pretty]\n" +
360
+ " The message is the remaining arguments, or the whole of stdin with --stdin (for text with\n" +
361
+ " newlines/quotes).\n" +
362
+ " If the other side has said something you have not read, the send is REFUSED and tells you how\n" +
363
+ " many messages are waiting. Read them ('cello receive <session-id>', or 'cello transcript\n" +
364
+ " <session-id>' for the whole conversation) and send again. This is deliberate: you cannot\n" +
365
+ " reply to something you never saw. The refusal is printed verbatim and never auto-fixed.",
366
+ flags: [
367
+ { name: "--agent", consumesValue: false },
368
+ { name: "--stdin", consumesValue: false },
369
+ ],
370
+ ipcMethod: IPC_METHODS.send,
377
371
  jsonOut: true,
378
372
  async run(ctx, args) {
379
- const { pretty, positional } = parityOpts(args);
380
- return stopAgent(ctx.celloDir, positional[0] ?? "", { pretty });
373
+ const { agent, pretty, positional } = parityOpts(args);
374
+ const useStdin = positional.includes("--stdin");
375
+ const rest = positional.filter((a) => a !== "--stdin");
376
+ const sessionId = rest[0] ?? "";
377
+ const content = useStdin ? await readStdin() : rest.slice(1).join(" ");
378
+ return send(ctx.celloDir, sessionId, content, { agent, pretty });
381
379
  },
382
380
  },
383
381
  {
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"],
382
+ name: "receive",
383
+ group: "Messaging",
384
+ summary: "Read the next message, or catch up on everything you missed with --since-seq.",
385
+ help: "Usage: cello receive <session-id> [--since-seq N] [--timeout-ms N] [--agent <name>] [--pretty]\n" +
386
+ " Default: WAITS for the next message (up to --timeout-ms, default 30000).\n" +
387
+ " With --since-seq N: returns every message after number N at once, immediately, without\n" +
388
+ " waiting — this is how you catch up after being away. Mirrors cello_receive exactly.",
389
+ flags: [
390
+ { name: "--agent", consumesValue: false },
391
+ { name: "--timeout-ms", consumesValue: true },
392
+ { name: "--since-seq", consumesValue: true },
393
+ ],
394
+ ipcMethod: IPC_METHODS.receive,
393
395
  jsonOut: true,
394
396
  async run(ctx, args) {
395
- const { pretty, positional } = parityOpts(args);
396
- return useAgent(ctx.celloDir, positional[0] ?? "", { pretty });
397
+ const { agent, pretty, positional } = parityOpts(args);
398
+ const since = takeValueFlag(positional, "--since-seq");
399
+ const timeout = takeValueFlag(since.rest, "--timeout-ms");
400
+ try {
401
+ return await receive(ctx.celloDir, timeout.rest[0] ?? "", {
402
+ agent,
403
+ pretty,
404
+ sinceSeq: numberOrUndefined(since.value, "--since-seq"),
405
+ timeoutMs: numberOrUndefined(timeout.value, "--timeout-ms"),
406
+ });
407
+ }
408
+ catch (err) {
409
+ return flagError(err);
410
+ }
397
411
  },
398
412
  },
399
413
  {
400
414
  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.",
415
+ group: "Messaging",
416
+ summary: "See who tried to reach you and what is unread, without reading anything.",
417
+ help: "Usage: cello inbox [--scope current|all] [--agent <name>] [--pretty] — what did I miss?\n" +
418
+ " Shows pending session requests and unread message COUNTS — never message content, and it\n" +
419
+ " does not mark anything as read ('cello receive' does that). Use it after being away.\n" +
420
+ " --scope all covers every agent you have, not just the current one.",
405
421
  flags: [
406
422
  { name: "--agent", consumesValue: false },
407
423
  { name: "--scope", consumesValue: true },
@@ -430,12 +446,46 @@ export const COMMANDS = [
430
446
  return inbox(ctx.celloDir, { agent, pretty, scope: value });
431
447
  },
432
448
  },
449
+ // ═══ Sessions & receipts — what the conversations left behind ═══════════════════════════════
450
+ {
451
+ name: "sessions",
452
+ group: "Sessions & receipts",
453
+ summary: "List your sessions (open by default; --all/--closed/--failed to filter).",
454
+ help: "Usage: cello sessions [--open|--closed|--failed|--all] [--limit N] — list session history (defaults to open).",
455
+ flags: [
456
+ { name: "--open" },
457
+ { name: "--closed" },
458
+ { name: "--failed" },
459
+ { name: "--all" },
460
+ { name: "--limit", consumesValue: true },
461
+ ],
462
+ async run(ctx, args) {
463
+ let filter;
464
+ if (args.includes("--all"))
465
+ filter = "all";
466
+ else if (args.includes("--closed"))
467
+ filter = "closed";
468
+ else if (args.includes("--failed"))
469
+ filter = "failed";
470
+ else if (args.includes("--open"))
471
+ filter = "open";
472
+ const limitIdx = args.indexOf("--limit");
473
+ let limit;
474
+ if (limitIdx !== -1 && args[limitIdx + 1] !== undefined) {
475
+ const n = Number(args[limitIdx + 1]);
476
+ if (Number.isFinite(n) && n > 0)
477
+ limit = Math.floor(n);
478
+ }
479
+ return legacy(await sessions(ctx.celloDir, { filter, limit }));
480
+ },
481
+ },
433
482
  {
434
483
  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.",
484
+ group: "Sessions & receipts",
485
+ summary: "Print the full conversation for a session — everything sent and received.",
486
+ help: "Usage: cello transcript <session-id> [--agent <name>] [--pretty] — the whole conversation.\n" +
487
+ " Sent AND received messages, in order. Stored on disk, so it survives a daemon restart.\n" +
488
+ " Reading it also catches you up, which un-blocks 'cello send' after you have been away.",
439
489
  flags: AGENT_FLAG,
440
490
  ipcMethod: IPC_METHODS.transcript,
441
491
  jsonOut: true,
@@ -446,12 +496,16 @@ export const COMMANDS = [
446
496
  },
447
497
  {
448
498
  name: "sealed-receipt",
449
- summary: "Print a closed session's notarized bilateral seal receipt.",
499
+ group: "Sessions & receipts",
500
+ // THE one users want. Named and described so it cannot be confused with relay-receipts.
501
+ summary: "Print a closed session's notarized receipt — proof both sides signed off on the conversation.",
450
502
  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).",
503
+ " This is the proof CELLO exists to produce: when a session closes, both parties sign off on\n" +
504
+ " the whole conversation and the directory notarizes it. The receipt is tamper-evident — if a\n" +
505
+ " single message were altered, added or dropped, it would no longer match.\n" +
506
+ " It attests RECEIPT, never agreement (implies_assent: false) — an unanswered last message\n" +
507
+ " reads as delivered-but-unanswered, never as consent.\n" +
508
+ " NOT the same as 'cello relay-receipts', which is a low-level delivery-plumbing artifact.",
455
509
  flags: AGENT_FLAG,
456
510
  ipcMethod: IPC_METHODS["sealed-receipt"],
457
511
  jsonOut: true,
@@ -460,137 +514,189 @@ export const COMMANDS = [
460
514
  return sealedReceipt(ctx.celloDir, positional[0] ?? "", { agent, pretty });
461
515
  },
462
516
  },
463
- // ─── Group B: live conversation ───────────────────────────────────────────────────────────
464
517
  {
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,
518
+ name: "relay-receipts",
519
+ group: "Sessions & receipts",
520
+ // VERIFIED against the handler (cello_get_relay_receipts → getRelayReceipts): per-MESSAGE
521
+ // signatures from a RELAY attesting it handled and ordered that message. Renamed from
522
+ // `receipts` because a name that differed from `sealed-receipt` by one plural could not be
523
+ // rescued by any description — Andre could not tell them apart, and he wrote the protocol.
524
+ summary: "Advanced/debug: per-message proofs signed by a relay. Not the session receipt — see 'sealed-receipt'.",
525
+ help: "Usage: cello relay-receipts <name> — ADVANCED / DEBUG. You almost certainly want\n" +
526
+ " 'cello sealed-receipt <session-id>' instead.\n" +
527
+ " When a message cannot go directly to the other agent (they are offline, or the network is in\n" +
528
+ " the way), it goes via a relay. The relay signs a small receipt saying it handled that message\n" +
529
+ " and where it fell in the order. This lists those — a plumbing artifact for diagnosing\n" +
530
+ " delivery, one per message.\n" +
531
+ " It says NOTHING about the conversation being agreed or sealed. That is 'cello sealed-receipt'.",
473
532
  async run(ctx, args) {
474
- const { agent, pretty, positional } = parityOpts(args);
475
- return initiate(ctx.celloDir, positional[0] ?? "", { agent, pretty });
533
+ return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
476
534
  },
477
535
  },
536
+ // ═══ Contacts — the address book (plural) and one contact (singular) ════════════════════════
478
537
  {
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,
538
+ name: "contacts",
539
+ group: "Contacts",
540
+ summary: "List your address book — everyone this agent knows, and how much they're trusted.",
541
+ help: "Usage: cello contacts [--agent <name>] [--pretty] — list the whole address book.\n" +
542
+ " Contacts are added automatically when you open a session with someone, or accept theirs.\n" +
543
+ " To act on ONE contact, use 'cello contact <pubkey> <operation>'.\n" +
544
+ " --agent defaults to the current agent (or the only online one).",
545
+ flags: AGENT_FLAG,
491
546
  jsonOut: true,
547
+ ipcMethod: IPC_METHODS.contacts,
492
548
  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 });
549
+ const { agent, pretty } = parityOpts(args);
550
+ return contactList(ctx.celloDir, { agent, pretty });
499
551
  },
500
552
  },
501
553
  {
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,
554
+ name: "contact",
555
+ group: "Contacts",
556
+ summary: "Act on ONE contact: add, remove, set-tier, set-away, set-moniker.",
557
+ help: "Usage: cello contact <pubkey> <operation> [args] [--agent <name>] [--pretty]\n" +
558
+ "\n" +
559
+ " Operations:\n" +
560
+ " add add this peer to the address book\n" +
561
+ " remove remove them (they go back to being a stranger)\n" +
562
+ " set-tier <0..4> how much they're trusted: 0=blocked, 1=stranger, 2=known,\n" +
563
+ " 3=trusted (reaches you even when you're away), 4=vip.\n" +
564
+ " A higher tier RAISES their limits; it never removes screening.\n" +
565
+ " set-away <message…> what THIS person hears when you're away (empty clears it)\n" +
566
+ " set-moniker <name> YOUR pet name for THEM (empty clears it). Always wins over the\n" +
567
+ " name they offer — the one thing they cannot spoof.\n" +
568
+ "\n" +
569
+ " To list the whole book, use 'cello contacts'.\n" +
570
+ " Note: 'set-moniker' names a CONTACT. 'cello moniker' sets your OWN outbound name.\n" +
571
+ " Example: cello contact 178d420b… set-tier 3 --agent alice",
572
+ flags: AGENT_FLAG,
573
+ jsonOut: true, // review F3: the WHOLE address book honors §3 — one command, one contract
515
574
  async run(ctx, args) {
516
575
  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
- });
576
+ const o = { agent, pretty };
577
+ // §3 SHAPE: `contact <pubkey> <op>` — the subject first, then what to do to them. (The old
578
+ // shape was `contact <op> <pubkey>`, which read like a verb table rather than an address book.)
579
+ const [pubkey, op, valueArg] = positional;
580
+ if (!pubkey || !op)
581
+ return { stdout: helpForSpec("contact"), stderr: "", exitCode: 1 };
582
+ if (op === "add")
583
+ return contactAdd(ctx.celloDir, pubkey, o);
584
+ if (op === "remove")
585
+ return contactRemove(ctx.celloDir, pubkey, o);
586
+ if (op === "set-tier" && valueArg !== undefined) {
587
+ // Daemon validates the value; a non-numeric arg surfaces as its invalid_tier verdict.
588
+ return contactSetTier(ctx.celloDir, pubkey, Number(valueArg), o);
526
589
  }
527
- catch (err) {
528
- return flagError(err);
590
+ if (op === "set-away") {
591
+ // The rest of the args form the away text; empty → clear.
592
+ const message = positional.slice(2).join(" ");
593
+ return contactSetAway(ctx.celloDir, pubkey, message.length > 0 ? message : null, o);
594
+ }
595
+ if (op === "set-moniker") {
596
+ // Empty → null clears it, mirroring the tool.
597
+ const moniker = positional.slice(2).join(" ");
598
+ return contactSetMoniker(ctx.celloDir, pubkey, moniker.length > 0 ? moniker : null, o);
529
599
  }
600
+ return { stdout: helpForSpec("contact"), stderr: "", exitCode: 1 };
530
601
  },
531
602
  },
603
+ // ═══ Other ══════════════════════════════════════════════════════════════════════════════════
532
604
  {
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,
605
+ name: "settings",
606
+ group: "Other",
607
+ summary: "Get or set how reachable an agent is (limits per trust tier, away messages).",
608
+ help: "Usage: cello settings get [key] [--agent <name>] | cello settings set <key> <value> [--agent <name>]\n" +
609
+ " Per-agent reachability policy (DOD-SETTINGS-1). Keys: bounds.<tier>.max_sessions, bounds.<tier>.max_bytes\n" +
610
+ " (tier = unknown|known|whitelisted|vip; a finite positive integer), away.default, away.tier.<tier> (away text).\n" +
611
+ " An unset key uses the built-in default. Example: cello settings set bounds.known.max_sessions 8 --agent alice",
612
+ flags: AGENT_FLAG,
540
613
  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);
614
+ const { agent, positional } = splitAgentFlag(args);
615
+ const [sub, key, value] = positional;
616
+ if (sub === "get")
617
+ return legacy(await settingsGet(ctx.celloDir, key, agent)); // key optional → all
618
+ if (sub === "set" && key && value !== undefined) {
619
+ return legacy(await settingsSet(ctx.celloDir, key, value, agent));
552
620
  }
621
+ return {
622
+ stdout: "Usage: cello settings get [key] [--agent <name>] | cello settings set <key> <value> [--agent <name>]",
623
+ stderr: "",
624
+ exitCode: 1,
625
+ };
553
626
  },
554
627
  },
555
628
  {
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,
629
+ name: "moniker",
630
+ group: "Other",
631
+ summary: "Set the name OTHERS see when this agent contacts them (like caller ID).",
632
+ help: "Usage: cello moniker set <name> [--agent <agent>] | cello moniker clear [--agent <agent>]\n" +
633
+ " Your OUTBOUND name — what shows up on the counterparty's screen when you reach them.\n" +
634
+ " Defaults to the agent name; 'set' overrides it, 'clear' restores the default.\n" +
635
+ // MONIKER-0 AC2: the regex text is DERIVED from the shared constant, never hand-typed.
636
+ ` Name rule: 1–64 characters, letters/digits/'-'/'_' only, no spaces (regex ${MONIKER_RE.source}).\n` +
637
+ " It is a HINT, not proof — like caller ID, the receiver is shown it as self-declared and can\n" +
638
+ " override it with their own pet name for you. Never sent to the directory.\n" +
639
+ " Example: cello moniker set Wonderland_Alice --agent alice",
640
+ flags: AGENT_FLAG,
568
641
  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 });
642
+ const { agent, positional } = splitAgentFlag(args);
643
+ const [sub, name] = positional;
644
+ if (sub === "set" && name)
645
+ return legacy(await monikerSet(ctx.celloDir, name, agent));
646
+ if (sub === "clear" && !name)
647
+ return legacy(await monikerSet(ctx.celloDir, null, agent));
648
+ return { stdout: helpForSpec("moniker"), stderr: "", exitCode: 1 };
573
649
  },
574
650
  },
575
651
  {
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,
652
+ name: "telegram",
653
+ group: "Other",
654
+ summary: "Connect a Telegram bot to your daemon for notifications, status updates, etc.",
655
+ help: "Usage: cello telegram set-token <bot_token> <allowlisted_chat_id>\n" +
656
+ " Connects a Telegram bot to your daemon so you get notified there (someone reaching you,\n" +
657
+ " status updates, and more over time). Starts polling immediately.\n" +
658
+ " The chat id you give is the ONLY chat that ever receives anything.",
585
659
  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") });
660
+ const [sub, botToken, chatId] = args;
661
+ if (sub === "set-token" && botToken && chatId) {
662
+ return legacy(await telegramSetToken(ctx.celloDir, botToken, chatId));
590
663
  }
591
- catch (err) {
592
- return flagError(err);
664
+ return { stdout: "Usage: cello telegram set-token <bot_token> <allowlisted_chat_id>", stderr: "", exitCode: 1 };
665
+ },
666
+ },
667
+ {
668
+ name: "bridge",
669
+ group: "Other",
670
+ // Renamed from `install`, which read as "install CELLO itself" and hardcoded Hermes — the
671
+ // runtime is a PARAMETER. More runtimes are coming; the description must not claim otherwise.
672
+ summary: "Bridge CELLO into a third-party agent runtime (Hermes, OpenClaw, …).",
673
+ help: "Usage: cello bridge <runtime> --agent <name> [--hermes-home <path>]\n" +
674
+ " Wires the local CELLO daemon into a third-party agent runtime so that agent can use CELLO.\n" +
675
+ " Supported runtimes: hermes (more coming).\n" +
676
+ "\n" +
677
+ " hermes: scaffolds the CELLO plugin into the Hermes home (default ~/.hermes), binds\n" +
678
+ " CELLO_AGENT_NAME in its .env, and registers via 'hermes plugins enable cello' +\n" +
679
+ " 'hermes mcp add cello'. Idempotent — re-run to upgrade.\n" +
680
+ " Afterwards, restart the gateway: hermes gateway restart\n" +
681
+ "\n" +
682
+ " Example: cello bridge hermes --agent alice",
683
+ flags: [{ name: "--agent" }, { name: "--hermes-home" }],
684
+ async run(_ctx, args) {
685
+ const agentIdx = args.indexOf("--agent");
686
+ const homeIdx = args.indexOf("--hermes-home");
687
+ // Find the target positional, excluding both flags AND their values — so
688
+ // `cello bridge --agent alice hermes` still resolves target=hermes.
689
+ const target = args.find((a, i) => !a.startsWith("-") &&
690
+ !(agentIdx !== -1 && i === agentIdx + 1) &&
691
+ !(homeIdx !== -1 && i === homeIdx + 1));
692
+ if (target !== "hermes") {
693
+ return { stdout: helpForSpec("bridge"), stderr: "", exitCode: 1 };
593
694
  }
695
+ const { installHermes } = await import("./hermes/install-hermes.js");
696
+ return legacy(await installHermes({
697
+ agentName: agentIdx !== -1 ? (args[agentIdx + 1] ?? "") : "",
698
+ hermesHome: homeIdx !== -1 ? args[homeIdx + 1] : undefined,
699
+ }));
594
700
  },
595
701
  },
596
702
  ];
@@ -630,12 +736,19 @@ export function flagsFor(name) {
630
736
  return map;
631
737
  }
632
738
  /**
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`.
739
+ * DOD-ONBOARD-HELP-1 §1: render the `Commands:` table GROUPED and in logical order.
740
+ *
741
+ * Flat-and-arbitrary was the reopen: `register` appeared before `create-agent`, so the table
742
+ * literally listed step 2 above step 1. Sections in GROUP_ORDER, commands in declaration order
743
+ * within each — the order a reader would actually do them. Name column is padded across the WHOLE
744
+ * table (not per group) so the summaries line up as one column down the page.
635
745
  */
636
746
  export function renderCommandsTable() {
637
747
  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")}`;
748
+ const sections = GROUP_ORDER.map((group) => {
749
+ const rows = COMMANDS.filter((c) => c.group === group).map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
750
+ return rows.length === 0 ? null : `${group}:\n${rows.join("\n")}`;
751
+ }).filter((s) => s !== null);
752
+ return sections.join("\n\n");
640
753
  }
641
754
  //# sourceMappingURL=registry.js.map