@cello-protocol/cli 0.0.44 → 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/arg-parse.d.ts +20 -0
- package/dist/arg-parse.d.ts.map +1 -0
- package/dist/arg-parse.js +30 -0
- package/dist/arg-parse.js.map +1 -0
- package/dist/bin/cello.d.ts +8 -5
- package/dist/bin/cello.d.ts.map +1 -1
- package/dist/bin/cello.js +42 -197
- package/dist/bin/cello.js.map +1 -1
- package/dist/cli-args.d.ts +27 -19
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +49 -103
- package/dist/cli-args.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +4 -3
- package/dist/commands.js.map +1 -1
- package/dist/hermes/assets.d.ts +2 -2
- package/dist/hermes/assets.d.ts.map +1 -1
- package/dist/hermes/assets.js +11 -11
- package/dist/hermes/install-hermes.d.ts +1 -1
- package/dist/hermes/install-hermes.js +3 -3
- package/dist/hermes/install-hermes.js.map +1 -1
- package/dist/json-out.d.ts +41 -0
- package/dist/json-out.d.ts.map +1 -0
- package/dist/json-out.js +60 -0
- package/dist/json-out.js.map +1 -0
- package/dist/parity-commands.d.ts +170 -0
- package/dist/parity-commands.d.ts.map +1 -0
- package/dist/parity-commands.js +326 -0
- package/dist/parity-commands.js.map +1 -0
- package/dist/registry.d.ts +86 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +754 -0
- package/dist/registry.js.map +1 -0
- package/package.json +2 -2
package/dist/registry.js
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-CLI-PARITY-1 §4 — the command REGISTRY: the single source of truth for the `cello` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Each entry carries { name, summary, help, flags, run }. Everything derives from this one table:
|
|
5
|
+
* - dispatch (src/bin/cello.ts) — no switch to keep in sync,
|
|
6
|
+
* - the `cello --help` described `Commands:` table — rendered from each entry's `summary`
|
|
7
|
+
* (this is DOD-ONBOARD-HELP-1's remaining gap; the old surface was a pipe-delimited blob),
|
|
8
|
+
* - per-command `cello <cmd> --help` — the pre-existing help text, moved here VERBATIM,
|
|
9
|
+
* - the recognized-flag set used to reject unknown flags before dispatch.
|
|
10
|
+
*
|
|
11
|
+
* Consequence, and the reason for the refactor: the help table, per-command help, and dispatch
|
|
12
|
+
* CANNOT DRIFT, and adding a command FORCES adding its one-line summary.
|
|
13
|
+
*/
|
|
14
|
+
import { MONIKER_RE } from "@cello-protocol/protocol-types";
|
|
15
|
+
import { login, logout, status, register, createAgent, removeAgent, refreshShares, relayReceipts, sessions, settingsGet, settingsSet, monikerSet, telegramSetToken, } from "./commands.js";
|
|
16
|
+
import { splitAgentFlag } from "./arg-parse.js";
|
|
17
|
+
import { IPC_METHODS, contactAdd, contactRemove, contactList, contactSetTier, contactSetAway, listAgents, startAgent, stopAgent, useAgent, inbox, transcript, contactSetMoniker, sealedReceipt, initiate, send, receive, receiveSession, closeSession, awaitSession, } from "./parity-commands.js";
|
|
18
|
+
/** Read the whole of stdin — `cello send <id> --stdin` for message text with newlines/quotes. */
|
|
19
|
+
async function readStdin() {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for await (const chunk of process.stdin)
|
|
22
|
+
chunks.push(Buffer.from(chunk));
|
|
23
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
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
|
+
];
|
|
38
|
+
/** Parse the parity commands' shared flags out of argv (`--agent`, `--pretty`, and value flags). */
|
|
39
|
+
function parityOpts(args) {
|
|
40
|
+
const { agent, positional } = splitAgentFlag(args);
|
|
41
|
+
const pretty = positional.includes("--pretty");
|
|
42
|
+
return { agent, pretty, positional: positional.filter((a) => a !== "--pretty") };
|
|
43
|
+
}
|
|
44
|
+
/** Read `--flag <value>` out of a positional list, returning the value and the remaining args. */
|
|
45
|
+
function takeValueFlag(args, flag) {
|
|
46
|
+
const i = args.indexOf(flag);
|
|
47
|
+
if (i === -1)
|
|
48
|
+
return { rest: args };
|
|
49
|
+
return { value: args[i + 1], rest: args.filter((_, j) => j !== i && j !== i + 1) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Review F5 — a numeric flag given a NON-NUMERIC value must fail loud, never be silently dropped.
|
|
53
|
+
*
|
|
54
|
+
* `--since-seq abc` used to parse to undefined, which `defined()` then removed, which turned a
|
|
55
|
+
* stateless CATCH-UP into a 30-second BLOCKING live wait that returns `content: null` — so a script
|
|
56
|
+
* asking "what did I miss?" was answered "nothing new" to a question it never asked. Silently
|
|
57
|
+
* changing the meaning of a command is worse than refusing it.
|
|
58
|
+
*
|
|
59
|
+
* Throws a BadFlagValue, which run() converts to a structured error + exit 1.
|
|
60
|
+
*/
|
|
61
|
+
class BadFlagValue extends Error {
|
|
62
|
+
flag;
|
|
63
|
+
value;
|
|
64
|
+
constructor(flag, value) {
|
|
65
|
+
super(`${flag} expects a number, got '${value}'`);
|
|
66
|
+
this.flag = flag;
|
|
67
|
+
this.value = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function numberOrUndefined(raw, flag) {
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const n = Number(raw);
|
|
74
|
+
if (!Number.isFinite(n))
|
|
75
|
+
throw new BadFlagValue(flag, raw);
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
/** Turn a BadFlagValue into the §3 structured error; rethrow anything else. */
|
|
79
|
+
function flagError(err) {
|
|
80
|
+
if (err instanceof BadFlagValue) {
|
|
81
|
+
return {
|
|
82
|
+
stdout: "",
|
|
83
|
+
stderr: JSON.stringify({
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: "invalid_flag_value",
|
|
86
|
+
flag: err.flag,
|
|
87
|
+
value: err.value,
|
|
88
|
+
guidance: `${err.flag} expects a number. Got '${err.value}'. The command was NOT run — a dropped flag would have silently changed what it does.`,
|
|
89
|
+
}),
|
|
90
|
+
exitCode: 1,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
/** Adapt a legacy CommandResult (single `output` string, always stdout) to the CliOutput triple. */
|
|
96
|
+
function legacy(result) {
|
|
97
|
+
return { stdout: result.output, stderr: "", exitCode: result.exitCode };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `--agent <name>` — recognized by every agent-scoped command.
|
|
101
|
+
*
|
|
102
|
+
* `consumesValue: false` is deliberate and is what checkArgs PARITY requires: the pre-existing
|
|
103
|
+
* checkArgs never skipped --agent's value (only `--limit` did), and flipping this to true would
|
|
104
|
+
* change `cello contacts --agent --bogus` from a fail-loud unknown_flag into a silently
|
|
105
|
+
* accepted agent literally named "--bogus". The value is claimed by splitAgentFlag (arg-parse.ts),
|
|
106
|
+
* which owns --agent parsing; checkArgs only needs to know the FLAG is legal. Same for install's
|
|
107
|
+
* --agent / --hermes-home below.
|
|
108
|
+
*/
|
|
109
|
+
const AGENT_FLAG = [{ name: "--agent", consumesValue: false }];
|
|
110
|
+
/** Agent-scoped parity commands also take --pretty (granted automatically via `jsonOut`). */
|
|
111
|
+
const AGENT_AND_TIMEOUT = [
|
|
112
|
+
{ name: "--agent", consumesValue: false },
|
|
113
|
+
{ name: "--timeout-ms", consumesValue: true },
|
|
114
|
+
];
|
|
115
|
+
export const COMMANDS = [
|
|
116
|
+
// ═══ Setup — get a working agent, in the order you actually do it ═══════════════════════════
|
|
117
|
+
{
|
|
118
|
+
name: "login",
|
|
119
|
+
group: "Setup",
|
|
120
|
+
summary: "Start the local CELLO daemon and bring your agents online.",
|
|
121
|
+
help: "Usage: cello login — start the daemon (or connect to an existing one).",
|
|
122
|
+
async run(ctx) {
|
|
123
|
+
return legacy(await login(ctx.celloDir, ctx.daemonBin, ctx.logger));
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "logout",
|
|
128
|
+
group: "Setup",
|
|
129
|
+
summary: "Stop the daemon. Waits until it has actually exited.",
|
|
130
|
+
help: "Usage: cello logout — send shutdown to the running daemon.",
|
|
131
|
+
async run(ctx) {
|
|
132
|
+
// DOD-LOGOUT-WAIT-1: logout WAITS for the daemon to actually die before claiming
|
|
133
|
+
// "Daemon stopped." — the immediate progress line tells the operator the command
|
|
134
|
+
// activated and the short pause is expected.
|
|
135
|
+
return legacy(await logout(ctx.celloDir, ctx.onProgress));
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "status",
|
|
140
|
+
group: "Setup",
|
|
141
|
+
summary: "Show whether the daemon is running and which agents are online.",
|
|
142
|
+
help: "Usage: cello status — query the daemon and print the structured status JSON.",
|
|
143
|
+
async run(ctx) {
|
|
144
|
+
return legacy(await status(ctx.celloDir));
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
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" +
|
|
165
|
+
" The token is a single-use pre-authorization ticket from the CELLO Operations Agent on Telegram, format 'CELLO-' + 33 characters, valid 24h.\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" +
|
|
168
|
+
" Quoting is only needed if a value contains spaces (agent names and tokens never do).",
|
|
169
|
+
async run(ctx, args) {
|
|
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.
|
|
172
|
+
const agent = args[0] ?? "";
|
|
173
|
+
const preAuthToken = args[1] ?? process.env.CELLO_PREAUTH_TOKEN ?? "";
|
|
174
|
+
const phoneStub = args[2] ?? "";
|
|
175
|
+
return legacy(await register(ctx.celloDir, agent, preAuthToken, phoneStub));
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
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.",
|
|
183
|
+
async run(ctx, args) {
|
|
184
|
+
return legacy(await removeAgent(ctx.celloDir, args[0] ?? ""));
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
// ═══ Agents — day-to-day control of who is online and who you are acting as ═════════════════
|
|
188
|
+
{
|
|
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,
|
|
196
|
+
async run(ctx, args) {
|
|
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 });
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
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.",
|
|
257
|
+
async run(ctx, args) {
|
|
258
|
+
return legacy(await refreshShares(ctx.celloDir, args[0] ?? ""));
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
// ═══ Messaging — the conversation itself ════════════════════════════════════════════════════
|
|
262
|
+
{
|
|
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,
|
|
272
|
+
async run(ctx, args) {
|
|
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: "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"],
|
|
315
|
+
jsonOut: true,
|
|
316
|
+
async run(ctx, args) {
|
|
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
|
+
}
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
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"],
|
|
345
|
+
jsonOut: true,
|
|
346
|
+
async run(ctx, args) {
|
|
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 });
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
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,
|
|
371
|
+
jsonOut: true,
|
|
372
|
+
async run(ctx, args) {
|
|
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 });
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
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,
|
|
395
|
+
jsonOut: true,
|
|
396
|
+
async run(ctx, args) {
|
|
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
|
+
}
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "inbox",
|
|
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.",
|
|
421
|
+
flags: [
|
|
422
|
+
{ name: "--agent", consumesValue: false },
|
|
423
|
+
{ name: "--scope", consumesValue: true },
|
|
424
|
+
],
|
|
425
|
+
ipcMethod: IPC_METHODS.inbox,
|
|
426
|
+
jsonOut: true,
|
|
427
|
+
async run(ctx, args) {
|
|
428
|
+
const { agent, pretty, positional } = parityOpts(args);
|
|
429
|
+
const { value } = takeValueFlag(positional, "--scope");
|
|
430
|
+
// Review F6: an UNRECOGNIZED scope must not silently become the default. A typo'd
|
|
431
|
+
// `--scope all` (e.g. "al") would have answered with `current`'s data and exit 0 — the
|
|
432
|
+
// operator reads "no notifications" while another agent's inbox is full.
|
|
433
|
+
if (value !== undefined && value !== "all" && value !== "current") {
|
|
434
|
+
return {
|
|
435
|
+
stdout: "",
|
|
436
|
+
stderr: JSON.stringify({
|
|
437
|
+
ok: false,
|
|
438
|
+
reason: "invalid_flag_value",
|
|
439
|
+
flag: "--scope",
|
|
440
|
+
value,
|
|
441
|
+
guidance: "--scope must be 'current' or 'all'. The command was NOT run — answering a different question than the one asked is worse than refusing.",
|
|
442
|
+
}),
|
|
443
|
+
exitCode: 1,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return inbox(ctx.celloDir, { agent, pretty, scope: value });
|
|
447
|
+
},
|
|
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
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: "transcript",
|
|
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.",
|
|
489
|
+
flags: AGENT_FLAG,
|
|
490
|
+
ipcMethod: IPC_METHODS.transcript,
|
|
491
|
+
jsonOut: true,
|
|
492
|
+
async run(ctx, args) {
|
|
493
|
+
const { agent, pretty, positional } = parityOpts(args);
|
|
494
|
+
return transcript(ctx.celloDir, positional[0] ?? "", { agent, pretty });
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "sealed-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.",
|
|
502
|
+
help: "Usage: cello sealed-receipt <session-id> [--agent <name>] [--pretty] — the NOTARIZED receipt.\n" +
|
|
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.",
|
|
509
|
+
flags: AGENT_FLAG,
|
|
510
|
+
ipcMethod: IPC_METHODS["sealed-receipt"],
|
|
511
|
+
jsonOut: true,
|
|
512
|
+
async run(ctx, args) {
|
|
513
|
+
const { agent, pretty, positional } = parityOpts(args);
|
|
514
|
+
return sealedReceipt(ctx.celloDir, positional[0] ?? "", { agent, pretty });
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
{
|
|
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'.",
|
|
532
|
+
async run(ctx, args) {
|
|
533
|
+
return legacy(await relayReceipts(ctx.celloDir, args[0] ?? ""));
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
// ═══ Contacts — the address book (plural) and one contact (singular) ════════════════════════
|
|
537
|
+
{
|
|
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,
|
|
546
|
+
jsonOut: true,
|
|
547
|
+
ipcMethod: IPC_METHODS.contacts,
|
|
548
|
+
async run(ctx, args) {
|
|
549
|
+
const { agent, pretty } = parityOpts(args);
|
|
550
|
+
return contactList(ctx.celloDir, { agent, pretty });
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
{
|
|
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
|
|
574
|
+
async run(ctx, args) {
|
|
575
|
+
const { agent, pretty, positional } = parityOpts(args);
|
|
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);
|
|
589
|
+
}
|
|
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);
|
|
599
|
+
}
|
|
600
|
+
return { stdout: helpForSpec("contact"), stderr: "", exitCode: 1 };
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
// ═══ Other ══════════════════════════════════════════════════════════════════════════════════
|
|
604
|
+
{
|
|
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,
|
|
613
|
+
async run(ctx, args) {
|
|
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));
|
|
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
|
+
};
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
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,
|
|
641
|
+
async run(ctx, args) {
|
|
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 };
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
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.",
|
|
659
|
+
async run(ctx, args) {
|
|
660
|
+
const [sub, botToken, chatId] = args;
|
|
661
|
+
if (sub === "set-token" && botToken && chatId) {
|
|
662
|
+
return legacy(await telegramSetToken(ctx.celloDir, botToken, chatId));
|
|
663
|
+
}
|
|
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 };
|
|
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
|
+
}));
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
export function commandNames() {
|
|
704
|
+
return COMMANDS.map((c) => c.name);
|
|
705
|
+
}
|
|
706
|
+
export function findCommand(name) {
|
|
707
|
+
return COMMANDS.find((c) => c.name === name);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Internal: a spec's help by name, for commands that print their own usage on bad input.
|
|
711
|
+
*
|
|
712
|
+
* Called with hardcoded names that MUST resolve, so a miss is a programmer error (a rename typo),
|
|
713
|
+
* not a runtime condition. Throwing beats the old `?? ""` default, which would have printed an
|
|
714
|
+
* EMPTY string with exit 1 — silently swallowing the operator's only guidance, and doing it in the
|
|
715
|
+
* one code path whose entire job is to explain what went wrong.
|
|
716
|
+
*/
|
|
717
|
+
function helpForSpec(name) {
|
|
718
|
+
const spec = findCommand(name);
|
|
719
|
+
if (!spec)
|
|
720
|
+
throw new Error(`registry: no command '${name}' (a hardcoded help lookup is out of sync)`);
|
|
721
|
+
return spec.help;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The flags a command recognizes, derived from its registry entry. `--pretty` is granted
|
|
725
|
+
* automatically to every command honoring the §3 JSON contract, so it can never be forgotten.
|
|
726
|
+
*/
|
|
727
|
+
export function flagsFor(name) {
|
|
728
|
+
const spec = findCommand(name);
|
|
729
|
+
const map = new Map();
|
|
730
|
+
if (!spec)
|
|
731
|
+
return map;
|
|
732
|
+
for (const f of spec.flags ?? [])
|
|
733
|
+
map.set(f.name, f);
|
|
734
|
+
if (spec.jsonOut)
|
|
735
|
+
map.set("--pretty", { name: "--pretty" });
|
|
736
|
+
return map;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
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.
|
|
745
|
+
*/
|
|
746
|
+
export function renderCommandsTable() {
|
|
747
|
+
const width = Math.max(...COMMANDS.map((c) => c.name.length));
|
|
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");
|
|
753
|
+
}
|
|
754
|
+
//# sourceMappingURL=registry.js.map
|