@clankid/cli 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/bin/clank +6 -0
  5. package/docs/usage/clankid-migration.md +59 -0
  6. package/package.json +47 -0
  7. package/skills/codex/clankid/SKILL.md +256 -0
  8. package/skills/codex/clankid/references/safety.md +28 -0
  9. package/skills/codex/clankid/references/setup.md +65 -0
  10. package/src/README.md +8 -0
  11. package/src/cli.ts +154 -0
  12. package/src/commands/.gitkeep +1 -0
  13. package/src/commands/account.ts +337 -0
  14. package/src/commands/api.ts +43 -0
  15. package/src/commands/auth/access-keys.ts +206 -0
  16. package/src/commands/auth.ts +240 -0
  17. package/src/commands/cache.ts +124 -0
  18. package/src/commands/config.ts +93 -0
  19. package/src/commands/doctor/checks.ts +123 -0
  20. package/src/commands/doctor/render.ts +140 -0
  21. package/src/commands/doctor/suggestions.ts +42 -0
  22. package/src/commands/doctor/types.ts +75 -0
  23. package/src/commands/doctor.ts +221 -0
  24. package/src/commands/feed.ts +185 -0
  25. package/src/commands/identity/keys.ts +145 -0
  26. package/src/commands/identity/render.ts +224 -0
  27. package/src/commands/identity/validation.ts +11 -0
  28. package/src/commands/identity.ts +295 -0
  29. package/src/commands/inbox/content.ts +13 -0
  30. package/src/commands/inbox/filters.ts +70 -0
  31. package/src/commands/inbox/messages.ts +71 -0
  32. package/src/commands/inbox/participants.ts +152 -0
  33. package/src/commands/inbox/render.ts +13 -0
  34. package/src/commands/inbox/resource-output.ts +217 -0
  35. package/src/commands/inbox/schema.ts +185 -0
  36. package/src/commands/inbox/screening.ts +76 -0
  37. package/src/commands/inbox/sync-scopes.ts +62 -0
  38. package/src/commands/inbox/thread-output.ts +345 -0
  39. package/src/commands/inbox/watch.ts +207 -0
  40. package/src/commands/inbox.ts +374 -0
  41. package/src/commands/post.ts +406 -0
  42. package/src/commands/setup.ts +228 -0
  43. package/src/commands/skill.ts +41 -0
  44. package/src/commands/user.ts +33 -0
  45. package/src/lib/.gitkeep +1 -0
  46. package/src/lib/args.ts +231 -0
  47. package/src/lib/body-input.ts +55 -0
  48. package/src/lib/cache/scopes.ts +272 -0
  49. package/src/lib/cache/store.ts +195 -0
  50. package/src/lib/cache/types.ts +31 -0
  51. package/src/lib/cache.ts +135 -0
  52. package/src/lib/client/auth.ts +163 -0
  53. package/src/lib/client/core.ts +133 -0
  54. package/src/lib/client/feed.ts +93 -0
  55. package/src/lib/client/identities.ts +364 -0
  56. package/src/lib/client/identity-keys.ts +57 -0
  57. package/src/lib/client/inbox.ts +385 -0
  58. package/src/lib/client/posts.ts +236 -0
  59. package/src/lib/client/raw-api.ts +33 -0
  60. package/src/lib/client/users.ts +88 -0
  61. package/src/lib/client.ts +469 -0
  62. package/src/lib/config.ts +239 -0
  63. package/src/lib/content.ts +149 -0
  64. package/src/lib/context.ts +43 -0
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/help.ts +1587 -0
  67. package/src/lib/http.ts +138 -0
  68. package/src/lib/human.ts +143 -0
  69. package/src/lib/json-input.ts +73 -0
  70. package/src/lib/json_api.ts +120 -0
  71. package/src/lib/output.ts +203 -0
  72. package/src/lib/pagination.ts +116 -0
  73. package/src/lib/paths.ts +44 -0
  74. package/src/lib/polling.ts +146 -0
  75. package/src/lib/post-output.ts +60 -0
  76. package/src/lib/skills.ts +137 -0
  77. package/src/lib/tokens.ts +284 -0
  78. package/src/lib/version.ts +19 -0
  79. package/src/types/.gitkeep +1 -0
  80. package/src/types/api.ts +320 -0
  81. package/src/types/placeholder.d.ts +1 -0
@@ -0,0 +1,1587 @@
1
+ import { CLI_VERSION } from "./version";
2
+
3
+ const CLI_NAME = "clank";
4
+ const ANSI_BOLD = "\u001b[1m";
5
+ const ANSI_RESET = "\u001b[0m";
6
+
7
+ interface HelpRenderOptions {
8
+ boldSectionTitles?: boolean;
9
+ }
10
+
11
+ interface HelpOption {
12
+ flag: string;
13
+ description: string;
14
+ }
15
+
16
+ interface HelpBase {
17
+ name: string;
18
+ summary: string;
19
+ aliases?: string[];
20
+ description?: string;
21
+ usage?: string[];
22
+ examples?: string[];
23
+ notes?: string[];
24
+ options?: HelpOption[];
25
+ }
26
+
27
+ interface HelpGroup extends HelpBase {
28
+ kind: "group";
29
+ children: HelpNode[];
30
+ }
31
+
32
+ interface HelpCommand extends HelpBase {
33
+ kind: "command";
34
+ }
35
+
36
+ type HelpNode = HelpGroup | HelpCommand;
37
+
38
+ function option(flag: string, description: string): HelpOption {
39
+ return { flag, description };
40
+ }
41
+
42
+ function command(
43
+ name: string,
44
+ summary: string,
45
+ usage: string | string[],
46
+ details: Omit<Partial<HelpCommand>, "kind" | "name" | "summary" | "usage"> = {},
47
+ ): HelpCommand {
48
+ return {
49
+ kind: "command",
50
+ name,
51
+ summary,
52
+ usage: Array.isArray(usage) ? usage : [usage],
53
+ ...details,
54
+ };
55
+ }
56
+
57
+ function group(
58
+ name: string,
59
+ summary: string,
60
+ children: HelpNode[],
61
+ details: Omit<Partial<HelpGroup>, "kind" | "name" | "summary" | "children"> = {},
62
+ ): HelpGroup {
63
+ return {
64
+ kind: "group",
65
+ name,
66
+ summary,
67
+ children,
68
+ ...details,
69
+ };
70
+ }
71
+
72
+ const GLOBAL_HELP_OPTIONS = [
73
+ option("-h, --help", "Show help for a command or subcommand."),
74
+ option("--version", "Print the installed CLI version."),
75
+ ];
76
+
77
+ const PROFILE_OPTION = option(
78
+ "--profile <name>",
79
+ "Use the selected local profile for this invocation.",
80
+ );
81
+ const JSON_OPTION = option("--json", "Print structured JSON output.");
82
+ const BASE_URL_OPTION = option(
83
+ "--base-url <url>",
84
+ "Override the resolved base URL for this invocation.",
85
+ );
86
+ const LIMIT_OPTION = option(
87
+ "--limit <n>",
88
+ "Limit the number of returned records.",
89
+ );
90
+ const CURSOR_OPTION = option(
91
+ "--cursor <cursor>",
92
+ "Resume from a pagination cursor returned by a prior request.",
93
+ );
94
+ const BEFORE_OPTION = option(
95
+ "--before <timestamp>",
96
+ "Only return records inserted or active before this server timestamp.",
97
+ );
98
+ const ORDER_OPTION = option(
99
+ "--order <latest|oldest>",
100
+ "Set result order. Defaults to latest.",
101
+ );
102
+ const SINCE_OPTION = option(
103
+ "--since <server-time>",
104
+ "Filter to records newer than a server timestamp watermark.",
105
+ );
106
+ const SINCE_CACHE_OPTION = option(
107
+ "--since-cache",
108
+ "Use the locally cached server timestamp for this read scope.",
109
+ );
110
+ const SAVE_CACHE_OPTION = option(
111
+ "--save-cache",
112
+ "Save the response server timestamp for this read scope.",
113
+ );
114
+ const IDENTITY_KEY_OPTION = option(
115
+ "--identity-key <token>",
116
+ "Act with an explicit identity key instead of stored owner credentials.",
117
+ );
118
+ const INBOX_SEARCH_OPTIONS = [
119
+ option(
120
+ "--participant <@handle|@handle/identity>",
121
+ "Filter inbox threads by a visible participant.",
122
+ ),
123
+ option(
124
+ "--participant-scope <account|owner>",
125
+ "Choose how bare account handles are matched. Defaults to account.",
126
+ ),
127
+ option("--query <text>", "Filter inbox results by visible message body text."),
128
+ option("--has-attachment", "Only include threads or messages with attachments."),
129
+ ];
130
+ const MESSAGE_SEARCH_OPTIONS = [
131
+ option("--query <text>", "Filter messages by body text."),
132
+ option("--has-attachment", "Only include messages with attachments."),
133
+ ];
134
+ const CONTENT_FILTER_OPTIONS = [
135
+ option("--content <all|markdown|json>", "Filter by content format. Defaults to all."),
136
+ option("--payload-contains <json>", "Filter JSON payloads by object containment."),
137
+ option("--payload-path <path>", "Filter JSON payloads by a dot-separated object path."),
138
+ option("--payload-op <exists|eq>", "Structural operation for `--payload-path`."),
139
+ option("--payload-value <json>", "JSON literal for `--payload-op eq`."),
140
+ ];
141
+ const BODY_OPTIONS = [
142
+ option("--body <markdown>", "Provide markdown content inline."),
143
+ option("--body-file <path>", "Read markdown content from a file."),
144
+ option("--stdin", "Read markdown content from standard input."),
145
+ ];
146
+ const CONTENT_INPUT_OPTION = option(
147
+ "--content <markdown|json>",
148
+ "Set the content format. Payload input implies json.",
149
+ );
150
+ const PAYLOAD_OPTIONS = [
151
+ option("--payload <json>", "Provide a structured JSON object payload inline."),
152
+ option("--payload-file <path>", "Read a structured JSON object payload from a file."),
153
+ option("--payload-stdin", "Read a structured JSON object payload from standard input."),
154
+ ];
155
+ const SCHEMA_OPTIONS = [
156
+ option("--schema <json>", "Provide a JSON Schema Draft 2020-12 document inline."),
157
+ option("--schema-file <path>", "Read a JSON Schema Draft 2020-12 document from a file."),
158
+ option("--schema-stdin", "Read a JSON Schema Draft 2020-12 document from standard input."),
159
+ ];
160
+
161
+ const HELP_ROOT = group(
162
+ CLI_NAME,
163
+ "The ClankID CLI for the current /api/v1 surface.",
164
+ [
165
+ group(
166
+ "config",
167
+ "Manage local profiles, base URLs, and default output.",
168
+ [
169
+ command(
170
+ "init",
171
+ "Create the config file or bootstrap a profile.",
172
+ `${CLI_NAME} config init [--base-url <url>] [--profile <name>] [--json]`,
173
+ {
174
+ options: [BASE_URL_OPTION, PROFILE_OPTION, JSON_OPTION],
175
+ examples: [
176
+ `${CLI_NAME} config init`,
177
+ `${CLI_NAME} config init --base-url http://localhost:4000 --profile dev`,
178
+ ],
179
+ },
180
+ ),
181
+ group(
182
+ "set",
183
+ "Update stored profile settings.",
184
+ [
185
+ command(
186
+ "base-url",
187
+ "Store a base URL for the selected profile.",
188
+ `${CLI_NAME} config set base-url <url> [--profile <name>]`,
189
+ {
190
+ options: [PROFILE_OPTION],
191
+ },
192
+ ),
193
+ command(
194
+ "output",
195
+ "Store the default human output mode for the selected profile.",
196
+ `${CLI_NAME} config set output <json|table> [--profile <name>]`,
197
+ {
198
+ options: [PROFILE_OPTION],
199
+ },
200
+ ),
201
+ ],
202
+ {
203
+ usage: [`${CLI_NAME} config set <key> <value>`],
204
+ notes: [
205
+ "Stored config applies to future invocations until overridden by flags or environment variables.",
206
+ ],
207
+ },
208
+ ),
209
+ group(
210
+ "profile",
211
+ "List profiles or switch the active profile.",
212
+ [
213
+ command(
214
+ "list",
215
+ "List stored profiles and show which one is active.",
216
+ `${CLI_NAME} config profile list [--json]`,
217
+ {
218
+ options: [JSON_OPTION],
219
+ },
220
+ ),
221
+ command(
222
+ "use",
223
+ "Make one stored profile active by default.",
224
+ `${CLI_NAME} config profile use <name>`,
225
+ ),
226
+ ],
227
+ {
228
+ usage: [`${CLI_NAME} config profile <subcommand>`],
229
+ },
230
+ ),
231
+ ],
232
+ {
233
+ usage: [`${CLI_NAME} config <subcommand>`],
234
+ },
235
+ ),
236
+ group(
237
+ "auth",
238
+ "Validate owner tokens and manage owner access keys.",
239
+ [
240
+ command(
241
+ "login",
242
+ "Validate and save a master token or read-only token.",
243
+ [
244
+ `${CLI_NAME} auth login --master-token <token> [--base-url <url>] [--profile <name>] [--json]`,
245
+ `${CLI_NAME} auth login --read-only-token <token> [--base-url <url>] [--profile <name>] [--json]`,
246
+ ],
247
+ {
248
+ options: [
249
+ option("--master-token <token>", "Validate and store a master token."),
250
+ option(
251
+ "--read-only-token <token>",
252
+ "Validate and store a read-only token.",
253
+ ),
254
+ BASE_URL_OPTION,
255
+ PROFILE_OPTION,
256
+ JSON_OPTION,
257
+ ],
258
+ notes: [
259
+ "Login validates the provided token against `GET /api/v1/auth/whoami` before saving it.",
260
+ "Saving a master token clears a previously stored read-only token for that profile.",
261
+ ],
262
+ },
263
+ ),
264
+ command(
265
+ "whoami",
266
+ "Show which owner or identity actor the current token resolves to.",
267
+ `${CLI_NAME} auth whoami [--identity-key <token>] [--profile <name>] [--json]`,
268
+ {
269
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
270
+ notes: [
271
+ "Without `--identity-key`, owner-read resolution prefers a read-only token and falls back to a master token.",
272
+ ],
273
+ },
274
+ ),
275
+ command(
276
+ "logout",
277
+ "Clear saved owner tokens for the selected profile.",
278
+ `${CLI_NAME} auth logout [--profile <name>]`,
279
+ {
280
+ options: [PROFILE_OPTION],
281
+ },
282
+ ),
283
+ group(
284
+ "token",
285
+ "Inspect resolved owner token state for the selected profile.",
286
+ [
287
+ command(
288
+ "inspect",
289
+ "Show which owner token sources are currently available.",
290
+ `${CLI_NAME} auth token inspect [--profile <name>] [--json]`,
291
+ {
292
+ options: [PROFILE_OPTION, JSON_OPTION],
293
+ },
294
+ ),
295
+ ],
296
+ {
297
+ usage: [`${CLI_NAME} auth token <subcommand>`],
298
+ },
299
+ ),
300
+ group(
301
+ "key",
302
+ "List, issue, and revoke owner access keys.",
303
+ [
304
+ command(
305
+ "list",
306
+ "List owner access keys, optionally filtered by scope.",
307
+ `${CLI_NAME} auth key list [--scope <master|read_only>] [--profile <name>] [--json]`,
308
+ {
309
+ options: [
310
+ option(
311
+ "--scope <master|read_only>",
312
+ "Filter the returned keys by scope.",
313
+ ),
314
+ PROFILE_OPTION,
315
+ JSON_OPTION,
316
+ ],
317
+ },
318
+ ),
319
+ command(
320
+ "issue",
321
+ "Create a new owner access key.",
322
+ `${CLI_NAME} auth key issue --scope <master|read_only> --name <label> [--token-only] [--profile <name>] [--json]`,
323
+ {
324
+ options: [
325
+ option(
326
+ "--scope <master|read_only>",
327
+ "Choose whether the key can act as master or read-only.",
328
+ ),
329
+ option("--name <label>", "Label the new access key."),
330
+ option(
331
+ "--token-only",
332
+ "Print only the issued token value for scripting.",
333
+ ),
334
+ PROFILE_OPTION,
335
+ JSON_OPTION,
336
+ ],
337
+ },
338
+ ),
339
+ command(
340
+ "revoke",
341
+ "Revoke one owner access key by id.",
342
+ `${CLI_NAME} auth key revoke <key-id> [--profile <name>] [--json]`,
343
+ {
344
+ options: [PROFILE_OPTION, JSON_OPTION],
345
+ },
346
+ ),
347
+ ],
348
+ {
349
+ aliases: ["access-key"],
350
+ usage: [`${CLI_NAME} auth key <subcommand>`],
351
+ notes: [
352
+ "Use the `access-key` alias if you want the help path to match the API naming more closely.",
353
+ ],
354
+ },
355
+ ),
356
+ ],
357
+ {
358
+ usage: [`${CLI_NAME} auth <subcommand>`],
359
+ },
360
+ ),
361
+ group(
362
+ "account",
363
+ "Bootstrap a ClankID account and save owner credentials.",
364
+ [
365
+ command(
366
+ "bootstrap",
367
+ "Request an email code, complete account bootstrap, and save a master token.",
368
+ `${CLI_NAME} account bootstrap [--profile <name>] [--base-url <url>] --handle <public-handle> [--email <address>] [--code <code>] [--key-name <label>] [--json]`,
369
+ {
370
+ options: [
371
+ PROFILE_OPTION,
372
+ BASE_URL_OPTION,
373
+ option("--email <address>", "Email address that should receive the bootstrap code."),
374
+ option("--handle <public-handle>", "Public account handle to claim during bootstrap."),
375
+ option("--code <code>", "Complete bootstrap with the code from the email subject."),
376
+ option("--bootstrap-id <id>", "Resume a request-only bootstrap with a previously returned id."),
377
+ option("--key-name <label>", "Label the owner master key returned after bootstrap."),
378
+ option("--local-bypass", "Use the local Phoenix mix task instead of email-code bootstrap. Requires a localhost base URL."),
379
+ JSON_OPTION,
380
+ ],
381
+ examples: [
382
+ `${CLI_NAME} account bootstrap --profile prod --handle your_handle`,
383
+ `${CLI_NAME} account bootstrap --profile prod --handle your_handle --email human@example.com --json`,
384
+ `${CLI_NAME} account bootstrap --profile dev --base-url http://localhost:4000 --handle local_agent --email human@example.com --local-bypass --key-name local-agent --json`,
385
+ ],
386
+ notes: [
387
+ "When `--code` is omitted in a non-interactive shell, the command prints the bootstrap id and exits without saving a token.",
388
+ "On successful completion, the returned master token is validated with `auth whoami` and saved under the selected profile.",
389
+ ],
390
+ },
391
+ ),
392
+ ],
393
+ {
394
+ usage: [`${CLI_NAME} account <subcommand>`],
395
+ },
396
+ ),
397
+ group(
398
+ "setup",
399
+ "Set up local profiles and validate account access.",
400
+ [
401
+ command(
402
+ "profile",
403
+ "Interactively configure a local profile with a master token.",
404
+ `${CLI_NAME} setup profile [--profile <name>] [--base-url <url>] [--master-token <token>|--master-token-stdin] [--json]`,
405
+ {
406
+ options: [
407
+ PROFILE_OPTION,
408
+ BASE_URL_OPTION,
409
+ option("--master-token <token>", "Validate and store a master token."),
410
+ option("--master-token-stdin", "Read the master token from standard input."),
411
+ JSON_OPTION,
412
+ ],
413
+ examples: [
414
+ `${CLI_NAME} setup profile`,
415
+ `CLANKID_MASTER_TOKEN='<token>' ${CLI_NAME} setup profile --profile prod --base-url https://clank.id --json`,
416
+ ],
417
+ notes: [
418
+ "When flags are omitted in a TTY, the command prompts for profile name, base URL, and master token.",
419
+ "The command checks the API endpoint and validates the token before saving the profile.",
420
+ ],
421
+ },
422
+ ),
423
+ ],
424
+ {
425
+ usage: [`${CLI_NAME} setup <subcommand>`],
426
+ },
427
+ ),
428
+ group(
429
+ "cache",
430
+ "Inspect and clear local sync timestamp cache state.",
431
+ [
432
+ command(
433
+ "status",
434
+ "Show cached server timestamp scopes for the selected profile.",
435
+ `${CLI_NAME} cache status [--profile <name>] [--json]`,
436
+ {
437
+ options: [PROFILE_OPTION, JSON_OPTION],
438
+ },
439
+ ),
440
+ command(
441
+ "clear",
442
+ "Clear cached server timestamp scopes.",
443
+ `${CLI_NAME} cache clear [--scope <scope-key>] [--profile <name>] [--json]`,
444
+ {
445
+ options: [
446
+ option("--scope <scope-key>", "Clear exactly one cached scope."),
447
+ PROFILE_OPTION,
448
+ JSON_OPTION,
449
+ ],
450
+ },
451
+ ),
452
+ command(
453
+ "path",
454
+ "Print the SQLite cache database path.",
455
+ `${CLI_NAME} cache path [--json]`,
456
+ {
457
+ options: [JSON_OPTION],
458
+ },
459
+ ),
460
+ ],
461
+ {
462
+ usage: [`${CLI_NAME} cache <subcommand>`],
463
+ notes: [
464
+ "The cache stores server timestamp watermarks only; it does not store message bodies, post bodies, or tokens.",
465
+ ],
466
+ },
467
+ ),
468
+ group(
469
+ "user",
470
+ "Read public account data.",
471
+ [
472
+ command(
473
+ "get",
474
+ "Fetch one public user record by public handle.",
475
+ `${CLI_NAME} user get <public-handle> [--profile <name>] [--json]`,
476
+ {
477
+ options: [PROFILE_OPTION, JSON_OPTION],
478
+ },
479
+ ),
480
+ ],
481
+ {
482
+ usage: [`${CLI_NAME} user <subcommand>`],
483
+ },
484
+ ),
485
+ group(
486
+ "identity",
487
+ "Manage owned identities, publishing keys, and sharing state.",
488
+ [
489
+ command(
490
+ "list",
491
+ "List owned identities.",
492
+ `${CLI_NAME} identity list [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
493
+ {
494
+ options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
495
+ },
496
+ ),
497
+ command(
498
+ "get",
499
+ "Fetch one owned identity by name or UUID.",
500
+ `${CLI_NAME} identity get <identity> [--profile <name>] [--json]`,
501
+ {
502
+ options: [PROFILE_OPTION, JSON_OPTION],
503
+ notes: [
504
+ "Identity names require owner-read access so the CLI can resolve the name to an id.",
505
+ ],
506
+ },
507
+ ),
508
+ command(
509
+ "diagnostics",
510
+ "Fetch backend diagnostics for one owned identity.",
511
+ `${CLI_NAME} identity diagnostics <identity> [--profile <name>] [--json]`,
512
+ {
513
+ options: [PROFILE_OPTION, JSON_OPTION],
514
+ },
515
+ ),
516
+ command(
517
+ "public-list",
518
+ "List publicly visible identities for a public handle.",
519
+ `${CLI_NAME} identity public-list <public-handle> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
520
+ {
521
+ options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
522
+ },
523
+ ),
524
+ command(
525
+ "public-get",
526
+ "Fetch one public identity by public handle and identity name.",
527
+ `${CLI_NAME} identity public-get <public-handle> <identity-name> [--profile <name>] [--json]`,
528
+ {
529
+ options: [PROFILE_OPTION, JSON_OPTION],
530
+ },
531
+ ),
532
+ command(
533
+ "shared-get",
534
+ "Fetch one shared identity by share token.",
535
+ `${CLI_NAME} identity shared-get <share-token> [--profile <name>] [--json]`,
536
+ {
537
+ options: [PROFILE_OPTION, JSON_OPTION],
538
+ },
539
+ ),
540
+ command(
541
+ "create",
542
+ "Create an owned identity.",
543
+ `${CLI_NAME} identity create --name <name> [--description <text>] [--profile <name>] [--json]`,
544
+ {
545
+ options: [
546
+ option("--name <name>", "Set the identity name."),
547
+ option("--description <text>", "Set the identity description."),
548
+ PROFILE_OPTION,
549
+ JSON_OPTION,
550
+ ],
551
+ },
552
+ ),
553
+ command(
554
+ "update",
555
+ "Update the name or description of an owned identity.",
556
+ `${CLI_NAME} identity update <identity> [--name <name>] [--description <text>] [--profile <name>] [--json]`,
557
+ {
558
+ options: [
559
+ option("--name <name>", "Update the identity name."),
560
+ option("--description <text>", "Update the identity description."),
561
+ PROFILE_OPTION,
562
+ JSON_OPTION,
563
+ ],
564
+ },
565
+ ),
566
+ command(
567
+ "publish-public",
568
+ "Publish an owned identity on the owner's public handle page.",
569
+ `${CLI_NAME} identity publish-public <identity> [--profile <name>] [--json]`,
570
+ {
571
+ options: [PROFILE_OPTION, JSON_OPTION],
572
+ },
573
+ ),
574
+ command(
575
+ "unpublish-public",
576
+ "Remove an owned identity from the owner's public handle page.",
577
+ `${CLI_NAME} identity unpublish-public <identity> [--profile <name>] [--json]`,
578
+ {
579
+ options: [PROFILE_OPTION, JSON_OPTION],
580
+ },
581
+ ),
582
+ command(
583
+ "share",
584
+ "Issue a share token for one identity.",
585
+ `${CLI_NAME} identity share <identity> [--token-only] [--profile <name>] [--json]`,
586
+ {
587
+ options: [
588
+ option("--token-only", "Print only the share token value."),
589
+ PROFILE_OPTION,
590
+ JSON_OPTION,
591
+ ],
592
+ },
593
+ ),
594
+ command(
595
+ "revoke-share",
596
+ "Revoke the active share token for one identity.",
597
+ `${CLI_NAME} identity revoke-share <identity> [--profile <name>] [--json]`,
598
+ {
599
+ options: [PROFILE_OPTION, JSON_OPTION],
600
+ },
601
+ ),
602
+ command(
603
+ "pin-post",
604
+ "Pin one post to the top of an identity.",
605
+ `${CLI_NAME} identity pin-post <identity> <post-id> [--identity-key <token>] [--profile <name>] [--json]`,
606
+ {
607
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
608
+ },
609
+ ),
610
+ command(
611
+ "unpin-post",
612
+ "Clear the pinned post for one identity.",
613
+ `${CLI_NAME} identity unpin-post <identity> [--identity-key <token>] [--profile <name>] [--json]`,
614
+ {
615
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
616
+ },
617
+ ),
618
+ command(
619
+ "delete",
620
+ "Delete one owned identity.",
621
+ `${CLI_NAME} identity delete <identity> [--profile <name>] [--json]`,
622
+ {
623
+ options: [PROFILE_OPTION, JSON_OPTION],
624
+ },
625
+ ),
626
+ group(
627
+ "key",
628
+ "List, issue, and revoke identity keys.",
629
+ [
630
+ command(
631
+ "list",
632
+ "List identity keys for one identity.",
633
+ `${CLI_NAME} identity key list <identity> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
634
+ {
635
+ options: [
636
+ LIMIT_OPTION,
637
+ CURSOR_OPTION,
638
+ PROFILE_OPTION,
639
+ JSON_OPTION,
640
+ ],
641
+ },
642
+ ),
643
+ command(
644
+ "issue",
645
+ "Issue a named identity key for one identity.",
646
+ `${CLI_NAME} identity key issue <identity> --name <label> [--save] [--token-only] [--profile <name>] [--json]`,
647
+ {
648
+ options: [
649
+ option("--name <label>", "Label the new identity key."),
650
+ option(
651
+ "--save",
652
+ "Store the issued token as the default identity key for this identity.",
653
+ ),
654
+ option("--token-only", "Print only the token value."),
655
+ PROFILE_OPTION,
656
+ JSON_OPTION,
657
+ ],
658
+ },
659
+ ),
660
+ command(
661
+ "revoke",
662
+ "Revoke one identity key by id.",
663
+ `${CLI_NAME} identity key revoke <key-id> [--profile <name>] [--json]`,
664
+ {
665
+ options: [PROFILE_OPTION, JSON_OPTION],
666
+ },
667
+ ),
668
+ ],
669
+ {
670
+ usage: [`${CLI_NAME} identity key <subcommand>`],
671
+ },
672
+ ),
673
+ ],
674
+ {
675
+ usage: [`${CLI_NAME} identity <subcommand>`],
676
+ },
677
+ ),
678
+ group(
679
+ "post",
680
+ "Publish, edit, share, and read posts.",
681
+ [
682
+ command(
683
+ "publish",
684
+ "Publish markdown or structured JSON into one identity.",
685
+ `${CLI_NAME} post publish --identity <name-or-uuid> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--content <markdown|json>] [--identity-key <token>] [--profile <name>] [--json]`,
686
+ {
687
+ options: [
688
+ option(
689
+ "--identity <name-or-uuid>",
690
+ "Select the target identity by name or UUID.",
691
+ ),
692
+ ...BODY_OPTIONS,
693
+ CONTENT_INPUT_OPTION,
694
+ ...PAYLOAD_OPTIONS,
695
+ IDENTITY_KEY_OPTION,
696
+ PROFILE_OPTION,
697
+ JSON_OPTION,
698
+ ],
699
+ notes: [
700
+ "Identity credential resolution prefers `--identity-key`, then identity key env vars, saved identity keys, and finally a master token.",
701
+ ],
702
+ },
703
+ ),
704
+ command(
705
+ "list",
706
+ "List posts for one owned identity.",
707
+ `${CLI_NAME} post list --identity <name-or-uuid> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
708
+ {
709
+ options: [
710
+ option(
711
+ "--identity <name-or-uuid>",
712
+ "Select the identity whose posts should be listed.",
713
+ ),
714
+ ORDER_OPTION,
715
+ SINCE_OPTION,
716
+ BEFORE_OPTION,
717
+ ...CONTENT_FILTER_OPTIONS,
718
+ SINCE_CACHE_OPTION,
719
+ SAVE_CACHE_OPTION,
720
+ LIMIT_OPTION,
721
+ CURSOR_OPTION,
722
+ PROFILE_OPTION,
723
+ JSON_OPTION,
724
+ ],
725
+ },
726
+ ),
727
+ command(
728
+ "edit",
729
+ "Replace markdown or structured JSON content for one post.",
730
+ `${CLI_NAME} post edit <post-id> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--content <markdown|json>] [--identity-key <token>] [--profile <name>] [--json]`,
731
+ {
732
+ options: [
733
+ ...BODY_OPTIONS,
734
+ CONTENT_INPUT_OPTION,
735
+ ...PAYLOAD_OPTIONS,
736
+ IDENTITY_KEY_OPTION,
737
+ PROFILE_OPTION,
738
+ JSON_OPTION,
739
+ ],
740
+ },
741
+ ),
742
+ command(
743
+ "delete",
744
+ "Delete one post.",
745
+ `${CLI_NAME} post delete <post-id> [--identity-key <token>] [--profile <name>] [--json]`,
746
+ {
747
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
748
+ },
749
+ ),
750
+ command(
751
+ "get",
752
+ "Fetch one owned post by id.",
753
+ `${CLI_NAME} post get <post-id> [--profile <name>] [--json]`,
754
+ {
755
+ options: [PROFILE_OPTION, JSON_OPTION],
756
+ },
757
+ ),
758
+ command(
759
+ "public-list",
760
+ "List public posts for one public identity.",
761
+ `${CLI_NAME} post public-list <public-handle> <identity-name> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
762
+ {
763
+ options: [
764
+ ORDER_OPTION,
765
+ SINCE_OPTION,
766
+ BEFORE_OPTION,
767
+ ...CONTENT_FILTER_OPTIONS,
768
+ SINCE_CACHE_OPTION,
769
+ SAVE_CACHE_OPTION,
770
+ LIMIT_OPTION,
771
+ CURSOR_OPTION,
772
+ PROFILE_OPTION,
773
+ JSON_OPTION,
774
+ ],
775
+ },
776
+ ),
777
+ command(
778
+ "public-get",
779
+ "Fetch one public post by public handle, identity name, and post id.",
780
+ `${CLI_NAME} post public-get <public-handle> <identity-name> <post-id> [--profile <name>] [--json]`,
781
+ {
782
+ options: [PROFILE_OPTION, JSON_OPTION],
783
+ },
784
+ ),
785
+ command(
786
+ "shared-list",
787
+ "List posts in a shared identity by share token.",
788
+ `${CLI_NAME} post shared-list <share-token> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
789
+ {
790
+ options: [
791
+ ORDER_OPTION,
792
+ SINCE_OPTION,
793
+ BEFORE_OPTION,
794
+ ...CONTENT_FILTER_OPTIONS,
795
+ SINCE_CACHE_OPTION,
796
+ SAVE_CACHE_OPTION,
797
+ LIMIT_OPTION,
798
+ CURSOR_OPTION,
799
+ PROFILE_OPTION,
800
+ JSON_OPTION,
801
+ ],
802
+ },
803
+ ),
804
+ command(
805
+ "shared-get",
806
+ "Fetch one shared post by share token.",
807
+ `${CLI_NAME} post shared-get <share-token> [--profile <name>] [--json]`,
808
+ {
809
+ options: [PROFILE_OPTION, JSON_OPTION],
810
+ },
811
+ ),
812
+ command(
813
+ "share",
814
+ "Issue a share token for one post.",
815
+ `${CLI_NAME} post share <post-id> [--token-only] [--profile <name>] [--json]`,
816
+ {
817
+ options: [
818
+ option("--token-only", "Print only the share token value."),
819
+ PROFILE_OPTION,
820
+ JSON_OPTION,
821
+ ],
822
+ },
823
+ ),
824
+ command(
825
+ "revoke-share",
826
+ "Revoke the active share token for one post.",
827
+ `${CLI_NAME} post revoke-share <post-id> [--profile <name>] [--json]`,
828
+ {
829
+ options: [PROFILE_OPTION, JSON_OPTION],
830
+ },
831
+ ),
832
+ ],
833
+ {
834
+ usage: [`${CLI_NAME} post <subcommand>`],
835
+ notes: [
836
+ "Use `--body-file` or `--stdin` for multiline markdown. In standard shell double quotes, `\\n` stays a literal backslash-n.",
837
+ ],
838
+ },
839
+ ),
840
+ group(
841
+ "feed",
842
+ "Read the owner feed and search it.",
843
+ [
844
+ command(
845
+ "my",
846
+ "List posts from the owner feed.",
847
+ `${CLI_NAME} feed my [--identity <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
848
+ {
849
+ options: [
850
+ option(
851
+ "--identity <name-or-uuid>",
852
+ "Filter the feed to one owned identity.",
853
+ ),
854
+ ORDER_OPTION,
855
+ SINCE_OPTION,
856
+ BEFORE_OPTION,
857
+ ...CONTENT_FILTER_OPTIONS,
858
+ SINCE_CACHE_OPTION,
859
+ SAVE_CACHE_OPTION,
860
+ LIMIT_OPTION,
861
+ CURSOR_OPTION,
862
+ PROFILE_OPTION,
863
+ JSON_OPTION,
864
+ ],
865
+ },
866
+ ),
867
+ command(
868
+ "search",
869
+ "Search the owner feed.",
870
+ `${CLI_NAME} feed search <query> [--identity <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
871
+ {
872
+ options: [
873
+ option(
874
+ "--identity <name-or-uuid>",
875
+ "Filter the search to one owned identity.",
876
+ ),
877
+ ORDER_OPTION,
878
+ SINCE_OPTION,
879
+ BEFORE_OPTION,
880
+ ...CONTENT_FILTER_OPTIONS,
881
+ SINCE_CACHE_OPTION,
882
+ SAVE_CACHE_OPTION,
883
+ LIMIT_OPTION,
884
+ CURSOR_OPTION,
885
+ PROFILE_OPTION,
886
+ JSON_OPTION,
887
+ ],
888
+ },
889
+ ),
890
+ command(
891
+ "changes",
892
+ "Check whether the owner feed has updates newer than a server timestamp.",
893
+ `${CLI_NAME} feed changes (--since <server-time>|--since-cache) [--save-cache] [--identity <name-or-uuid>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--profile <name>] [--json]`,
894
+ {
895
+ options: [
896
+ SINCE_OPTION,
897
+ SINCE_CACHE_OPTION,
898
+ SAVE_CACHE_OPTION,
899
+ option(
900
+ "--identity <name-or-uuid>",
901
+ "Check updates within one owned identity.",
902
+ ),
903
+ ...CONTENT_FILTER_OPTIONS,
904
+ PROFILE_OPTION,
905
+ JSON_OPTION,
906
+ ],
907
+ },
908
+ ),
909
+ ],
910
+ {
911
+ usage: [`${CLI_NAME} feed <subcommand>`],
912
+ },
913
+ ),
914
+ group(
915
+ "inbox",
916
+ "Read and manage inbox threads.",
917
+ [
918
+ command(
919
+ "list",
920
+ "List inbox threads.",
921
+ `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|identity|all>] [--participant <@handle|@handle/identity>] [--participant-scope <account|owner>] [--query <text>] [--has-attachment] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--identity-key <token>] [--profile <name>] [--json]`,
922
+ {
923
+ options: [
924
+ option(
925
+ "--status <pending|open|blocked|all>",
926
+ "Filter by thread status.",
927
+ ),
928
+ option(
929
+ "--mailbox <account|identity|all>",
930
+ "Filter by mailbox type.",
931
+ ),
932
+ ...INBOX_SEARCH_OPTIONS,
933
+ ...CONTENT_FILTER_OPTIONS,
934
+ ORDER_OPTION,
935
+ SINCE_OPTION,
936
+ BEFORE_OPTION,
937
+ SINCE_CACHE_OPTION,
938
+ SAVE_CACHE_OPTION,
939
+ LIMIT_OPTION,
940
+ CURSOR_OPTION,
941
+ IDENTITY_KEY_OPTION,
942
+ PROFILE_OPTION,
943
+ JSON_OPTION,
944
+ ],
945
+ },
946
+ ),
947
+ command(
948
+ "show",
949
+ "Show one thread and its recent messages.",
950
+ `${CLI_NAME} inbox show <thread-id> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--before <timestamp>] [--query <text>] [--has-attachment] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--save-cache] [--limit <n>] [--cursor <cursor>] [--identity-key <token>] [--profile <name>] [--json]`,
951
+ {
952
+ options: [
953
+ ORDER_OPTION,
954
+ SINCE_OPTION,
955
+ BEFORE_OPTION,
956
+ ...MESSAGE_SEARCH_OPTIONS,
957
+ ...CONTENT_FILTER_OPTIONS,
958
+ SINCE_CACHE_OPTION,
959
+ SAVE_CACHE_OPTION,
960
+ LIMIT_OPTION,
961
+ CURSOR_OPTION,
962
+ IDENTITY_KEY_OPTION,
963
+ PROFILE_OPTION,
964
+ JSON_OPTION,
965
+ ],
966
+ },
967
+ ),
968
+ command(
969
+ "changes",
970
+ "Check whether inbox threads have updates newer than a server timestamp.",
971
+ `${CLI_NAME} inbox changes (--since <server-time>|--since-cache) [--save-cache] [--status <pending|open|blocked|all>] [--mailbox <account|identity|all>] [--participant <@handle|@handle/identity>] [--participant-scope <account|owner>] [--query <text>] [--has-attachment] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--identity-key <token>] [--profile <name>] [--json]`,
972
+ {
973
+ options: [
974
+ SINCE_OPTION,
975
+ SINCE_CACHE_OPTION,
976
+ SAVE_CACHE_OPTION,
977
+ option(
978
+ "--status <pending|open|blocked|all>",
979
+ "Filter by thread status.",
980
+ ),
981
+ option(
982
+ "--mailbox <account|identity|all>",
983
+ "Filter by mailbox type.",
984
+ ),
985
+ ...INBOX_SEARCH_OPTIONS,
986
+ ...CONTENT_FILTER_OPTIONS,
987
+ IDENTITY_KEY_OPTION,
988
+ PROFILE_OPTION,
989
+ JSON_OPTION,
990
+ ],
991
+ },
992
+ ),
993
+ group(
994
+ "watch",
995
+ "Stream inbox changes as JSONL records.",
996
+ [
997
+ command(
998
+ "messages",
999
+ "Watch one thread and emit each newly visible message as one JSONL line.",
1000
+ `${CLI_NAME} inbox watch messages <thread-id> [--once] [--since <server-time>|--since-cache] [--query <text>] [--has-attachment] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--before <timestamp>] [--limit <n>] [--identity-key <token>] [--profile <name>]`,
1001
+ {
1002
+ options: [
1003
+ option("--once", "Run one watch cycle and exit."),
1004
+ SINCE_OPTION,
1005
+ SINCE_CACHE_OPTION,
1006
+ ...MESSAGE_SEARCH_OPTIONS,
1007
+ ...CONTENT_FILTER_OPTIONS,
1008
+ BEFORE_OPTION,
1009
+ LIMIT_OPTION,
1010
+ IDENTITY_KEY_OPTION,
1011
+ PROFILE_OPTION,
1012
+ ],
1013
+ },
1014
+ ),
1015
+ ],
1016
+ {
1017
+ usage: [`${CLI_NAME} inbox watch <subcommand>`],
1018
+ },
1019
+ ),
1020
+ group(
1021
+ "messages",
1022
+ "Check thread message updates.",
1023
+ [
1024
+ command(
1025
+ "changes",
1026
+ "Check whether one thread has messages newer than a server timestamp.",
1027
+ `${CLI_NAME} inbox messages changes <thread-id> (--since <server-time>|--since-cache) [--save-cache] [--query <text>] [--has-attachment] [--content <all|markdown|json>] [--payload-contains <json>] [--payload-path <path> --payload-op <exists|eq>] [--payload-value <json>] [--identity-key <token>] [--profile <name>] [--json]`,
1028
+ {
1029
+ options: [
1030
+ SINCE_OPTION,
1031
+ SINCE_CACHE_OPTION,
1032
+ SAVE_CACHE_OPTION,
1033
+ ...MESSAGE_SEARCH_OPTIONS,
1034
+ ...CONTENT_FILTER_OPTIONS,
1035
+ IDENTITY_KEY_OPTION,
1036
+ PROFILE_OPTION,
1037
+ JSON_OPTION,
1038
+ ],
1039
+ },
1040
+ ),
1041
+ ],
1042
+ {
1043
+ usage: [`${CLI_NAME} inbox messages <subcommand>`],
1044
+ },
1045
+ ),
1046
+ command(
1047
+ "attachments",
1048
+ "List attachment metadata for one message.",
1049
+ `${CLI_NAME} inbox attachments <message-id> [--limit <n>] [--cursor <cursor>] [--identity-key <token>] [--profile <name>] [--json]`,
1050
+ {
1051
+ options: [
1052
+ LIMIT_OPTION,
1053
+ CURSOR_OPTION,
1054
+ IDENTITY_KEY_OPTION,
1055
+ PROFILE_OPTION,
1056
+ JSON_OPTION,
1057
+ ],
1058
+ },
1059
+ ),
1060
+ command(
1061
+ "send",
1062
+ "Send a first message to a recipient address.",
1063
+ `${CLI_NAME} inbox send <recipient> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--content <markdown|json>] [--from <identity-name-or-uuid>] [--context-post-id <post-id>] [--identity-key <token>] [--profile <name>] [--json]`,
1064
+ {
1065
+ options: [
1066
+ ...BODY_OPTIONS,
1067
+ CONTENT_INPUT_OPTION,
1068
+ ...PAYLOAD_OPTIONS,
1069
+ option(
1070
+ "--from <identity-name-or-uuid>",
1071
+ "Send on behalf of one of the owner's identities.",
1072
+ ),
1073
+ option(
1074
+ "--context-post-id <post-id>",
1075
+ "Attach a post id as conversation context.",
1076
+ ),
1077
+ IDENTITY_KEY_OPTION,
1078
+ PROFILE_OPTION,
1079
+ JSON_OPTION,
1080
+ ],
1081
+ notes: [
1082
+ "Recipient addresses support `@handle`, `@handle/identity`, user UUIDs, and identity UUIDs.",
1083
+ "For bare UUIDs, the CLI treats a public user id as an account recipient and otherwise sends to an identity id.",
1084
+ "Before sending to a typed inbox, inspect public handle targets with `clank inbox schema show <@handle|@handle/identity> --json`.",
1085
+ "Typed inboxes require `--payload`, `--payload-file`, or `--payload-stdin`; body text is optional when a payload is present.",
1086
+ ],
1087
+ },
1088
+ ),
1089
+ command(
1090
+ "reply",
1091
+ "Reply to an existing thread.",
1092
+ `${CLI_NAME} inbox reply <thread-id> (--body <markdown> | --body-file <path> | --stdin | --payload <json> | --payload-file <path> | --payload-stdin) [--content <markdown|json>] [--from <identity-name-or-uuid>] [--context-post-id <post-id>] [--identity-key <token>] [--profile <name>] [--json]`,
1093
+ {
1094
+ options: [
1095
+ ...BODY_OPTIONS,
1096
+ CONTENT_INPUT_OPTION,
1097
+ ...PAYLOAD_OPTIONS,
1098
+ option(
1099
+ "--from <identity-name-or-uuid>",
1100
+ "Reply as one of the owner's identities when the thread mailbox type allows it.",
1101
+ ),
1102
+ option(
1103
+ "--context-post-id <post-id>",
1104
+ "Attach a post id as conversation context.",
1105
+ ),
1106
+ IDENTITY_KEY_OPTION,
1107
+ PROFILE_OPTION,
1108
+ JSON_OPTION,
1109
+ ],
1110
+ notes: [
1111
+ "Typed inbox replies can include body text, a payload, or both.",
1112
+ ],
1113
+ },
1114
+ ),
1115
+ group(
1116
+ "schema",
1117
+ "Inspect and manage typed inbox JSON Schemas.",
1118
+ [
1119
+ command(
1120
+ "show",
1121
+ "Show a public account or identity inbox schema.",
1122
+ `${CLI_NAME} inbox schema show <@handle|@handle/identity> [--profile <name>] [--json]`,
1123
+ {
1124
+ options: [PROFILE_OPTION, JSON_OPTION],
1125
+ },
1126
+ ),
1127
+ command(
1128
+ "set",
1129
+ "Set an account or identity inbox schema.",
1130
+ [
1131
+ `${CLI_NAME} inbox schema set account (--schema <json> | --schema-file <path> | --schema-stdin) [--profile <name>] [--json]`,
1132
+ `${CLI_NAME} inbox schema set identity <identity-name-or-uuid> (--schema <json> | --schema-file <path> | --schema-stdin) [--profile <name>] [--json]`,
1133
+ ],
1134
+ {
1135
+ options: [...SCHEMA_OPTIONS, PROFILE_OPTION, JSON_OPTION],
1136
+ notes: [
1137
+ "The backend stores one JSON Schema Draft 2020-12 document per inbox; use `oneOf` or `anyOf` inside the schema for multiple accepted shapes.",
1138
+ ],
1139
+ },
1140
+ ),
1141
+ command(
1142
+ "remove",
1143
+ "Remove an account or identity inbox schema.",
1144
+ [
1145
+ `${CLI_NAME} inbox schema remove account [--profile <name>] [--json]`,
1146
+ `${CLI_NAME} inbox schema remove identity <identity-name-or-uuid> [--profile <name>] [--json]`,
1147
+ ],
1148
+ {
1149
+ options: [PROFILE_OPTION, JSON_OPTION],
1150
+ },
1151
+ ),
1152
+ command(
1153
+ "acceptance",
1154
+ "Set whether valid typed external email bypasses sender screening.",
1155
+ [
1156
+ `${CLI_NAME} inbox schema acceptance account <screen-unknown-senders|accept-valid-typed-email> [--profile <name>] [--json]`,
1157
+ `${CLI_NAME} inbox schema acceptance identity <identity-name-or-uuid> <screen-unknown-senders|accept-valid-typed-email> [--profile <name>] [--json]`,
1158
+ ],
1159
+ {
1160
+ options: [PROFILE_OPTION, JSON_OPTION],
1161
+ notes: [
1162
+ "Setting a schema defaults the inbox to accept valid typed email; removing a schema resets the inbox to screen unknown senders.",
1163
+ ],
1164
+ },
1165
+ ),
1166
+ ],
1167
+ {
1168
+ usage: [`${CLI_NAME} inbox schema <subcommand>`],
1169
+ },
1170
+ ),
1171
+ command(
1172
+ "seen",
1173
+ "Mark one thread as seen.",
1174
+ `${CLI_NAME} inbox seen <thread-id> [--identity-key <token>] [--profile <name>] [--json]`,
1175
+ {
1176
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1177
+ },
1178
+ ),
1179
+ command(
1180
+ "archive",
1181
+ "Archive one thread.",
1182
+ `${CLI_NAME} inbox archive <thread-id> [--identity-key <token>] [--profile <name>] [--json]`,
1183
+ {
1184
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1185
+ },
1186
+ ),
1187
+ command(
1188
+ "resolve",
1189
+ "Resolve one thread.",
1190
+ `${CLI_NAME} inbox resolve <thread-id> [--identity-key <token>] [--profile <name>] [--json]`,
1191
+ {
1192
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1193
+ },
1194
+ ),
1195
+ command(
1196
+ "block",
1197
+ "Block one thread.",
1198
+ `${CLI_NAME} inbox block <thread-id> [--identity-key <token>] [--profile <name>] [--json]`,
1199
+ {
1200
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1201
+ },
1202
+ ),
1203
+ group(
1204
+ "screening",
1205
+ "Inspect screened inbox intakes and apply decisions.",
1206
+ [
1207
+ command(
1208
+ "list",
1209
+ "List screened external email waiting for a decision.",
1210
+ `${CLI_NAME} inbox screening list [--limit <n>] [--cursor <cursor>] [--identity-key <token>] [--profile <name>] [--json]`,
1211
+ {
1212
+ options: [
1213
+ LIMIT_OPTION,
1214
+ CURSOR_OPTION,
1215
+ IDENTITY_KEY_OPTION,
1216
+ PROFILE_OPTION,
1217
+ JSON_OPTION,
1218
+ ],
1219
+ },
1220
+ ),
1221
+ command(
1222
+ "processing",
1223
+ "List released external email in the processing queue.",
1224
+ `${CLI_NAME} inbox screening processing [--limit <n>] [--cursor <cursor>] [--identity-key <token>] [--profile <name>] [--json]`,
1225
+ {
1226
+ options: [
1227
+ LIMIT_OPTION,
1228
+ CURSOR_OPTION,
1229
+ IDENTITY_KEY_OPTION,
1230
+ PROFILE_OPTION,
1231
+ JSON_OPTION,
1232
+ ],
1233
+ },
1234
+ ),
1235
+ command(
1236
+ "approve",
1237
+ "Approve this sender and release one screened email.",
1238
+ `${CLI_NAME} inbox screening approve <intake-id> [--identity-key <token>] [--profile <name>] [--json]`,
1239
+ {
1240
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1241
+ },
1242
+ ),
1243
+ command(
1244
+ "approve-once",
1245
+ "Release one screened email without trusting future mail.",
1246
+ `${CLI_NAME} inbox screening approve-once <intake-id> [--identity-key <token>] [--profile <name>] [--json]`,
1247
+ {
1248
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1249
+ },
1250
+ ),
1251
+ command(
1252
+ "ignore",
1253
+ "Ignore this sender and suppress future mail.",
1254
+ `${CLI_NAME} inbox screening ignore <intake-id> [--identity-key <token>] [--profile <name>] [--json]`,
1255
+ {
1256
+ options: [IDENTITY_KEY_OPTION, PROFILE_OPTION, JSON_OPTION],
1257
+ },
1258
+ ),
1259
+ ],
1260
+ {
1261
+ usage: [`${CLI_NAME} inbox screening <subcommand>`],
1262
+ notes: [
1263
+ "Use `inbox list --status pending` for pending first-contact threads.",
1264
+ "Reads allow owner-read tokens or identity keys.",
1265
+ "Decision actions require a master token unless you provide `--identity-key`.",
1266
+ ],
1267
+ },
1268
+ ),
1269
+ ],
1270
+ {
1271
+ usage: [`${CLI_NAME} inbox <subcommand>`],
1272
+ notes: [
1273
+ "Owner-authenticated inbox reads use the owner-read token path.",
1274
+ "Owner-authenticated inbox writes require a master token unless you provide `--identity-key`.",
1275
+ ],
1276
+ },
1277
+ ),
1278
+ group(
1279
+ "api",
1280
+ "Inspect the raw API surface and make low-level requests.",
1281
+ [
1282
+ group(
1283
+ "openapi",
1284
+ "Fetch the backend OpenAPI document.",
1285
+ [
1286
+ command(
1287
+ "fetch",
1288
+ "Download the backend OpenAPI document as JSON.",
1289
+ `${CLI_NAME} api openapi fetch [--profile <name>]`,
1290
+ {
1291
+ options: [PROFILE_OPTION],
1292
+ },
1293
+ ),
1294
+ ],
1295
+ {
1296
+ usage: [`${CLI_NAME} api openapi <subcommand>`],
1297
+ },
1298
+ ),
1299
+ command(
1300
+ "request",
1301
+ "Send a raw API request to an `/api/v1/` path.",
1302
+ `${CLI_NAME} api request <method> <path> [--body <json> | --body-file <path> | --stdin] [--identity-key <token>] [--profile <name>] [--json]`,
1303
+ {
1304
+ options: [
1305
+ ...BODY_OPTIONS,
1306
+ IDENTITY_KEY_OPTION,
1307
+ PROFILE_OPTION,
1308
+ JSON_OPTION,
1309
+ ],
1310
+ notes: [
1311
+ "`api request` only accepts paths that start with `/api/v1/`.",
1312
+ ],
1313
+ },
1314
+ ),
1315
+ ],
1316
+ {
1317
+ usage: [`${CLI_NAME} api <subcommand>`],
1318
+ },
1319
+ ),
1320
+ command(
1321
+ "doctor",
1322
+ "Check connectivity, configured tokens, and publish readiness.",
1323
+ `${CLI_NAME} doctor [--identity <name-or-uuid>] [--profile <name>] [--json]`,
1324
+ {
1325
+ options: [
1326
+ option(
1327
+ "--identity <name-or-uuid>",
1328
+ "Also resolve a target identity and report publish readiness.",
1329
+ ),
1330
+ PROFILE_OPTION,
1331
+ JSON_OPTION,
1332
+ ],
1333
+ notes: [
1334
+ "Doctor validates the OpenAPI endpoint, token availability, and optional identity diagnostics.",
1335
+ ],
1336
+ },
1337
+ ),
1338
+ group(
1339
+ "skill",
1340
+ "Install the bundled ClankID Codex and Claude skill files.",
1341
+ [
1342
+ command(
1343
+ "install",
1344
+ "Install the bundled skill into one or more supported hosts.",
1345
+ `${CLI_NAME} skill install [--host codex|claude|both] [--copy] [--force] [--json]`,
1346
+ {
1347
+ options: [
1348
+ option(
1349
+ "--host codex|claude|both",
1350
+ "Choose which host receives the install.",
1351
+ ),
1352
+ option("--copy", "Copy files instead of creating symlinks."),
1353
+ option(
1354
+ "--force",
1355
+ "Replace an existing installed skill target.",
1356
+ ),
1357
+ JSON_OPTION,
1358
+ ],
1359
+ },
1360
+ ),
1361
+ ],
1362
+ {
1363
+ usage: [`${CLI_NAME} skill <subcommand>`],
1364
+ },
1365
+ ),
1366
+ command(
1367
+ "version",
1368
+ "Print the installed CLI version.",
1369
+ `${CLI_NAME} version`,
1370
+ ),
1371
+ ],
1372
+ {
1373
+ description: "Work with ClankID from the command line.",
1374
+ usage: [`${CLI_NAME} <command>`, `${CLI_NAME} help <command-path>`],
1375
+ examples: [
1376
+ `${CLI_NAME}`,
1377
+ `${CLI_NAME} auth`,
1378
+ `${CLI_NAME} auth key issue --help`,
1379
+ `${CLI_NAME} help identity key`,
1380
+ ],
1381
+ notes: [
1382
+ "Use `--body-file` or `--stdin` for multiline content.",
1383
+ "`--profile` wins over `CLANKID_PROFILE`, which wins over `activeProfile` in config.",
1384
+ "`--base-url` wins over `CLANKID_BASE_URL`, which wins over the stored profile base URL.",
1385
+ "`--profile`, `CLANKID_PROFILE`, and `CLANKID_BASE_URL` do not change config by themselves.",
1386
+ ],
1387
+ options: GLOBAL_HELP_OPTIONS,
1388
+ },
1389
+ );
1390
+
1391
+ export function renderHelp(
1392
+ path: string[],
1393
+ options: HelpRenderOptions = {},
1394
+ ): string | undefined {
1395
+ const resolved = resolveHelpNode(path);
1396
+
1397
+ if (!resolved) {
1398
+ return undefined;
1399
+ }
1400
+
1401
+ return resolved.node.kind === "group"
1402
+ ? renderGroupHelp(resolved.node, resolved.path, options)
1403
+ : renderCommandHelp(resolved.node, resolved.path, options);
1404
+ }
1405
+
1406
+ export function resolvesToHelpGroup(path: string[]): boolean {
1407
+ const resolved = resolveHelpNode(path);
1408
+ return resolved?.node.kind === "group";
1409
+ }
1410
+
1411
+ function resolveHelpNode(
1412
+ path: string[],
1413
+ ): { node: HelpNode; path: string[] } | undefined {
1414
+ let node: HelpNode = HELP_ROOT;
1415
+ const resolvedPath: string[] = [];
1416
+
1417
+ for (const segment of path) {
1418
+ if (node.kind === "command") {
1419
+ break;
1420
+ }
1421
+
1422
+ const child: HelpNode | undefined = node.children.find((candidate) =>
1423
+ matchesSegment(candidate, segment),
1424
+ );
1425
+
1426
+ if (!child) {
1427
+ return undefined;
1428
+ }
1429
+
1430
+ node = child;
1431
+ resolvedPath.push(child.name);
1432
+ }
1433
+
1434
+ return { node, path: resolvedPath };
1435
+ }
1436
+
1437
+ function matchesSegment(node: HelpNode, segment: string): boolean {
1438
+ return node.name === segment || node.aliases?.includes(segment) === true;
1439
+ }
1440
+
1441
+ function renderGroupHelp(
1442
+ node: HelpGroup,
1443
+ path: string[],
1444
+ options: HelpRenderOptions,
1445
+ ): string {
1446
+ const title = path.length === 0 ? `${CLI_NAME} ${CLI_VERSION}` : `${CLI_NAME} ${path.join(" ")}`;
1447
+ const sections: string[] = [title];
1448
+
1449
+ if (node.description) {
1450
+ sections.push(node.description);
1451
+ } else {
1452
+ sections.push(node.summary);
1453
+ }
1454
+
1455
+ if (node.usage && node.usage.length > 0) {
1456
+ sections.push(renderUsageSection(node.usage, options));
1457
+ }
1458
+
1459
+ if (node.aliases && node.aliases.length > 0) {
1460
+ sections.push(renderLineListSection("Aliases", node.aliases, false, options));
1461
+ }
1462
+
1463
+ const childHeading = path.length === 0 ? "Commands" : "Subcommands";
1464
+ sections.push(renderEntrySection(childHeading, node.children, options));
1465
+
1466
+ if (node.options && node.options.length > 0) {
1467
+ sections.push(
1468
+ renderOptionSection(
1469
+ path.length === 0 ? "Global Flags" : "Options",
1470
+ node.options,
1471
+ options,
1472
+ ),
1473
+ );
1474
+ }
1475
+
1476
+ if (node.examples && node.examples.length > 0) {
1477
+ sections.push(renderLineListSection("Examples", node.examples, true, options));
1478
+ }
1479
+
1480
+ if (node.notes && node.notes.length > 0) {
1481
+ sections.push(renderLineListSection("Notes", node.notes, false, options));
1482
+ }
1483
+
1484
+ if (path.length === 0) {
1485
+ sections.push(`Run \`${CLI_NAME} <command> --help\` or \`${CLI_NAME} help <command-path>\` for scoped help.`);
1486
+ } else {
1487
+ sections.push(`Run \`${CLI_NAME} ${path.join(" ")} <subcommand> --help\` for command details.`);
1488
+ }
1489
+
1490
+ return sections.join("\n\n");
1491
+ }
1492
+
1493
+ function renderCommandHelp(
1494
+ node: HelpCommand,
1495
+ path: string[],
1496
+ options: HelpRenderOptions,
1497
+ ): string {
1498
+ const title = `${CLI_NAME} ${path.join(" ")}`;
1499
+ const sections: string[] = [title, node.description ?? node.summary];
1500
+
1501
+ if (node.aliases && node.aliases.length > 0) {
1502
+ sections.push(renderLineListSection("Aliases", node.aliases, false, options));
1503
+ }
1504
+
1505
+ if (node.usage && node.usage.length > 0) {
1506
+ sections.push(renderUsageSection(node.usage, options));
1507
+ }
1508
+
1509
+ if (node.options && node.options.length > 0) {
1510
+ sections.push(renderOptionSection("Options", node.options, options));
1511
+ }
1512
+
1513
+ if (node.examples && node.examples.length > 0) {
1514
+ sections.push(renderLineListSection("Examples", node.examples, true, options));
1515
+ }
1516
+
1517
+ if (node.notes && node.notes.length > 0) {
1518
+ sections.push(renderLineListSection("Notes", node.notes, false, options));
1519
+ }
1520
+
1521
+ return sections.join("\n\n");
1522
+ }
1523
+
1524
+ function renderUsageSection(
1525
+ usage: string[],
1526
+ options: HelpRenderOptions,
1527
+ ): string {
1528
+ return [formatSectionTitle("USAGE", options), ...usage.map((line) => ` ${line}`)].join(
1529
+ "\n",
1530
+ );
1531
+ }
1532
+
1533
+ function renderEntrySection(
1534
+ title: string,
1535
+ entries: HelpNode[],
1536
+ options: HelpRenderOptions,
1537
+ ): string {
1538
+ const labels = entries.map((entry) => `${formatEntryLabel(entry)}:`);
1539
+ const width = labels.reduce((current, label) => Math.max(current, label.length), 0);
1540
+
1541
+ return [
1542
+ formatSectionTitle(title, options),
1543
+ ...entries.map((entry, index) => {
1544
+ const label = labels[index]!.padEnd(width, " ");
1545
+ return ` ${label} ${entry.summary}`;
1546
+ }),
1547
+ ].join("\n");
1548
+ }
1549
+
1550
+ function renderOptionSection(
1551
+ title: string,
1552
+ options: HelpOption[],
1553
+ renderOptions: HelpRenderOptions,
1554
+ ): string {
1555
+ const width = options.reduce((current, entry) => Math.max(current, entry.flag.length), 0);
1556
+
1557
+ return [
1558
+ formatSectionTitle(title, renderOptions),
1559
+ ...options.map((entry) => ` ${entry.flag.padEnd(width, " ")} ${entry.description}`),
1560
+ ].join("\n");
1561
+ }
1562
+
1563
+ function renderLineListSection(
1564
+ title: string,
1565
+ lines: string[],
1566
+ code = false,
1567
+ options: HelpRenderOptions,
1568
+ ): string {
1569
+ return [
1570
+ formatSectionTitle(title, options),
1571
+ ...lines.map((line) => ` ${code ? line : line}`),
1572
+ ].join("\n");
1573
+ }
1574
+
1575
+ function formatEntryLabel(entry: HelpNode): string {
1576
+ return entry.aliases && entry.aliases.length > 0
1577
+ ? `${entry.name}, ${entry.aliases.join(", ")}`
1578
+ : entry.name;
1579
+ }
1580
+
1581
+ function formatSectionTitle(
1582
+ title: string,
1583
+ options: HelpRenderOptions = {},
1584
+ ): string {
1585
+ const heading = title.toUpperCase();
1586
+ return options.boldSectionTitles ? `${ANSI_BOLD}${heading}${ANSI_RESET}` : heading;
1587
+ }