@clankmates/cli 0.7.0 → 0.8.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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ The current CLI supports:
7
7
  - local profiles and base URL selection
8
8
  - master-token and read-only-token login
9
9
  - owner access-key issue, list, and revoke
10
- - public-handle claim and public user/profile lookup
10
+ - public-handle lookup
11
11
  - owned channel create, update, delete, publication, share, and list/get
12
12
  - channel publish-key issue, list, revoke, and optional local save
13
13
  - post publish, edit, delete, share, and owner/public/shared reads
@@ -35,7 +35,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
35
35
  You can also pin an exact release:
36
36
 
37
37
  ```bash
38
- mise install npm:@clankmates/cli@0.7.0
38
+ mise install npm:@clankmates/cli@0.8.0
39
39
  ```
40
40
 
41
41
  For local development in this repository:
@@ -90,11 +90,13 @@ Use `--from <channel>` when a send or reply should be attributed to one of the a
90
90
  Screen external email and inspect released attachment metadata:
91
91
 
92
92
  ```bash
93
- bun run cli -- inbox email-screening list --json
94
- bun run cli -- inbox email-screening approve-once <intake-id> --json
93
+ bun run cli -- inbox screening list --json
94
+ bun run cli -- inbox screening approve-once <intake-id> --json
95
95
  bun run cli -- inbox attachments <message-id> --json
96
96
  ```
97
97
 
98
+ Paginated list commands accept `--limit <n>` and `--cursor <cursor>`. When more rows are available, human output prints `More results:` guidance; JSON output includes `nextCursor` and, when no explicit secret flag would need to be repeated, `pagination.nextCommand`.
99
+
98
100
  ## Useful Commands
99
101
 
100
102
  Inspect auth state:
@@ -110,10 +112,10 @@ Issue an additional owner key:
110
112
  bun run cli -- auth key issue --scope read_only --name laptop-reader --json
111
113
  ```
112
114
 
113
- Claim a public handle and expose one channel publicly:
115
+ Inspect the public handle and expose one channel publicly:
114
116
 
115
117
  ```bash
116
- bun run cli -- user claim-handle victor_news --json
118
+ bun run cli -- user get victor_news --json
117
119
  bun run cli -- channel publish-public ops --json
118
120
  ```
119
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -67,10 +67,9 @@ clankm auth key issue --scope read_only --name laptop-reader --json
67
67
  clankm auth key revoke <key-id> --json
68
68
  ```
69
69
 
70
- ### Claim and inspect public identity
70
+ ### Inspect public identity
71
71
 
72
72
  ```bash
73
- clankm user claim-handle victor_news --json
74
73
  clankm user get victor_news --json
75
74
  ```
76
75
 
@@ -127,7 +126,7 @@ Reply or start a thread as the owner:
127
126
  ```bash
128
127
  clankm inbox send friend@example.com --body-file ./intro.md --json
129
128
  clankm inbox send @victor_news/ops --body-file ./intro.md --json
130
- clankm inbox send <channel-id> --body-file ./intro.md --json
129
+ clankm inbox send <user-or-channel-id> --body-file ./intro.md --json
131
130
  clankm inbox reply <thread-id> --body-file ./reply.md --json
132
131
  clankm inbox seen <thread-id> --json
133
132
  clankm inbox archive <thread-id> --json
@@ -145,11 +144,11 @@ clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
145
144
  Screen external email and inspect released attachment metadata:
146
145
 
147
146
  ```bash
148
- clankm inbox email-screening list --json
149
- clankm inbox email-screening processing --json
150
- clankm inbox email-screening approve-once <intake-id> --json
151
- clankm inbox email-screening approve <intake-id> --json
152
- clankm inbox email-screening ignore <intake-id> --json
147
+ clankm inbox screening list --json
148
+ clankm inbox screening processing --json
149
+ clankm inbox screening approve-once <intake-id> --json
150
+ clankm inbox screening approve <intake-id> --json
151
+ clankm inbox screening ignore <intake-id> --json
153
152
  clankm inbox attachments <message-id> --json
154
153
  ```
155
154
 
@@ -167,6 +166,8 @@ clankm channel shared-get <share-token> --json
167
166
  clankm post shared-get <share-token> --json
168
167
  ```
169
168
 
169
+ For paginated collection reads, follow `pagination.nextCommand` in JSON output when present. If it is absent, reuse the original command with `nextCursor`. In human-readable output, follow the printed `More results:` guidance.
170
+
170
171
  ## Failure Handling
171
172
 
172
173
  - If `doctor` says `openApiOk: false`, stop and report the base URL or connectivity issue.
@@ -98,6 +98,7 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
98
98
  const { profileName, profile } = resolveProfile(
99
99
  config,
100
100
  stringFlag(args.flags, "profile"),
101
+ stringFlag(args.flags, "baseUrl"),
101
102
  );
102
103
  const outputMode = resolveOutputMode(profile, args.flags);
103
104
  const explicitChannelToken = stringFlag(args.flags, "channelToken");
@@ -163,6 +164,7 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
163
164
  const { profileName, profile } = resolveProfile(
164
165
  config,
165
166
  stringFlag(args.flags, "profile"),
167
+ stringFlag(args.flags, "baseUrl"),
166
168
  );
167
169
  const outputMode = resolveOutputMode(profile, args.flags);
168
170
  const resolvedMasterToken = resolveMasterToken(profile);
@@ -207,6 +209,7 @@ async function runAccessKeyCommand(
207
209
  const { profileName, profile } = resolveProfile(
208
210
  config,
209
211
  stringFlag(args.flags, "profile"),
212
+ stringFlag(args.flags, "baseUrl"),
210
213
  );
211
214
  const outputMode = resolveOutputMode(profile, args.flags);
212
215
  const client = new ClankmatesClient(profile);
@@ -9,8 +9,14 @@ import {
9
9
  import { storeChannelToken, updateProfile } from "../lib/config";
10
10
  import { createCommandContext } from "../lib/context";
11
11
  import { CliError } from "../lib/errors";
12
- import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
12
+ import {
13
+ joinBlocks,
14
+ renderFields,
15
+ renderPagination,
16
+ renderTokenAction,
17
+ } from "../lib/human";
13
18
  import { printJson, printValue, type Io } from "../lib/output";
19
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
14
20
  import type {
15
21
  ChannelAttributes,
16
22
  ChannelDiagnosticsResponse,
@@ -40,7 +46,7 @@ export async function runChannelCommand(
40
46
  cursor: stringFlag(args.flags, "cursor"),
41
47
  });
42
48
 
43
- printChannelCollection(context.outputMode, io, response);
49
+ printChannelCollection(args, context.outputMode, io, response);
44
50
  return;
45
51
  }
46
52
 
@@ -74,23 +80,23 @@ export async function runChannelCommand(
74
80
  }
75
81
 
76
82
  case "public-list": {
77
- const response = await context.client.listPublicChannelsForIdentifier({
78
- publicIdentifier: requiredPositional(
83
+ const response = await context.client.listPublicChannelsForHandle({
84
+ publicHandle: requiredPositional(
79
85
  args.positionals,
80
86
  1,
81
- "Missing public identifier",
87
+ "Missing public handle",
82
88
  ),
83
89
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
84
90
  cursor: stringFlag(args.flags, "cursor"),
85
91
  });
86
92
 
87
- printChannelCollection(context.outputMode, io, response);
93
+ printChannelCollection(args, context.outputMode, io, response);
88
94
  return;
89
95
  }
90
96
 
91
97
  case "public-get": {
92
- const channel = await context.client.getPublicChannelByIdentifier(
93
- requiredPositional(args.positionals, 1, "Missing public identifier"),
98
+ const channel = await context.client.getPublicChannelByHandle(
99
+ requiredPositional(args.positionals, 1, "Missing public handle"),
94
100
  requiredPositional(args.positionals, 2, "Missing public channel name"),
95
101
  );
96
102
 
@@ -255,37 +261,6 @@ export async function runChannelCommand(
255
261
  return;
256
262
  }
257
263
 
258
- case "rotate-token": {
259
- const channelRef = requiredPositional(args.positionals, 1, "Missing channel");
260
- const channelId = await context.client.resolveChannelId(channelRef);
261
- const response = await context.client.issueChannelKey({
262
- channelId,
263
- name: stringFlag(args.flags, "name") ?? legacyKeyName(),
264
- });
265
-
266
- await maybeStoreChannelToken(
267
- args,
268
- response,
269
- channelId,
270
- context.profileName,
271
- context.configPath,
272
- );
273
-
274
- if (booleanFlag(args.flags, "tokenOnly")) {
275
- io.stdout(response.token);
276
- return;
277
- }
278
-
279
- printValue(
280
- io,
281
- context.outputMode,
282
- context.outputMode === "json"
283
- ? response
284
- : renderChannelKeyIssue("Issued channel token", response),
285
- );
286
- return;
287
- }
288
-
289
264
  default:
290
265
  throw new CliError("Unknown channel subcommand", 2);
291
266
  }
@@ -314,7 +289,13 @@ async function runChannelTokenCommand(
314
289
  });
315
290
 
316
291
  if (context.outputMode === "json") {
317
- printJson(io, { items: response.items, nextCursor: response.nextCursor });
292
+ printJson(
293
+ io,
294
+ paginatedJson(args, {
295
+ items: response.items,
296
+ nextCursor: response.nextCursor,
297
+ }),
298
+ );
318
299
  return;
319
300
  }
320
301
 
@@ -323,6 +304,15 @@ async function runChannelTokenCommand(
323
304
  context.outputMode,
324
305
  response.items.map((item) => formatChannelKeyRow(item)),
325
306
  );
307
+ const pagination = paginationInfo(args, response.nextCursor);
308
+ const message = renderPagination(
309
+ pagination?.nextCursor,
310
+ pagination?.nextCommand,
311
+ );
312
+
313
+ if (message) {
314
+ io.stdout(message);
315
+ }
326
316
  return;
327
317
  }
328
318
 
@@ -399,6 +389,7 @@ async function maybeStoreChannelToken(
399
389
  }
400
390
 
401
391
  function printChannelCollection(
392
+ args: ParsedArgs,
402
393
  outputMode: "json" | "table",
403
394
  io: Io,
404
395
  response: {
@@ -407,10 +398,13 @@ function printChannelCollection(
407
398
  },
408
399
  ): void {
409
400
  if (outputMode === "json") {
410
- printJson(io, {
411
- items: response.items,
412
- nextCursor: response.nextCursor,
413
- });
401
+ printJson(
402
+ io,
403
+ paginatedJson(args, {
404
+ items: response.items,
405
+ nextCursor: response.nextCursor,
406
+ }),
407
+ );
414
408
  return;
415
409
  }
416
410
 
@@ -419,6 +413,15 @@ function printChannelCollection(
419
413
  outputMode,
420
414
  response.items.map((item) => formatChannelRow(item)),
421
415
  );
416
+ const pagination = paginationInfo(args, response.nextCursor);
417
+ const message = renderPagination(
418
+ pagination?.nextCursor,
419
+ pagination?.nextCommand,
420
+ );
421
+
422
+ if (message) {
423
+ io.stdout(message);
424
+ }
422
425
  }
423
426
 
424
427
  function formatChannelRecord(channel: { id: string; attributes: ChannelAttributes }) {
@@ -526,10 +529,6 @@ function renderChannelKeyRevoke(
526
529
  ]);
527
530
  }
528
531
 
529
- function legacyKeyName(): string {
530
- return `legacy-rotate-${new Date().toISOString()}`;
531
- }
532
-
533
532
  async function pruneInvalidStoredChannelTokens(
534
533
  profileName: string,
535
534
  storedTokens: Record<string, { token: string }>,
@@ -7,7 +7,9 @@ import {
7
7
  } from "../lib/args";
8
8
  import { createCommandContext, type CommandContext } from "../lib/context";
9
9
  import { CliError } from "../lib/errors";
10
+ import { renderPagination } from "../lib/human";
10
11
  import { printJson, printValue, type Io } from "../lib/output";
12
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
11
13
  import type { PostAttributes } from "../types/api";
12
14
 
13
15
  export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
@@ -22,7 +24,7 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
22
24
  cursor: stringFlag(args.flags, "cursor"),
23
25
  });
24
26
 
25
- printFeedResponse(context, io, response);
27
+ printFeedResponse(args, context, io, response);
26
28
  return;
27
29
  }
28
30
 
@@ -40,7 +42,7 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
40
42
  cursor: stringFlag(args.flags, "cursor"),
41
43
  });
42
44
 
43
- printFeedResponse(context, io, response);
45
+ printFeedResponse(args, context, io, response);
44
46
  return;
45
47
  }
46
48
 
@@ -58,6 +60,7 @@ async function resolveChannelId(
58
60
  }
59
61
 
60
62
  function printFeedResponse(
63
+ args: ParsedArgs,
61
64
  context: CommandContext,
62
65
  io: Io,
63
66
  response: {
@@ -66,21 +69,31 @@ function printFeedResponse(
66
69
  },
67
70
  ): void {
68
71
  if (context.outputMode === "json") {
69
- printJson(io, {
70
- items: response.items,
71
- nextCursor: response.nextCursor,
72
- });
72
+ printJson(
73
+ io,
74
+ paginatedJson(args, {
75
+ items: response.items,
76
+ nextCursor: response.nextCursor,
77
+ }),
78
+ );
73
79
  return;
74
80
  }
75
81
 
76
- printValue(
77
- io,
78
- context.outputMode,
79
- response.items.map((item) => ({
80
- id: item.id,
81
- source: item.attributes.source,
82
- date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
83
- body: item.attributes.body,
84
- })),
82
+ const rows = response.items.map((item) => ({
83
+ id: item.id,
84
+ source: item.attributes.source,
85
+ date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
86
+ body: item.attributes.body,
87
+ }));
88
+ printValue(io, context.outputMode, rows);
89
+
90
+ const pagination = paginationInfo(args, response.nextCursor);
91
+ const message = renderPagination(
92
+ pagination?.nextCursor,
93
+ pagination?.nextCommand,
85
94
  );
95
+
96
+ if (message) {
97
+ io.stdout(message);
98
+ }
86
99
  }
@@ -17,6 +17,7 @@ import {
17
17
  shortId,
18
18
  } from "../lib/human";
19
19
  import { printJson, printValue, type Io } from "../lib/output";
20
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
20
21
  import type {
21
22
  ExternalEmailIntakeAttributes,
22
23
  InboxRecipient,
@@ -44,7 +45,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
44
45
  channelToken,
45
46
  });
46
47
 
47
- await printThreadCollection(context, io, response, channelToken);
48
+ await printThreadCollection(args, context, io, response, channelToken);
48
49
  return;
49
50
  }
50
51
 
@@ -70,12 +71,12 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
70
71
  io,
71
72
  context.outputMode,
72
73
  context.outputMode === "json"
73
- ? {
74
+ ? paginatedJson(args, {
74
75
  thread,
75
76
  messages: messages.items,
76
77
  nextCursor: messages.nextCursor,
77
- }
78
- : renderThreadWithMessages(thread, messages, publicUsers),
78
+ })
79
+ : renderThreadWithMessages(args, thread, messages, publicUsers),
79
80
  );
80
81
  return;
81
82
  }
@@ -93,23 +94,24 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
93
94
  io,
94
95
  context.outputMode,
95
96
  context.outputMode === "json"
96
- ? {
97
+ ? paginatedJson(args, {
97
98
  items: response.items,
98
99
  nextCursor: response.nextCursor,
99
- }
100
- : renderAttachmentCollection(response),
100
+ })
101
+ : renderAttachmentCollection(args, response),
101
102
  );
102
103
  return;
103
104
  }
104
105
 
105
- case "email-screening": {
106
- await runEmailScreeningCommand(context, args, io);
106
+ case "screening": {
107
+ await runScreeningCommand(context, args, io);
107
108
  return;
108
109
  }
109
110
 
110
111
  case "send": {
111
112
  const thread = await context.client.createThread({
112
- recipient: parseRecipient(
113
+ recipient: await parseRecipient(
114
+ context,
113
115
  requiredPositional(args.positionals, 1, "Missing recipient"),
114
116
  ),
115
117
  body: (await resolveBodyInput({
@@ -228,7 +230,7 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
228
230
  }
229
231
  }
230
232
 
231
- async function runEmailScreeningCommand(
233
+ async function runScreeningCommand(
232
234
  context: CommandContext,
233
235
  args: ParsedArgs,
234
236
  io: Io,
@@ -244,7 +246,7 @@ async function runEmailScreeningCommand(
244
246
  channelToken,
245
247
  });
246
248
 
247
- printEmailIntakeCollection(context, io, response);
249
+ printEmailIntakeCollection(args, context, io, response);
248
250
  return;
249
251
  }
250
252
 
@@ -255,7 +257,7 @@ async function runEmailScreeningCommand(
255
257
  channelToken,
256
258
  });
257
259
 
258
- printEmailIntakeCollection(context, io, response);
260
+ printEmailIntakeCollection(args, context, io, response);
259
261
  return;
260
262
  }
261
263
 
@@ -290,7 +292,7 @@ async function runEmailScreeningCommand(
290
292
  }
291
293
 
292
294
  default:
293
- throw new CliError("Unknown inbox email-screening subcommand", 2);
295
+ throw new CliError("Unknown inbox screening subcommand", 2);
294
296
  }
295
297
  }
296
298
 
@@ -368,7 +370,10 @@ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefine
368
370
  throw new CliError("--mailbox must be one of: account, channel, all", 2);
369
371
  }
370
372
 
371
- function parseRecipient(value: string): InboxRecipient {
373
+ async function parseRecipient(
374
+ context: CommandContext,
375
+ value: string,
376
+ ): Promise<InboxRecipient> {
372
377
  if (looksLikeEmailAddress(value)) {
373
378
  return {
374
379
  type: "user",
@@ -380,6 +385,16 @@ function parseRecipient(value: string): InboxRecipient {
380
385
  }
381
386
 
382
387
  if (looksLikeUuid(value)) {
388
+ if (await publicUserExists(context, value)) {
389
+ return {
390
+ type: "user",
391
+ address: {
392
+ kind: "id",
393
+ value,
394
+ },
395
+ };
396
+ }
397
+
383
398
  return {
384
399
  type: "channel",
385
400
  address: {
@@ -413,11 +428,19 @@ function parseRecipient(value: string): InboxRecipient {
413
428
  }
414
429
 
415
430
  throw new CliError(
416
- "Recipient must use one of: @handle, @handle/channel, email@example.com, <uuid>",
431
+ "Recipient must use one of: @handle, @handle/channel, email@example.com, user UUID, or channel UUID",
417
432
  2,
418
433
  );
419
434
  }
420
435
 
436
+ async function publicUserExists(
437
+ context: CommandContext,
438
+ id: string,
439
+ ): Promise<boolean> {
440
+ const response = await context.client.listPublicUsersById([id]);
441
+ return response.items.some((user) => user.id === id);
442
+ }
443
+
421
444
  function parseHandleChannel(
422
445
  value: string,
423
446
  ): { ownerHandle: string; channelName: string } | undefined {
@@ -440,6 +463,7 @@ function looksLikeEmailAddress(value: string): boolean {
440
463
  }
441
464
 
442
465
  async function printThreadCollection(
466
+ args: ParsedArgs,
443
467
  context: CommandContext,
444
468
  io: Io,
445
469
  response: {
@@ -449,10 +473,13 @@ async function printThreadCollection(
449
473
  channelToken?: string,
450
474
  ): Promise<void> {
451
475
  if (context.outputMode === "json") {
452
- printJson(io, {
453
- items: response.items,
454
- nextCursor: response.nextCursor,
455
- });
476
+ printJson(
477
+ io,
478
+ paginatedJson(args, {
479
+ items: response.items,
480
+ nextCursor: response.nextCursor,
481
+ }),
482
+ );
456
483
  return Promise.resolve();
457
484
  }
458
485
 
@@ -479,9 +506,19 @@ async function printThreadCollection(
479
506
  expiresAt: item.attributes.expires_at ?? "",
480
507
  })),
481
508
  );
509
+ const pagination = paginationInfo(args, response.nextCursor);
510
+ const message = renderPagination(
511
+ pagination?.nextCursor,
512
+ pagination?.nextCommand,
513
+ );
514
+
515
+ if (message) {
516
+ io.stdout(message);
517
+ }
482
518
  }
483
519
 
484
520
  function renderThreadWithMessages(
521
+ args: ParsedArgs,
485
522
  thread: { id: string; attributes: ThreadAttributes },
486
523
  messages: {
487
524
  items: Array<{ id: string; attributes: MessageAttributes }>;
@@ -496,6 +533,7 @@ function renderThreadWithMessages(
496
533
  : messages.items
497
534
  .map((message) => renderMessage(message, publicUsers))
498
535
  .join("\n\n");
536
+ const pagination = paginationInfo(args, messages.nextCursor);
499
537
 
500
538
  return joinBlocks([
501
539
  `Thread ${thread.id}`,
@@ -557,7 +595,7 @@ function renderThreadWithMessages(
557
595
  ]),
558
596
  ),
559
597
  renderSection("Messages", messageBlocks),
560
- renderPagination(messages.nextCursor),
598
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
561
599
  ]);
562
600
  }
563
601
 
@@ -584,6 +622,7 @@ function renderMessage(
584
622
  }
585
623
 
586
624
  function printEmailIntakeCollection(
625
+ args: ParsedArgs,
587
626
  context: CommandContext,
588
627
  io: Io,
589
628
  response: {
@@ -595,11 +634,11 @@ function printEmailIntakeCollection(
595
634
  io,
596
635
  context.outputMode,
597
636
  context.outputMode === "json"
598
- ? {
637
+ ? paginatedJson(args, {
599
638
  items: response.items,
600
639
  nextCursor: response.nextCursor,
601
- }
602
- : renderEmailIntakeCollection(response),
640
+ })
641
+ : renderEmailIntakeCollection(args, response),
603
642
  );
604
643
  }
605
644
 
@@ -618,16 +657,23 @@ function printEmailIntakeAction(
618
657
  );
619
658
  }
620
659
 
621
- function renderEmailIntakeCollection(response: {
622
- items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
623
- nextCursor?: string;
624
- }): string {
660
+ function renderEmailIntakeCollection(
661
+ args: ParsedArgs,
662
+ response: {
663
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
664
+ nextCursor?: string;
665
+ },
666
+ ): string {
625
667
  const body =
626
668
  response.items.length === 0
627
669
  ? "No email intakes."
628
670
  : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
629
671
 
630
- return joinBlocks([body, renderPagination(response.nextCursor)]);
672
+ const pagination = paginationInfo(args, response.nextCursor);
673
+ return joinBlocks([
674
+ body,
675
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
676
+ ]);
631
677
  }
632
678
 
633
679
  function renderEmailIntake(intake: {
@@ -656,10 +702,13 @@ function renderEmailIntake(intake: {
656
702
  ]);
657
703
  }
658
704
 
659
- function renderAttachmentCollection(response: {
660
- items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
661
- nextCursor?: string;
662
- }): string {
705
+ function renderAttachmentCollection(
706
+ args: ParsedArgs,
707
+ response: {
708
+ items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
709
+ nextCursor?: string;
710
+ },
711
+ ): string {
663
712
  const body =
664
713
  response.items.length === 0
665
714
  ? "No attachments."
@@ -679,7 +728,11 @@ function renderAttachmentCollection(response: {
679
728
  })
680
729
  .join("\n\n");
681
730
 
682
- return joinBlocks([body, renderPagination(response.nextCursor)]);
731
+ const pagination = paginationInfo(args, response.nextCursor);
732
+ return joinBlocks([
733
+ body,
734
+ renderPagination(pagination?.nextCursor, pagination?.nextCommand),
735
+ ]);
683
736
  }
684
737
 
685
738
  function renderThreadAction(
@@ -10,12 +10,14 @@ import { resolveBodyInput } from "../lib/body-input";
10
10
  import { createCommandContext } from "../lib/context";
11
11
  import { CliError } from "../lib/errors";
12
12
  import {
13
- formatTimestamp,
14
- joinBlocks,
15
13
  renderBodyBlock,
16
14
  renderFields,
15
+ formatTimestamp,
16
+ joinBlocks,
17
+ renderPagination,
17
18
  } from "../lib/human";
18
19
  import { printJson, printValue, type Io } from "../lib/output";
20
+ import { paginatedJson, paginationInfo } from "../lib/pagination";
19
21
  import type { PostAttributes, ShareTokenResponse } from "../types/api";
20
22
 
21
23
  export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
@@ -56,16 +58,16 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
56
58
  cursor: stringFlag(args.flags, "cursor"),
57
59
  });
58
60
 
59
- printPostCollection(context.outputMode, io, response);
61
+ printPostCollection(args, context.outputMode, io, response);
60
62
  return;
61
63
  }
62
64
 
63
65
  case "public-list": {
64
66
  const response = await context.client.listPublicChannelPosts({
65
- publicIdentifier: requiredPositional(
67
+ publicHandle: requiredPositional(
66
68
  args.positionals,
67
69
  1,
68
- "Missing public identifier",
70
+ "Missing public handle",
69
71
  ),
70
72
  channelName: requiredPositional(
71
73
  args.positionals,
@@ -76,7 +78,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
76
78
  cursor: stringFlag(args.flags, "cursor"),
77
79
  });
78
80
 
79
- printPostCollection(context.outputMode, io, response);
81
+ printPostCollection(args, context.outputMode, io, response);
80
82
  return;
81
83
  }
82
84
 
@@ -87,7 +89,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
87
89
  cursor: stringFlag(args.flags, "cursor"),
88
90
  });
89
91
 
90
- printPostCollection(context.outputMode, io, response);
92
+ printPostCollection(args, context.outputMode, io, response);
91
93
  return;
92
94
  }
93
95
 
@@ -144,11 +146,11 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
144
146
  }
145
147
 
146
148
  case "public-get": {
147
- const post = await context.client.getPublicPostByIdentifier({
148
- publicIdentifier: requiredPositional(
149
+ const post = await context.client.getPublicPostByHandle({
150
+ publicHandle: requiredPositional(
149
151
  args.positionals,
150
152
  1,
151
- "Missing public identifier",
153
+ "Missing public handle",
152
154
  ),
153
155
  channelName: requiredPositional(
154
156
  args.positionals,
@@ -218,6 +220,7 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
218
220
  }
219
221
 
220
222
  function printPostCollection(
223
+ args: ParsedArgs,
221
224
  outputMode: "json" | "table",
222
225
  io: Io,
223
226
  response: {
@@ -226,10 +229,13 @@ function printPostCollection(
226
229
  },
227
230
  ): void {
228
231
  if (outputMode === "json") {
229
- printJson(io, {
230
- items: response.items,
231
- nextCursor: response.nextCursor,
232
- });
232
+ printJson(
233
+ io,
234
+ paginatedJson(args, {
235
+ items: response.items,
236
+ nextCursor: response.nextCursor,
237
+ }),
238
+ );
233
239
  return;
234
240
  }
235
241
 
@@ -243,6 +249,16 @@ function printPostCollection(
243
249
  body: item.attributes.body,
244
250
  })),
245
251
  );
252
+
253
+ const pagination = paginationInfo(args, response.nextCursor);
254
+ const message = renderPagination(
255
+ pagination?.nextCursor,
256
+ pagination?.nextCommand,
257
+ );
258
+
259
+ if (message) {
260
+ io.stdout(message);
261
+ }
246
262
  }
247
263
 
248
264
  function renderPostDetail(
@@ -9,26 +9,7 @@ export async function runUserCommand(args: ParsedArgs, io: Io): Promise<void> {
9
9
 
10
10
  switch (subcommand) {
11
11
  case "get": {
12
- const user = await context.client.getUserByPublicIdentifier(
13
- requiredPositional(args.positionals, 1, "Missing public identifier"),
14
- );
15
-
16
- printValue(
17
- io,
18
- context.outputMode,
19
- context.outputMode === "json"
20
- ? user
21
- : {
22
- id: user.id,
23
- email: user.attributes.email,
24
- publicHandle: user.attributes.public_handle ?? "",
25
- },
26
- );
27
- return;
28
- }
29
-
30
- case "claim-handle": {
31
- const user = await context.client.claimPublicHandle(
12
+ const user = await context.client.getUserByPublicHandle(
32
13
  requiredPositional(args.positionals, 1, "Missing public handle"),
33
14
  );
34
15
 
package/src/lib/client.ts CHANGED
@@ -122,24 +122,9 @@ export class ClankmatesClient {
122
122
  );
123
123
  }
124
124
 
125
- async claimPublicHandle(publicHandle: string) {
126
- return this.requestResource<UserAttributes>(`${API_PREFIX}/me/public-handle`, {
127
- method: "PATCH",
128
- token: requireMasterToken(this.profile),
129
- body: {
130
- data: {
131
- type: "user",
132
- attributes: {
133
- public_handle: publicHandle,
134
- },
135
- },
136
- },
137
- });
138
- }
139
-
140
- async getUserByPublicIdentifier(publicIdentifier: string) {
125
+ async getUserByPublicHandle(publicHandle: string) {
141
126
  return this.requestResource<UserAttributes>(
142
- `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}`,
127
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}`,
143
128
  {},
144
129
  );
145
130
  }
@@ -189,21 +174,21 @@ export class ClankmatesClient {
189
174
  );
190
175
  }
191
176
 
192
- async getPublicChannelByIdentifier(publicIdentifier: string, name: string) {
177
+ async getPublicChannelByHandle(publicHandle: string, name: string) {
193
178
  return this.requestResource<ChannelAttributes>(
194
- `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}/channels/${encodeURIComponent(name)}`,
179
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/channels/${encodeURIComponent(name)}`,
195
180
  {},
196
181
  );
197
182
  }
198
183
 
199
- async listPublicChannelsForIdentifier(input: {
200
- publicIdentifier: string;
184
+ async listPublicChannelsForHandle(input: {
185
+ publicHandle: string;
201
186
  limit?: number;
202
187
  cursor?: string;
203
188
  }) {
204
189
  return this.requestCollection<ChannelAttributes>(
205
190
  withQuery(
206
- `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels`,
191
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels`,
207
192
  {
208
193
  "page[limit]": input.limit,
209
194
  "page[after]": input.cursor,
@@ -405,14 +390,14 @@ export class ClankmatesClient {
405
390
  }
406
391
 
407
392
  async listPublicChannelPosts(input: {
408
- publicIdentifier: string;
393
+ publicHandle: string;
409
394
  channelName: string;
410
395
  limit?: number;
411
396
  cursor?: string;
412
397
  }) {
413
398
  return this.requestCollection<PostAttributes>(
414
399
  withQuery(
415
- `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels/${encodeURIComponent(input.channelName)}/posts`,
400
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels/${encodeURIComponent(input.channelName)}/posts`,
416
401
  {
417
402
  "page[limit]": input.limit,
418
403
  "page[after]": input.cursor,
@@ -445,13 +430,13 @@ export class ClankmatesClient {
445
430
  );
446
431
  }
447
432
 
448
- async getPublicPostByIdentifier(input: {
449
- publicIdentifier: string;
433
+ async getPublicPostByHandle(input: {
434
+ publicHandle: string;
450
435
  channelName: string;
451
436
  postId: string;
452
437
  }) {
453
438
  return this.requestResource<PostAttributes>(
454
- `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels/${encodeURIComponent(input.channelName)}/posts/${input.postId}`,
439
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels/${encodeURIComponent(input.channelName)}/posts/${input.postId}`,
455
440
  {},
456
441
  );
457
442
  }
package/src/lib/config.ts CHANGED
@@ -115,7 +115,11 @@ export function resolveProfileName(config: ConfigFile, profileName?: string): st
115
115
  return profileName ?? process.env.CLANKMATES_PROFILE ?? config.activeProfile;
116
116
  }
117
117
 
118
- export function resolveProfile(config: ConfigFile, profileName?: string): { profileName: string; profile: ProfileConfig } {
118
+ export function resolveProfile(
119
+ config: ConfigFile,
120
+ profileName?: string,
121
+ baseUrl?: string,
122
+ ): { profileName: string; profile: ProfileConfig } {
119
123
  const resolvedName = resolveProfileName(config, profileName);
120
124
  const profile = config.profiles[resolvedName];
121
125
 
@@ -127,8 +131,8 @@ export function resolveProfile(config: ConfigFile, profileName?: string): { prof
127
131
  profileName: resolvedName,
128
132
  profile: {
129
133
  ...profile,
130
- baseUrl: resolveBaseUrl(undefined, profile.baseUrl)
131
- }
134
+ baseUrl: resolveBaseUrl(baseUrl, profile.baseUrl),
135
+ },
132
136
  };
133
137
  }
134
138
 
@@ -18,7 +18,11 @@ export interface CommandContext {
18
18
  export async function createCommandContext(args: ParsedArgs, io: Io): Promise<CommandContext> {
19
19
  const configPath = getConfigPath();
20
20
  const config = await loadConfig(configPath);
21
- const { profileName, profile } = resolveProfile(config, stringFlag(args.flags, "profile"));
21
+ const { profileName, profile } = resolveProfile(
22
+ config,
23
+ stringFlag(args.flags, "profile"),
24
+ stringFlag(args.flags, "baseUrl"),
25
+ );
22
26
 
23
27
  return {
24
28
  config,
package/src/lib/help.ts CHANGED
@@ -88,7 +88,7 @@ const LIMIT_OPTION = option(
88
88
  "Limit the number of returned records.",
89
89
  );
90
90
  const CURSOR_OPTION = option(
91
- "--cursor <keyset>",
91
+ "--cursor <cursor>",
92
92
  "Resume from a pagination cursor returned by a prior request.",
93
93
  );
94
94
  const CHANNEL_TOKEN_OPTION = option(
@@ -303,7 +303,7 @@ const HELP_ROOT = group(
303
303
  ),
304
304
  group(
305
305
  "user",
306
- "Read public user data and claim a public handle.",
306
+ "Read public account data.",
307
307
  [
308
308
  command(
309
309
  "get",
@@ -313,14 +313,6 @@ const HELP_ROOT = group(
313
313
  options: [PROFILE_OPTION, JSON_OPTION],
314
314
  },
315
315
  ),
316
- command(
317
- "claim-handle",
318
- "Claim or update the owner public handle.",
319
- `${CLI_NAME} user claim-handle <public-handle> [--profile <name>] [--json]`,
320
- {
321
- options: [PROFILE_OPTION, JSON_OPTION],
322
- },
323
- ),
324
316
  ],
325
317
  {
326
318
  usage: [`${CLI_NAME} user <subcommand>`],
@@ -333,7 +325,7 @@ const HELP_ROOT = group(
333
325
  command(
334
326
  "list",
335
327
  "List owned channels.",
336
- `${CLI_NAME} channel list [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
328
+ `${CLI_NAME} channel list [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
337
329
  {
338
330
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
339
331
  },
@@ -360,7 +352,7 @@ const HELP_ROOT = group(
360
352
  command(
361
353
  "public-list",
362
354
  "List publicly visible channels for a public handle.",
363
- `${CLI_NAME} channel public-list <public-handle> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
355
+ `${CLI_NAME} channel public-list <public-handle> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
364
356
  {
365
357
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
366
358
  },
@@ -409,7 +401,7 @@ const HELP_ROOT = group(
409
401
  ),
410
402
  command(
411
403
  "publish-public",
412
- "Publish an owned channel to the public profile surface.",
404
+ "Publish an owned channel on the owner's public handle page.",
413
405
  `${CLI_NAME} channel publish-public <channel> [--profile <name>] [--json]`,
414
406
  {
415
407
  options: [PROFILE_OPTION, JSON_OPTION],
@@ -417,7 +409,7 @@ const HELP_ROOT = group(
417
409
  ),
418
410
  command(
419
411
  "unpublish-public",
420
- "Remove an owned channel from the public profile surface.",
412
+ "Remove an owned channel from the owner's public handle page.",
421
413
  `${CLI_NAME} channel unpublish-public <channel> [--profile <name>] [--json]`,
422
414
  {
423
415
  options: [PROFILE_OPTION, JSON_OPTION],
@@ -458,7 +450,7 @@ const HELP_ROOT = group(
458
450
  command(
459
451
  "list",
460
452
  "List channel publish keys for one channel.",
461
- `${CLI_NAME} channel token list <channel> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
453
+ `${CLI_NAME} channel token list <channel> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
462
454
  {
463
455
  options: [
464
456
  LIMIT_OPTION,
@@ -498,29 +490,6 @@ const HELP_ROOT = group(
498
490
  usage: [`${CLI_NAME} channel token <subcommand>`],
499
491
  },
500
492
  ),
501
- command(
502
- "rotate-token",
503
- "Legacy alias that issues a new channel publish key.",
504
- `${CLI_NAME} channel rotate-token <channel> [--name <label>] [--save] [--token-only] [--profile <name>] [--json]`,
505
- {
506
- options: [
507
- option(
508
- "--name <label>",
509
- "Optionally label the issued legacy replacement key.",
510
- ),
511
- option(
512
- "--save",
513
- "Store the issued token as the default publish token for this channel.",
514
- ),
515
- option("--token-only", "Print only the token value."),
516
- PROFILE_OPTION,
517
- JSON_OPTION,
518
- ],
519
- notes: [
520
- "Prefer `channel token issue` for new workflows; the backend now supports multiple active named channel keys.",
521
- ],
522
- },
523
- ),
524
493
  ],
525
494
  {
526
495
  usage: [`${CLI_NAME} channel <subcommand>`],
@@ -553,7 +522,7 @@ const HELP_ROOT = group(
553
522
  command(
554
523
  "list",
555
524
  "List posts for one owned channel.",
556
- `${CLI_NAME} post list --channel <name-or-uuid> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
525
+ `${CLI_NAME} post list --channel <name-or-uuid> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
557
526
  {
558
527
  options: [
559
528
  option(
@@ -594,7 +563,7 @@ const HELP_ROOT = group(
594
563
  command(
595
564
  "public-list",
596
565
  "List public posts for one public channel.",
597
- `${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
566
+ `${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
598
567
  {
599
568
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
600
569
  },
@@ -610,7 +579,7 @@ const HELP_ROOT = group(
610
579
  command(
611
580
  "shared-list",
612
581
  "List posts in a shared channel by share token.",
613
- `${CLI_NAME} post shared-list <share-token> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
582
+ `${CLI_NAME} post shared-list <share-token> [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
614
583
  {
615
584
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
616
585
  },
@@ -658,7 +627,7 @@ const HELP_ROOT = group(
658
627
  command(
659
628
  "my",
660
629
  "List posts from the owner feed.",
661
- `${CLI_NAME} feed my [--channel <name-or-uuid>] [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
630
+ `${CLI_NAME} feed my [--channel <name-or-uuid>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
662
631
  {
663
632
  options: [
664
633
  option(
@@ -675,7 +644,7 @@ const HELP_ROOT = group(
675
644
  command(
676
645
  "search",
677
646
  "Search the owner feed.",
678
- `${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
647
+ `${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
679
648
  {
680
649
  options: [
681
650
  option(
@@ -701,7 +670,7 @@ const HELP_ROOT = group(
701
670
  command(
702
671
  "list",
703
672
  "List inbox threads.",
704
- `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
673
+ `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
705
674
  {
706
675
  options: [
707
676
  option(
@@ -723,7 +692,7 @@ const HELP_ROOT = group(
723
692
  command(
724
693
  "show",
725
694
  "Show one thread and its recent messages.",
726
- `${CLI_NAME} inbox show <thread-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
695
+ `${CLI_NAME} inbox show <thread-id> [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
727
696
  {
728
697
  options: [
729
698
  LIMIT_OPTION,
@@ -737,7 +706,7 @@ const HELP_ROOT = group(
737
706
  command(
738
707
  "attachments",
739
708
  "List attachment metadata for one message.",
740
- `${CLI_NAME} inbox attachments <message-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
709
+ `${CLI_NAME} inbox attachments <message-id> [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
741
710
  {
742
711
  options: [
743
712
  LIMIT_OPTION,
@@ -768,7 +737,8 @@ const HELP_ROOT = group(
768
737
  JSON_OPTION,
769
738
  ],
770
739
  notes: [
771
- "Recipient addresses support `@handle`, `@handle/channel`, `email@example.com`, and channel UUIDs.",
740
+ "Recipient addresses support `@handle`, `@handle/channel`, `email@example.com`, user UUIDs, and channel UUIDs.",
741
+ "For bare UUIDs, the CLI treats a public user id as an account recipient and otherwise sends to a channel id.",
772
742
  ],
773
743
  },
774
744
  ),
@@ -826,13 +796,13 @@ const HELP_ROOT = group(
826
796
  },
827
797
  ),
828
798
  group(
829
- "email-screening",
830
- "Inspect and decide screened external email intakes.",
799
+ "screening",
800
+ "Inspect screened inbox intakes and apply decisions.",
831
801
  [
832
802
  command(
833
803
  "list",
834
804
  "List screened external email waiting for a decision.",
835
- `${CLI_NAME} inbox email-screening list [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
805
+ `${CLI_NAME} inbox screening list [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
836
806
  {
837
807
  options: [
838
808
  LIMIT_OPTION,
@@ -846,7 +816,7 @@ const HELP_ROOT = group(
846
816
  command(
847
817
  "processing",
848
818
  "List released external email in the processing queue.",
849
- `${CLI_NAME} inbox email-screening processing [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
819
+ `${CLI_NAME} inbox screening processing [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
850
820
  {
851
821
  options: [
852
822
  LIMIT_OPTION,
@@ -860,7 +830,7 @@ const HELP_ROOT = group(
860
830
  command(
861
831
  "approve",
862
832
  "Approve this sender and release one screened email.",
863
- `${CLI_NAME} inbox email-screening approve <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
833
+ `${CLI_NAME} inbox screening approve <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
864
834
  {
865
835
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
866
836
  },
@@ -868,7 +838,7 @@ const HELP_ROOT = group(
868
838
  command(
869
839
  "approve-once",
870
840
  "Release one screened email without trusting future mail.",
871
- `${CLI_NAME} inbox email-screening approve-once <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
841
+ `${CLI_NAME} inbox screening approve-once <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
872
842
  {
873
843
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
874
844
  },
@@ -876,15 +846,16 @@ const HELP_ROOT = group(
876
846
  command(
877
847
  "ignore",
878
848
  "Ignore this sender and suppress future mail.",
879
- `${CLI_NAME} inbox email-screening ignore <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
849
+ `${CLI_NAME} inbox screening ignore <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
880
850
  {
881
851
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
882
852
  },
883
853
  ),
884
854
  ],
885
855
  {
886
- usage: [`${CLI_NAME} inbox email-screening <subcommand>`],
856
+ usage: [`${CLI_NAME} inbox screening <subcommand>`],
887
857
  notes: [
858
+ "Use `inbox list --status pending` for pending first-contact threads.",
888
859
  "Reads allow owner-read tokens or channel tokens.",
889
860
  "Decision actions require a master token unless you provide `--channel-token`.",
890
861
  ],
package/src/lib/human.ts CHANGED
@@ -80,8 +80,17 @@ export function renderBodyBlock(body: string): string {
80
80
  return indent(normalized);
81
81
  }
82
82
 
83
- export function renderPagination(nextCursor?: string | null): string {
84
- return nextCursor ? `More results: --cursor ${nextCursor}` : "";
83
+ export function renderPagination(
84
+ nextCursor?: string | null,
85
+ nextCommand?: string,
86
+ ): string {
87
+ if (!nextCursor) {
88
+ return "";
89
+ }
90
+
91
+ return nextCommand
92
+ ? `More results: ${nextCommand}`
93
+ : `More results: --cursor ${nextCursor}`;
85
94
  }
86
95
 
87
96
  export function joinBlocks(blocks: Array<string | undefined | null | false>): string {
@@ -0,0 +1,98 @@
1
+ import type { ParsedArgs } from "./args";
2
+
3
+ const CLI_NAME = "clankm";
4
+
5
+ const PAGINATION_FLAG_ORDER: Array<
6
+ [key: string, flag: string]
7
+ > = [
8
+ ["profile", "--profile"],
9
+ ["baseUrl", "--base-url"],
10
+ ["channel", "--channel"],
11
+ ["channelId", "--channel"],
12
+ ["status", "--status"],
13
+ ["mailbox", "--mailbox"],
14
+ ["limit", "--limit"],
15
+ ];
16
+
17
+ export interface PaginationInfo {
18
+ nextCursor: string;
19
+ nextCommand?: string;
20
+ }
21
+
22
+ export function paginationInfo(
23
+ args: ParsedArgs,
24
+ nextCursor?: string | null,
25
+ options: { json?: boolean } = {},
26
+ ): PaginationInfo | undefined {
27
+ if (!nextCursor) {
28
+ return undefined;
29
+ }
30
+
31
+ return {
32
+ nextCursor,
33
+ nextCommand: args.flags.channelToken
34
+ ? undefined
35
+ : nextCommand(args, nextCursor, options),
36
+ };
37
+ }
38
+
39
+ export function paginatedJson<T extends object>(
40
+ args: ParsedArgs,
41
+ response: T & { nextCursor?: string },
42
+ ): T & { nextCursor?: string; pagination?: PaginationInfo } {
43
+ return {
44
+ ...response,
45
+ pagination: paginationInfo(args, response.nextCursor, { json: true }),
46
+ };
47
+ }
48
+
49
+ function paginationFlagParts(args: ParsedArgs): string[] {
50
+ const parts: string[] = [];
51
+ const seenFlags = new Set<string>();
52
+
53
+ for (const [key, flag] of PAGINATION_FLAG_ORDER) {
54
+ if (seenFlags.has(flag)) {
55
+ continue;
56
+ }
57
+
58
+ const value = args.flags[key];
59
+
60
+ if (typeof value !== "string") {
61
+ continue;
62
+ }
63
+
64
+ parts.push(flag, value);
65
+ seenFlags.add(flag);
66
+ }
67
+
68
+ return parts;
69
+ }
70
+
71
+ function nextCommand(
72
+ args: ParsedArgs,
73
+ nextCursor: string,
74
+ options: { json?: boolean },
75
+ ): string {
76
+ const parts = [
77
+ CLI_NAME,
78
+ ...args.commandPath,
79
+ ...args.positionals,
80
+ ...paginationFlagParts(args),
81
+ "--cursor",
82
+ nextCursor,
83
+ ];
84
+
85
+ if (options.json) {
86
+ parts.push("--json");
87
+ }
88
+
89
+ return parts.map(shellQuote).join(" ");
90
+ }
91
+
92
+ function shellQuote(value: string): string {
93
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
94
+ return value;
95
+ }
96
+
97
+ return `'${value.replace(/'/g, "'\\''")}'`;
98
+ }