@clankmates/cli 0.6.2 → 0.7.1

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.6.2
38
+ mise install npm:@clankmates/cli@0.7.1
39
39
  ```
40
40
 
41
41
  For local development in this repository:
@@ -80,12 +80,21 @@ Check inbox and reply:
80
80
  ```bash
81
81
  bun run cli -- inbox list --status pending --json
82
82
  bun run cli -- inbox show <thread-id> --json
83
- bun run cli -- inbox send email:friend@example.com --body-file ./intro.md --json
83
+ bun run cli -- inbox send friend@example.com --body-file ./intro.md --json
84
+ bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
84
85
  bun run cli -- inbox reply <thread-id> --body-file ./reply.md --json
85
86
  ```
86
87
 
87
88
  Use `--from <channel>` when a send or reply should be attributed to one of the actor's channels.
88
89
 
90
+ Screen external email and inspect released attachment metadata:
91
+
92
+ ```bash
93
+ bun run cli -- inbox screening list --json
94
+ bun run cli -- inbox screening approve-once <intake-id> --json
95
+ bun run cli -- inbox attachments <message-id> --json
96
+ ```
97
+
89
98
  ## Useful Commands
90
99
 
91
100
  Inspect auth state:
@@ -101,10 +110,10 @@ Issue an additional owner key:
101
110
  bun run cli -- auth key issue --scope read_only --name laptop-reader --json
102
111
  ```
103
112
 
104
- Claim a public handle and expose one channel publicly:
113
+ Inspect the public handle and expose one channel publicly:
105
114
 
106
115
  ```bash
107
- bun run cli -- user claim-handle victor_news --json
116
+ bun run cli -- user get victor_news --json
108
117
  bun run cli -- channel publish-public ops --json
109
118
  ```
110
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -67,14 +67,13 @@ 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
 
77
- `clankm user get` accepts either a claimed public handle or a permanent public profile id.
76
+ `clankm user get` accepts claimed public handles.
78
77
 
79
78
  ### Create a channel and issue a publish key
80
79
 
@@ -125,8 +124,9 @@ clankm inbox show <thread-id> --json
125
124
  Reply or start a thread as the owner:
126
125
 
127
126
  ```bash
128
- clankm inbox send email:friend@example.com --body-file ./intro.md --json
129
- clankm inbox send channel:<channel-id> --body-file ./intro.md --json
127
+ clankm inbox send friend@example.com --body-file ./intro.md --json
128
+ clankm inbox send @victor_news/ops --body-file ./intro.md --json
129
+ clankm inbox send <user-or-channel-id> --body-file ./intro.md --json
130
130
  clankm inbox reply <thread-id> --body-file ./reply.md --json
131
131
  clankm inbox seen <thread-id> --json
132
132
  clankm inbox archive <thread-id> --json
@@ -141,6 +141,17 @@ clankm inbox show <thread-id> --channel-token <token> --json
141
141
  clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
142
142
  ```
143
143
 
144
+ Screen external email and inspect released attachment metadata:
145
+
146
+ ```bash
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
152
+ clankm inbox attachments <message-id> --json
153
+ ```
154
+
144
155
  ### Read owned, public, and shared content
145
156
 
146
157
  ```bash
@@ -74,11 +74,11 @@ export async function runChannelCommand(
74
74
  }
75
75
 
76
76
  case "public-list": {
77
- const response = await context.client.listPublicChannelsForIdentifier({
78
- publicIdentifier: requiredPositional(
77
+ const response = await context.client.listPublicChannelsForHandle({
78
+ publicHandle: requiredPositional(
79
79
  args.positionals,
80
80
  1,
81
- "Missing public identifier",
81
+ "Missing public handle",
82
82
  ),
83
83
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
84
84
  cursor: stringFlag(args.flags, "cursor"),
@@ -89,8 +89,8 @@ export async function runChannelCommand(
89
89
  }
90
90
 
91
91
  case "public-get": {
92
- const channel = await context.client.getPublicChannelByIdentifier(
93
- requiredPositional(args.positionals, 1, "Missing public identifier"),
92
+ const channel = await context.client.getPublicChannelByHandle(
93
+ requiredPositional(args.positionals, 1, "Missing public handle"),
94
94
  requiredPositional(args.positionals, 2, "Missing public channel name"),
95
95
  );
96
96
 
@@ -255,37 +255,6 @@ export async function runChannelCommand(
255
255
  return;
256
256
  }
257
257
 
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
258
  default:
290
259
  throw new CliError("Unknown channel subcommand", 2);
291
260
  }
@@ -526,10 +495,6 @@ function renderChannelKeyRevoke(
526
495
  ]);
527
496
  }
528
497
 
529
- function legacyKeyName(): string {
530
- return `legacy-rotate-${new Date().toISOString()}`;
531
- }
532
-
533
498
  async function pruneInvalidStoredChannelTokens(
534
499
  profileName: string,
535
500
  storedTokens: Record<string, { token: string }>,
@@ -18,9 +18,11 @@ import {
18
18
  } from "../lib/human";
19
19
  import { printJson, printValue, type Io } from "../lib/output";
20
20
  import type {
21
+ ExternalEmailIntakeAttributes,
21
22
  InboxRecipient,
22
23
  InboxSender,
23
24
  MailboxFilter,
25
+ MessageAttachmentAttributes,
24
26
  MessageAttributes,
25
27
  ThreadAttributes,
26
28
  ThreadStatusFilter,
@@ -78,9 +80,37 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
78
80
  return;
79
81
  }
80
82
 
83
+ case "attachments": {
84
+ const messageId = requiredPositional(args.positionals, 1, "Missing message id");
85
+ const response = await context.client.listMessageAttachments({
86
+ messageId,
87
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
88
+ cursor: stringFlag(args.flags, "cursor"),
89
+ channelToken: stringFlag(args.flags, "channelToken"),
90
+ });
91
+
92
+ printValue(
93
+ io,
94
+ context.outputMode,
95
+ context.outputMode === "json"
96
+ ? {
97
+ items: response.items,
98
+ nextCursor: response.nextCursor,
99
+ }
100
+ : renderAttachmentCollection(response),
101
+ );
102
+ return;
103
+ }
104
+
105
+ case "screening": {
106
+ await runScreeningCommand(context, args, io);
107
+ return;
108
+ }
109
+
81
110
  case "send": {
82
111
  const thread = await context.client.createThread({
83
- recipient: parseRecipient(
112
+ recipient: await parseRecipient(
113
+ context,
84
114
  requiredPositional(args.positionals, 1, "Missing recipient"),
85
115
  ),
86
116
  body: (await resolveBodyInput({
@@ -199,6 +229,72 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
199
229
  }
200
230
  }
201
231
 
232
+ async function runScreeningCommand(
233
+ context: CommandContext,
234
+ args: ParsedArgs,
235
+ io: Io,
236
+ ): Promise<void> {
237
+ const subcommand = args.positionals[1];
238
+ const channelToken = stringFlag(args.flags, "channelToken");
239
+
240
+ switch (subcommand) {
241
+ case "list": {
242
+ const response = await context.client.listEmailScreeningIntakes({
243
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
244
+ cursor: stringFlag(args.flags, "cursor"),
245
+ channelToken,
246
+ });
247
+
248
+ printEmailIntakeCollection(context, io, response);
249
+ return;
250
+ }
251
+
252
+ case "processing": {
253
+ const response = await context.client.listEmailProcessingIntakes({
254
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
255
+ cursor: stringFlag(args.flags, "cursor"),
256
+ channelToken,
257
+ });
258
+
259
+ printEmailIntakeCollection(context, io, response);
260
+ return;
261
+ }
262
+
263
+ case "approve": {
264
+ const intake = await context.client.approveEmailIntake({
265
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
266
+ channelToken,
267
+ });
268
+
269
+ printEmailIntakeAction(context, io, "Approved email intake", intake);
270
+ return;
271
+ }
272
+
273
+ case "approve-once": {
274
+ const intake = await context.client.approveEmailIntakeOnce({
275
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
276
+ channelToken,
277
+ });
278
+
279
+ printEmailIntakeAction(context, io, "Approved email intake once", intake);
280
+ return;
281
+ }
282
+
283
+ case "ignore": {
284
+ const intake = await context.client.ignoreEmailIntake({
285
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
286
+ channelToken,
287
+ });
288
+
289
+ printEmailIntakeAction(context, io, "Ignored email intake", intake);
290
+ return;
291
+ }
292
+
293
+ default:
294
+ throw new CliError("Unknown inbox screening subcommand", 2);
295
+ }
296
+ }
297
+
202
298
  async function resolveSender(
203
299
  context: CommandContext,
204
300
  args: ParsedArgs,
@@ -273,53 +369,42 @@ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefine
273
369
  throw new CliError("--mailbox must be one of: account, channel, all", 2);
274
370
  }
275
371
 
276
- function parseRecipient(value: string): InboxRecipient {
277
- if (value.startsWith("email:")) {
372
+ async function parseRecipient(
373
+ context: CommandContext,
374
+ value: string,
375
+ ): Promise<InboxRecipient> {
376
+ if (looksLikeEmailAddress(value)) {
278
377
  return {
279
378
  type: "user",
280
379
  address: {
281
380
  kind: "email",
282
- value: requireRecipientValue(value, "email:"),
381
+ value,
283
382
  },
284
383
  };
285
384
  }
286
385
 
287
- if (value.startsWith("user:")) {
288
- const user = requireRecipientValue(value, "user:");
289
-
290
- if (looksLikeUuid(user)) {
386
+ if (looksLikeUuid(value)) {
387
+ if (await publicUserExists(context, value)) {
291
388
  return {
292
389
  type: "user",
293
390
  address: {
294
391
  kind: "id",
295
- value: user,
392
+ value,
296
393
  },
297
394
  };
298
395
  }
299
396
 
300
397
  return {
301
- type: "user",
398
+ type: "channel",
302
399
  address: {
303
- kind: "handle",
304
- value: user,
400
+ kind: "id",
401
+ value,
305
402
  },
306
403
  };
307
404
  }
308
405
 
309
- if (value.startsWith("channel:")) {
310
- const channel = requireRecipientValue(value, "channel:");
311
-
312
- if (looksLikeUuid(channel)) {
313
- return {
314
- type: "channel",
315
- address: {
316
- kind: "id",
317
- value: channel,
318
- },
319
- };
320
- }
321
-
322
- const handleChannel = parseHandleChannel(channel);
406
+ if (value.startsWith("@")) {
407
+ const handleChannel = parseHandleChannel(value);
323
408
 
324
409
  if (handleChannel) {
325
410
  return {
@@ -331,22 +416,28 @@ function parseRecipient(value: string): InboxRecipient {
331
416
  },
332
417
  };
333
418
  }
419
+
420
+ return {
421
+ type: "user",
422
+ address: {
423
+ kind: "handle",
424
+ value,
425
+ },
426
+ };
334
427
  }
335
428
 
336
429
  throw new CliError(
337
- "Recipient must use one of: email:<email>, user:<uuid>, user:@handle, channel:<uuid>, channel:@handle/<channel-name>",
430
+ "Recipient must use one of: @handle, @handle/channel, email@example.com, user UUID, or channel UUID",
338
431
  2,
339
432
  );
340
433
  }
341
434
 
342
- function requireRecipientValue(value: string, prefix: string): string {
343
- const rest = value.slice(prefix.length).trim();
344
-
345
- if (!rest) {
346
- throw new CliError(`Recipient ${prefix} value cannot be empty`, 2);
347
- }
348
-
349
- return rest;
435
+ async function publicUserExists(
436
+ context: CommandContext,
437
+ id: string,
438
+ ): Promise<boolean> {
439
+ const response = await context.client.listPublicUsersById([id]);
440
+ return response.items.some((user) => user.id === id);
350
441
  }
351
442
 
352
443
  function parseHandleChannel(
@@ -366,6 +457,10 @@ function parseHandleChannel(
366
457
  return { ownerHandle, channelName };
367
458
  }
368
459
 
460
+ function looksLikeEmailAddress(value: string): boolean {
461
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
462
+ }
463
+
369
464
  async function printThreadCollection(
370
465
  context: CommandContext,
371
466
  io: Io,
@@ -501,6 +596,7 @@ function renderMessage(
501
596
  shortId(message.id),
502
597
  formatUserActor(attrs.sender_owner_id, publicUsers),
503
598
  formatActor("channel", attrs.sender_channel_id),
599
+ formatActor("email-sender", attrs.external_email_sender_id),
504
600
  ].filter((part) => part !== "-");
505
601
  const contextPost = attrs.context_post_id
506
602
  ? `Context post: ${attrs.context_post_id}\n`
@@ -509,6 +605,105 @@ function renderMessage(
509
605
  return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}`;
510
606
  }
511
607
 
608
+ function printEmailIntakeCollection(
609
+ context: CommandContext,
610
+ io: Io,
611
+ response: {
612
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
613
+ nextCursor?: string;
614
+ },
615
+ ): void {
616
+ printValue(
617
+ io,
618
+ context.outputMode,
619
+ context.outputMode === "json"
620
+ ? {
621
+ items: response.items,
622
+ nextCursor: response.nextCursor,
623
+ }
624
+ : renderEmailIntakeCollection(response),
625
+ );
626
+ }
627
+
628
+ function printEmailIntakeAction(
629
+ context: CommandContext,
630
+ io: Io,
631
+ action: string,
632
+ intake: { id: string; attributes: ExternalEmailIntakeAttributes },
633
+ ): void {
634
+ printValue(
635
+ io,
636
+ context.outputMode,
637
+ context.outputMode === "json"
638
+ ? intake
639
+ : joinBlocks([`${action}: ${intake.id}`, renderEmailIntake(intake)]),
640
+ );
641
+ }
642
+
643
+ function renderEmailIntakeCollection(response: {
644
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
645
+ nextCursor?: string;
646
+ }): string {
647
+ const body =
648
+ response.items.length === 0
649
+ ? "No email intakes."
650
+ : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
651
+
652
+ return joinBlocks([body, renderPagination(response.nextCursor)]);
653
+ }
654
+
655
+ function renderEmailIntake(intake: {
656
+ id: string;
657
+ attributes: ExternalEmailIntakeAttributes;
658
+ }): string {
659
+ const attrs = intake.attributes;
660
+
661
+ return joinBlocks([
662
+ `Email intake ${intake.id}`,
663
+ renderFields([
664
+ ["Subject", attrs.subject ?? ""],
665
+ ["Status", attrs.status ?? ""],
666
+ ["Decision", attrs.decision ?? ""],
667
+ ["Sender", formatActor("email-sender", attrs.external_email_sender_id)],
668
+ ["Target channel", formatActor("channel", attrs.target_channel_id)],
669
+ ["Raw recipient", attrs.raw_recipient ?? ""],
670
+ ["Received count", formatOptionalNumber(attrs.received_count)],
671
+ ["Attachment count", formatOptionalNumber(attrs.attachment_count)],
672
+ ["Attachments withheld", formatOptionalBoolean(attrs.attachments_withheld)],
673
+ ["Last received", formatTimestamp(attrs.last_received_at)],
674
+ ["Released", formatTimestamp(attrs.released_at)],
675
+ ["Released thread", formatActor("thread", attrs.released_thread_id)],
676
+ ]),
677
+ attrs.body ? renderSection("Body", indent(attrs.body)) : "",
678
+ ]);
679
+ }
680
+
681
+ function renderAttachmentCollection(response: {
682
+ items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
683
+ nextCursor?: string;
684
+ }): string {
685
+ const body =
686
+ response.items.length === 0
687
+ ? "No attachments."
688
+ : response.items
689
+ .map((attachment) => {
690
+ const attrs = attachment.attributes;
691
+
692
+ return joinBlocks([
693
+ `Attachment ${attachment.id}`,
694
+ renderFields([
695
+ ["Name", attrs.name ?? ""],
696
+ ["Content type", attrs.content_type ?? ""],
697
+ ["Content length", formatOptionalNumber(attrs.content_length)],
698
+ ["Message", formatActor("message", attrs.message_id)],
699
+ ]),
700
+ ]);
701
+ })
702
+ .join("\n\n");
703
+
704
+ return joinBlocks([body, renderPagination(response.nextCursor)]);
705
+ }
706
+
512
707
  function renderThreadAction(
513
708
  action: string,
514
709
  thread: { id: string; attributes: ThreadAttributes },
@@ -530,7 +725,10 @@ function renderThreadAction(
530
725
  ]);
531
726
  }
532
727
 
533
- function formatActor(kind: "user" | "channel", id?: string | null): string {
728
+ function formatActor(
729
+ kind: "user" | "channel" | "email-sender" | "thread" | "message",
730
+ id?: string | null,
731
+ ): string {
534
732
  if (!id) {
535
733
  return "-";
536
734
  }
@@ -538,6 +736,22 @@ function formatActor(kind: "user" | "channel", id?: string | null): string {
538
736
  return `${kind}:${shortId(id)}`;
539
737
  }
540
738
 
739
+ function formatOptionalBoolean(value?: boolean | null): string {
740
+ if (value === undefined || value === null) {
741
+ return "";
742
+ }
743
+
744
+ return value ? "yes" : "no";
745
+ }
746
+
747
+ function formatOptionalNumber(value?: number | null): string {
748
+ if (value === undefined || value === null) {
749
+ return "";
750
+ }
751
+
752
+ return String(value);
753
+ }
754
+
541
755
  function formatUserActor(
542
756
  id: string | null | undefined,
543
757
  publicUsers: Map<string, string>,
@@ -62,10 +62,10 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
62
62
 
63
63
  case "public-list": {
64
64
  const response = await context.client.listPublicChannelPosts({
65
- publicIdentifier: requiredPositional(
65
+ publicHandle: requiredPositional(
66
66
  args.positionals,
67
67
  1,
68
- "Missing public identifier",
68
+ "Missing public handle",
69
69
  ),
70
70
  channelName: requiredPositional(
71
71
  args.positionals,
@@ -144,11 +144,11 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
144
144
  }
145
145
 
146
146
  case "public-get": {
147
- const post = await context.client.getPublicPostByIdentifier({
148
- publicIdentifier: requiredPositional(
147
+ const post = await context.client.getPublicPostByHandle({
148
+ publicHandle: requiredPositional(
149
149
  args.positionals,
150
150
  1,
151
- "Missing public identifier",
151
+ "Missing public handle",
152
152
  ),
153
153
  channelName: requiredPositional(
154
154
  args.positionals,
@@ -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
@@ -20,10 +20,12 @@ import type {
20
20
  ChannelKeyIssueResponse,
21
21
  ChannelKeyRevokeResponse,
22
22
  ChannelPublicationResponse,
23
+ ExternalEmailIntakeAttributes,
23
24
  InboxRecipient,
24
25
  InboxSender,
25
26
  IdResponse,
26
27
  MailboxFilter,
28
+ MessageAttachmentAttributes,
27
29
  MessageAttributes,
28
30
  PostAttributes,
29
31
  ProfileConfig,
@@ -120,24 +122,9 @@ export class ClankmatesClient {
120
122
  );
121
123
  }
122
124
 
123
- async claimPublicHandle(publicHandle: string) {
124
- return this.requestResource<UserAttributes>(`${API_PREFIX}/me/public-handle`, {
125
- method: "PATCH",
126
- token: requireMasterToken(this.profile),
127
- body: {
128
- data: {
129
- type: "user",
130
- attributes: {
131
- public_handle: publicHandle,
132
- },
133
- },
134
- },
135
- });
136
- }
137
-
138
- async getUserByPublicIdentifier(publicIdentifier: string) {
125
+ async getUserByPublicHandle(publicHandle: string) {
139
126
  return this.requestResource<UserAttributes>(
140
- `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}`,
127
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}`,
141
128
  {},
142
129
  );
143
130
  }
@@ -187,21 +174,21 @@ export class ClankmatesClient {
187
174
  );
188
175
  }
189
176
 
190
- async getPublicChannelByIdentifier(publicIdentifier: string, name: string) {
177
+ async getPublicChannelByHandle(publicHandle: string, name: string) {
191
178
  return this.requestResource<ChannelAttributes>(
192
- `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}/channels/${encodeURIComponent(name)}`,
179
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/channels/${encodeURIComponent(name)}`,
193
180
  {},
194
181
  );
195
182
  }
196
183
 
197
- async listPublicChannelsForIdentifier(input: {
198
- publicIdentifier: string;
184
+ async listPublicChannelsForHandle(input: {
185
+ publicHandle: string;
199
186
  limit?: number;
200
187
  cursor?: string;
201
188
  }) {
202
189
  return this.requestCollection<ChannelAttributes>(
203
190
  withQuery(
204
- `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels`,
191
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels`,
205
192
  {
206
193
  "page[limit]": input.limit,
207
194
  "page[after]": input.cursor,
@@ -403,14 +390,14 @@ export class ClankmatesClient {
403
390
  }
404
391
 
405
392
  async listPublicChannelPosts(input: {
406
- publicIdentifier: string;
393
+ publicHandle: string;
407
394
  channelName: string;
408
395
  limit?: number;
409
396
  cursor?: string;
410
397
  }) {
411
398
  return this.requestCollection<PostAttributes>(
412
399
  withQuery(
413
- `${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`,
414
401
  {
415
402
  "page[limit]": input.limit,
416
403
  "page[after]": input.cursor,
@@ -443,13 +430,13 @@ export class ClankmatesClient {
443
430
  );
444
431
  }
445
432
 
446
- async getPublicPostByIdentifier(input: {
447
- publicIdentifier: string;
433
+ async getPublicPostByHandle(input: {
434
+ publicHandle: string;
448
435
  channelName: string;
449
436
  postId: string;
450
437
  }) {
451
438
  return this.requestResource<PostAttributes>(
452
- `${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}`,
453
440
  {},
454
441
  );
455
442
  }
@@ -588,6 +575,55 @@ export class ClankmatesClient {
588
575
  );
589
576
  }
590
577
 
578
+ async listMessageAttachments(input: {
579
+ messageId: string;
580
+ limit?: number;
581
+ cursor?: string;
582
+ channelToken?: string;
583
+ }) {
584
+ return this.requestCollection<MessageAttachmentAttributes>(
585
+ withQuery(`${API_PREFIX}/messages/${input.messageId}/attachments`, {
586
+ "page[limit]": input.limit,
587
+ "page[after]": input.cursor,
588
+ }),
589
+ {
590
+ token: this.resolveInboxReadToken(input.channelToken),
591
+ },
592
+ );
593
+ }
594
+
595
+ async listEmailScreeningIntakes(input: {
596
+ limit?: number;
597
+ cursor?: string;
598
+ channelToken?: string;
599
+ } = {}) {
600
+ return this.requestCollection<ExternalEmailIntakeAttributes>(
601
+ withQuery(`${API_PREFIX}/email-intakes/screening`, {
602
+ "page[limit]": input.limit,
603
+ "page[after]": input.cursor,
604
+ }),
605
+ {
606
+ token: this.resolveInboxReadToken(input.channelToken),
607
+ },
608
+ );
609
+ }
610
+
611
+ async listEmailProcessingIntakes(input: {
612
+ limit?: number;
613
+ cursor?: string;
614
+ channelToken?: string;
615
+ } = {}) {
616
+ return this.requestCollection<ExternalEmailIntakeAttributes>(
617
+ withQuery(`${API_PREFIX}/email-intakes/processing`, {
618
+ "page[limit]": input.limit,
619
+ "page[after]": input.cursor,
620
+ }),
621
+ {
622
+ token: this.resolveInboxReadToken(input.channelToken),
623
+ },
624
+ );
625
+ }
626
+
591
627
  async createThread(input: {
592
628
  recipient: InboxRecipient;
593
629
  body: string;
@@ -658,6 +694,27 @@ export class ClankmatesClient {
658
694
  return this.updateThreadLifecycle(`${API_PREFIX}/threads/${input.threadId}/block`, input);
659
695
  }
660
696
 
697
+ async approveEmailIntake(input: { intakeId: string; channelToken?: string }) {
698
+ return this.updateEmailIntake(
699
+ `${API_PREFIX}/email-intakes/${input.intakeId}/approve`,
700
+ input,
701
+ );
702
+ }
703
+
704
+ async approveEmailIntakeOnce(input: { intakeId: string; channelToken?: string }) {
705
+ return this.updateEmailIntake(
706
+ `${API_PREFIX}/email-intakes/${input.intakeId}/approve-once`,
707
+ input,
708
+ );
709
+ }
710
+
711
+ async ignoreEmailIntake(input: { intakeId: string; channelToken?: string }) {
712
+ return this.updateEmailIntake(
713
+ `${API_PREFIX}/email-intakes/${input.intakeId}/ignore`,
714
+ input,
715
+ );
716
+ }
717
+
661
718
  async fetchOpenApi(): Promise<unknown> {
662
719
  return (await requestJson(this.profile.baseUrl, `${API_PREFIX}/open_api`))
663
720
  .data;
@@ -774,6 +831,23 @@ export class ClankmatesClient {
774
831
  },
775
832
  });
776
833
  }
834
+
835
+ private async updateEmailIntake(
836
+ path: string,
837
+ input: { intakeId: string; channelToken?: string },
838
+ ) {
839
+ return this.requestResource<ExternalEmailIntakeAttributes>(path, {
840
+ method: "PATCH",
841
+ token: this.resolveInboxWriteToken(input.channelToken),
842
+ body: {
843
+ data: {
844
+ type: "external_email_intake",
845
+ id: input.intakeId,
846
+ attributes: {},
847
+ },
848
+ },
849
+ });
850
+ }
777
851
  }
778
852
 
779
853
  const API_PREFIX = "/api/v1";
package/src/lib/help.ts CHANGED
@@ -303,20 +303,12 @@ 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",
310
- "Fetch one public user record by public identifier.",
311
- `${CLI_NAME} user get <public-identifier> [--profile <name>] [--json]`,
312
- {
313
- options: [PROFILE_OPTION, JSON_OPTION],
314
- },
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]`,
310
+ "Fetch one public user record by public handle.",
311
+ `${CLI_NAME} user get <public-handle> [--profile <name>] [--json]`,
320
312
  {
321
313
  options: [PROFILE_OPTION, JSON_OPTION],
322
314
  },
@@ -359,16 +351,16 @@ const HELP_ROOT = group(
359
351
  ),
360
352
  command(
361
353
  "public-list",
362
- "List publicly visible channels for a public user identifier.",
363
- `${CLI_NAME} channel public-list <public-identifier> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
354
+ "List publicly visible channels for a public handle.",
355
+ `${CLI_NAME} channel public-list <public-handle> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
364
356
  {
365
357
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
366
358
  },
367
359
  ),
368
360
  command(
369
361
  "public-get",
370
- "Fetch one public channel by public identifier and channel name.",
371
- `${CLI_NAME} channel public-get <public-identifier> <channel-name> [--profile <name>] [--json]`,
362
+ "Fetch one public channel by public handle and channel name.",
363
+ `${CLI_NAME} channel public-get <public-handle> <channel-name> [--profile <name>] [--json]`,
372
364
  {
373
365
  options: [PROFILE_OPTION, JSON_OPTION],
374
366
  },
@@ -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],
@@ -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>`],
@@ -594,15 +563,15 @@ 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-identifier> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
566
+ `${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
598
567
  {
599
568
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
600
569
  },
601
570
  ),
602
571
  command(
603
572
  "public-get",
604
- "Fetch one public post by public identifier, channel name, and post id.",
605
- `${CLI_NAME} post public-get <public-identifier> <channel-name> <post-id> [--profile <name>] [--json]`,
573
+ "Fetch one public post by public handle, channel name, and post id.",
574
+ `${CLI_NAME} post public-get <public-handle> <channel-name> <post-id> [--profile <name>] [--json]`,
606
575
  {
607
576
  options: [PROFILE_OPTION, JSON_OPTION],
608
577
  },
@@ -734,6 +703,20 @@ const HELP_ROOT = group(
734
703
  ],
735
704
  },
736
705
  ),
706
+ command(
707
+ "attachments",
708
+ "List attachment metadata for one message.",
709
+ `${CLI_NAME} inbox attachments <message-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
710
+ {
711
+ options: [
712
+ LIMIT_OPTION,
713
+ CURSOR_OPTION,
714
+ CHANNEL_TOKEN_OPTION,
715
+ PROFILE_OPTION,
716
+ JSON_OPTION,
717
+ ],
718
+ },
719
+ ),
737
720
  command(
738
721
  "send",
739
722
  "Send a first message to a recipient address.",
@@ -754,7 +737,8 @@ const HELP_ROOT = group(
754
737
  JSON_OPTION,
755
738
  ],
756
739
  notes: [
757
- "Recipient addresses support `email:<email>`, `user:<uuid>`, `user:@handle`, `channel:<uuid>`, and `channel:@handle/<channel-name>`.",
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.",
758
742
  ],
759
743
  },
760
744
  ),
@@ -811,6 +795,72 @@ const HELP_ROOT = group(
811
795
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
812
796
  },
813
797
  ),
798
+ group(
799
+ "screening",
800
+ "Inspect screened inbox intakes and apply decisions.",
801
+ [
802
+ command(
803
+ "list",
804
+ "List screened external email waiting for a decision.",
805
+ `${CLI_NAME} inbox screening list [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
806
+ {
807
+ options: [
808
+ LIMIT_OPTION,
809
+ CURSOR_OPTION,
810
+ CHANNEL_TOKEN_OPTION,
811
+ PROFILE_OPTION,
812
+ JSON_OPTION,
813
+ ],
814
+ },
815
+ ),
816
+ command(
817
+ "processing",
818
+ "List released external email in the processing queue.",
819
+ `${CLI_NAME} inbox screening processing [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
820
+ {
821
+ options: [
822
+ LIMIT_OPTION,
823
+ CURSOR_OPTION,
824
+ CHANNEL_TOKEN_OPTION,
825
+ PROFILE_OPTION,
826
+ JSON_OPTION,
827
+ ],
828
+ },
829
+ ),
830
+ command(
831
+ "approve",
832
+ "Approve this sender and release one screened email.",
833
+ `${CLI_NAME} inbox screening approve <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
834
+ {
835
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
836
+ },
837
+ ),
838
+ command(
839
+ "approve-once",
840
+ "Release one screened email without trusting future mail.",
841
+ `${CLI_NAME} inbox screening approve-once <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
842
+ {
843
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
844
+ },
845
+ ),
846
+ command(
847
+ "ignore",
848
+ "Ignore this sender and suppress future mail.",
849
+ `${CLI_NAME} inbox screening ignore <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
850
+ {
851
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
852
+ },
853
+ ),
854
+ ],
855
+ {
856
+ usage: [`${CLI_NAME} inbox screening <subcommand>`],
857
+ notes: [
858
+ "Use `inbox list --status pending` for pending first-contact threads.",
859
+ "Reads allow owner-read tokens or channel tokens.",
860
+ "Decision actions require a master token unless you provide `--channel-token`.",
861
+ ],
862
+ },
863
+ ),
814
864
  ],
815
865
  {
816
866
  usage: [`${CLI_NAME} inbox <subcommand>`],
package/src/types/api.ts CHANGED
@@ -119,11 +119,39 @@ export interface MessageAttributes {
119
119
  body: string;
120
120
  sender_owner_id?: string | null;
121
121
  sender_channel_id?: string | null;
122
+ external_email_sender_id?: string | null;
122
123
  thread_id?: string | null;
123
124
  context_post_id?: string | null;
124
125
  inserted_at?: string;
125
126
  }
126
127
 
128
+ export interface ExternalEmailIntakeAttributes {
129
+ postmark_message_id?: string | null;
130
+ raw_recipient?: string | null;
131
+ subject?: string | null;
132
+ body?: string | null;
133
+ attachment_count?: number | null;
134
+ attachment_metadata?: Array<Record<string, unknown>> | null;
135
+ attachments_withheld?: boolean | null;
136
+ status?: string | null;
137
+ decision?: string | null;
138
+ received_count?: number | null;
139
+ last_received_at?: string | null;
140
+ released_at?: string | null;
141
+ owner_id?: string | null;
142
+ target_channel_id?: string | null;
143
+ external_email_sender_id?: string | null;
144
+ released_thread_id?: string | null;
145
+ }
146
+
147
+ export interface MessageAttachmentAttributes {
148
+ name?: string | null;
149
+ content_type?: string | null;
150
+ content_length?: number | null;
151
+ message_id?: string | null;
152
+ inserted_at?: string;
153
+ }
154
+
127
155
  export interface AccessKeyAttributes {
128
156
  expires_at: string;
129
157
  scope: AccessKeyScope;