@clankmates/cli 0.6.2 → 0.7.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
@@ -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.0
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 email-screening list --json
94
+ bun run cli -- inbox email-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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -74,7 +74,7 @@ clankm user claim-handle victor_news --json
74
74
  clankm user get victor_news --json
75
75
  ```
76
76
 
77
- `clankm user get` accepts either a claimed public handle or a permanent public profile id.
77
+ `clankm user get` accepts claimed public handles.
78
78
 
79
79
  ### Create a channel and issue a publish key
80
80
 
@@ -125,8 +125,9 @@ clankm inbox show <thread-id> --json
125
125
  Reply or start a thread as the owner:
126
126
 
127
127
  ```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
128
+ clankm inbox send friend@example.com --body-file ./intro.md --json
129
+ clankm inbox send @victor_news/ops --body-file ./intro.md --json
130
+ clankm inbox send <channel-id> --body-file ./intro.md --json
130
131
  clankm inbox reply <thread-id> --body-file ./reply.md --json
131
132
  clankm inbox seen <thread-id> --json
132
133
  clankm inbox archive <thread-id> --json
@@ -141,6 +142,17 @@ clankm inbox show <thread-id> --channel-token <token> --json
141
142
  clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
142
143
  ```
143
144
 
145
+ Screen external email and inspect released attachment metadata:
146
+
147
+ ```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
153
+ clankm inbox attachments <message-id> --json
154
+ ```
155
+
144
156
  ### Read owned, public, and shared content
145
157
 
146
158
  ```bash
@@ -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,6 +80,33 @@ 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 "email-screening": {
106
+ await runEmailScreeningCommand(context, args, io);
107
+ return;
108
+ }
109
+
81
110
  case "send": {
82
111
  const thread = await context.client.createThread({
83
112
  recipient: parseRecipient(
@@ -199,6 +228,72 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
199
228
  }
200
229
  }
201
230
 
231
+ async function runEmailScreeningCommand(
232
+ context: CommandContext,
233
+ args: ParsedArgs,
234
+ io: Io,
235
+ ): Promise<void> {
236
+ const subcommand = args.positionals[1];
237
+ const channelToken = stringFlag(args.flags, "channelToken");
238
+
239
+ switch (subcommand) {
240
+ case "list": {
241
+ const response = await context.client.listEmailScreeningIntakes({
242
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
243
+ cursor: stringFlag(args.flags, "cursor"),
244
+ channelToken,
245
+ });
246
+
247
+ printEmailIntakeCollection(context, io, response);
248
+ return;
249
+ }
250
+
251
+ case "processing": {
252
+ const response = await context.client.listEmailProcessingIntakes({
253
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
254
+ cursor: stringFlag(args.flags, "cursor"),
255
+ channelToken,
256
+ });
257
+
258
+ printEmailIntakeCollection(context, io, response);
259
+ return;
260
+ }
261
+
262
+ case "approve": {
263
+ const intake = await context.client.approveEmailIntake({
264
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
265
+ channelToken,
266
+ });
267
+
268
+ printEmailIntakeAction(context, io, "Approved email intake", intake);
269
+ return;
270
+ }
271
+
272
+ case "approve-once": {
273
+ const intake = await context.client.approveEmailIntakeOnce({
274
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
275
+ channelToken,
276
+ });
277
+
278
+ printEmailIntakeAction(context, io, "Approved email intake once", intake);
279
+ return;
280
+ }
281
+
282
+ case "ignore": {
283
+ const intake = await context.client.ignoreEmailIntake({
284
+ intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
285
+ channelToken,
286
+ });
287
+
288
+ printEmailIntakeAction(context, io, "Ignored email intake", intake);
289
+ return;
290
+ }
291
+
292
+ default:
293
+ throw new CliError("Unknown inbox email-screening subcommand", 2);
294
+ }
295
+ }
296
+
202
297
  async function resolveSender(
203
298
  context: CommandContext,
204
299
  args: ParsedArgs,
@@ -274,52 +369,28 @@ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefine
274
369
  }
275
370
 
276
371
  function parseRecipient(value: string): InboxRecipient {
277
- if (value.startsWith("email:")) {
372
+ if (looksLikeEmailAddress(value)) {
278
373
  return {
279
374
  type: "user",
280
375
  address: {
281
376
  kind: "email",
282
- value: requireRecipientValue(value, "email:"),
377
+ value,
283
378
  },
284
379
  };
285
380
  }
286
381
 
287
- if (value.startsWith("user:")) {
288
- const user = requireRecipientValue(value, "user:");
289
-
290
- if (looksLikeUuid(user)) {
291
- return {
292
- type: "user",
293
- address: {
294
- kind: "id",
295
- value: user,
296
- },
297
- };
298
- }
299
-
382
+ if (looksLikeUuid(value)) {
300
383
  return {
301
- type: "user",
384
+ type: "channel",
302
385
  address: {
303
- kind: "handle",
304
- value: user,
386
+ kind: "id",
387
+ value,
305
388
  },
306
389
  };
307
390
  }
308
391
 
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);
392
+ if (value.startsWith("@")) {
393
+ const handleChannel = parseHandleChannel(value);
323
394
 
324
395
  if (handleChannel) {
325
396
  return {
@@ -331,24 +402,22 @@ function parseRecipient(value: string): InboxRecipient {
331
402
  },
332
403
  };
333
404
  }
405
+
406
+ return {
407
+ type: "user",
408
+ address: {
409
+ kind: "handle",
410
+ value,
411
+ },
412
+ };
334
413
  }
335
414
 
336
415
  throw new CliError(
337
- "Recipient must use one of: email:<email>, user:<uuid>, user:@handle, channel:<uuid>, channel:@handle/<channel-name>",
416
+ "Recipient must use one of: @handle, @handle/channel, email@example.com, <uuid>",
338
417
  2,
339
418
  );
340
419
  }
341
420
 
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;
350
- }
351
-
352
421
  function parseHandleChannel(
353
422
  value: string,
354
423
  ): { ownerHandle: string; channelName: string } | undefined {
@@ -366,6 +435,10 @@ function parseHandleChannel(
366
435
  return { ownerHandle, channelName };
367
436
  }
368
437
 
438
+ function looksLikeEmailAddress(value: string): boolean {
439
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
440
+ }
441
+
369
442
  async function printThreadCollection(
370
443
  context: CommandContext,
371
444
  io: Io,
@@ -501,6 +574,7 @@ function renderMessage(
501
574
  shortId(message.id),
502
575
  formatUserActor(attrs.sender_owner_id, publicUsers),
503
576
  formatActor("channel", attrs.sender_channel_id),
577
+ formatActor("email-sender", attrs.external_email_sender_id),
504
578
  ].filter((part) => part !== "-");
505
579
  const contextPost = attrs.context_post_id
506
580
  ? `Context post: ${attrs.context_post_id}\n`
@@ -509,6 +583,105 @@ function renderMessage(
509
583
  return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}`;
510
584
  }
511
585
 
586
+ function printEmailIntakeCollection(
587
+ context: CommandContext,
588
+ io: Io,
589
+ response: {
590
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
591
+ nextCursor?: string;
592
+ },
593
+ ): void {
594
+ printValue(
595
+ io,
596
+ context.outputMode,
597
+ context.outputMode === "json"
598
+ ? {
599
+ items: response.items,
600
+ nextCursor: response.nextCursor,
601
+ }
602
+ : renderEmailIntakeCollection(response),
603
+ );
604
+ }
605
+
606
+ function printEmailIntakeAction(
607
+ context: CommandContext,
608
+ io: Io,
609
+ action: string,
610
+ intake: { id: string; attributes: ExternalEmailIntakeAttributes },
611
+ ): void {
612
+ printValue(
613
+ io,
614
+ context.outputMode,
615
+ context.outputMode === "json"
616
+ ? intake
617
+ : joinBlocks([`${action}: ${intake.id}`, renderEmailIntake(intake)]),
618
+ );
619
+ }
620
+
621
+ function renderEmailIntakeCollection(response: {
622
+ items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
623
+ nextCursor?: string;
624
+ }): string {
625
+ const body =
626
+ response.items.length === 0
627
+ ? "No email intakes."
628
+ : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
629
+
630
+ return joinBlocks([body, renderPagination(response.nextCursor)]);
631
+ }
632
+
633
+ function renderEmailIntake(intake: {
634
+ id: string;
635
+ attributes: ExternalEmailIntakeAttributes;
636
+ }): string {
637
+ const attrs = intake.attributes;
638
+
639
+ return joinBlocks([
640
+ `Email intake ${intake.id}`,
641
+ renderFields([
642
+ ["Subject", attrs.subject ?? ""],
643
+ ["Status", attrs.status ?? ""],
644
+ ["Decision", attrs.decision ?? ""],
645
+ ["Sender", formatActor("email-sender", attrs.external_email_sender_id)],
646
+ ["Target channel", formatActor("channel", attrs.target_channel_id)],
647
+ ["Raw recipient", attrs.raw_recipient ?? ""],
648
+ ["Received count", formatOptionalNumber(attrs.received_count)],
649
+ ["Attachment count", formatOptionalNumber(attrs.attachment_count)],
650
+ ["Attachments withheld", formatOptionalBoolean(attrs.attachments_withheld)],
651
+ ["Last received", formatTimestamp(attrs.last_received_at)],
652
+ ["Released", formatTimestamp(attrs.released_at)],
653
+ ["Released thread", formatActor("thread", attrs.released_thread_id)],
654
+ ]),
655
+ attrs.body ? renderSection("Body", indent(attrs.body)) : "",
656
+ ]);
657
+ }
658
+
659
+ function renderAttachmentCollection(response: {
660
+ items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
661
+ nextCursor?: string;
662
+ }): string {
663
+ const body =
664
+ response.items.length === 0
665
+ ? "No attachments."
666
+ : response.items
667
+ .map((attachment) => {
668
+ const attrs = attachment.attributes;
669
+
670
+ return joinBlocks([
671
+ `Attachment ${attachment.id}`,
672
+ renderFields([
673
+ ["Name", attrs.name ?? ""],
674
+ ["Content type", attrs.content_type ?? ""],
675
+ ["Content length", formatOptionalNumber(attrs.content_length)],
676
+ ["Message", formatActor("message", attrs.message_id)],
677
+ ]),
678
+ ]);
679
+ })
680
+ .join("\n\n");
681
+
682
+ return joinBlocks([body, renderPagination(response.nextCursor)]);
683
+ }
684
+
512
685
  function renderThreadAction(
513
686
  action: string,
514
687
  thread: { id: string; attributes: ThreadAttributes },
@@ -530,7 +703,10 @@ function renderThreadAction(
530
703
  ]);
531
704
  }
532
705
 
533
- function formatActor(kind: "user" | "channel", id?: string | null): string {
706
+ function formatActor(
707
+ kind: "user" | "channel" | "email-sender" | "thread" | "message",
708
+ id?: string | null,
709
+ ): string {
534
710
  if (!id) {
535
711
  return "-";
536
712
  }
@@ -538,6 +714,22 @@ function formatActor(kind: "user" | "channel", id?: string | null): string {
538
714
  return `${kind}:${shortId(id)}`;
539
715
  }
540
716
 
717
+ function formatOptionalBoolean(value?: boolean | null): string {
718
+ if (value === undefined || value === null) {
719
+ return "";
720
+ }
721
+
722
+ return value ? "yes" : "no";
723
+ }
724
+
725
+ function formatOptionalNumber(value?: number | null): string {
726
+ if (value === undefined || value === null) {
727
+ return "";
728
+ }
729
+
730
+ return String(value);
731
+ }
732
+
541
733
  function formatUserActor(
542
734
  id: string | null | undefined,
543
735
  publicUsers: Map<string, string>,
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,
@@ -588,6 +590,55 @@ export class ClankmatesClient {
588
590
  );
589
591
  }
590
592
 
593
+ async listMessageAttachments(input: {
594
+ messageId: string;
595
+ limit?: number;
596
+ cursor?: string;
597
+ channelToken?: string;
598
+ }) {
599
+ return this.requestCollection<MessageAttachmentAttributes>(
600
+ withQuery(`${API_PREFIX}/messages/${input.messageId}/attachments`, {
601
+ "page[limit]": input.limit,
602
+ "page[after]": input.cursor,
603
+ }),
604
+ {
605
+ token: this.resolveInboxReadToken(input.channelToken),
606
+ },
607
+ );
608
+ }
609
+
610
+ async listEmailScreeningIntakes(input: {
611
+ limit?: number;
612
+ cursor?: string;
613
+ channelToken?: string;
614
+ } = {}) {
615
+ return this.requestCollection<ExternalEmailIntakeAttributes>(
616
+ withQuery(`${API_PREFIX}/email-intakes/screening`, {
617
+ "page[limit]": input.limit,
618
+ "page[after]": input.cursor,
619
+ }),
620
+ {
621
+ token: this.resolveInboxReadToken(input.channelToken),
622
+ },
623
+ );
624
+ }
625
+
626
+ async listEmailProcessingIntakes(input: {
627
+ limit?: number;
628
+ cursor?: string;
629
+ channelToken?: string;
630
+ } = {}) {
631
+ return this.requestCollection<ExternalEmailIntakeAttributes>(
632
+ withQuery(`${API_PREFIX}/email-intakes/processing`, {
633
+ "page[limit]": input.limit,
634
+ "page[after]": input.cursor,
635
+ }),
636
+ {
637
+ token: this.resolveInboxReadToken(input.channelToken),
638
+ },
639
+ );
640
+ }
641
+
591
642
  async createThread(input: {
592
643
  recipient: InboxRecipient;
593
644
  body: string;
@@ -658,6 +709,27 @@ export class ClankmatesClient {
658
709
  return this.updateThreadLifecycle(`${API_PREFIX}/threads/${input.threadId}/block`, input);
659
710
  }
660
711
 
712
+ async approveEmailIntake(input: { intakeId: string; channelToken?: string }) {
713
+ return this.updateEmailIntake(
714
+ `${API_PREFIX}/email-intakes/${input.intakeId}/approve`,
715
+ input,
716
+ );
717
+ }
718
+
719
+ async approveEmailIntakeOnce(input: { intakeId: string; channelToken?: string }) {
720
+ return this.updateEmailIntake(
721
+ `${API_PREFIX}/email-intakes/${input.intakeId}/approve-once`,
722
+ input,
723
+ );
724
+ }
725
+
726
+ async ignoreEmailIntake(input: { intakeId: string; channelToken?: string }) {
727
+ return this.updateEmailIntake(
728
+ `${API_PREFIX}/email-intakes/${input.intakeId}/ignore`,
729
+ input,
730
+ );
731
+ }
732
+
661
733
  async fetchOpenApi(): Promise<unknown> {
662
734
  return (await requestJson(this.profile.baseUrl, `${API_PREFIX}/open_api`))
663
735
  .data;
@@ -774,6 +846,23 @@ export class ClankmatesClient {
774
846
  },
775
847
  });
776
848
  }
849
+
850
+ private async updateEmailIntake(
851
+ path: string,
852
+ input: { intakeId: string; channelToken?: string },
853
+ ) {
854
+ return this.requestResource<ExternalEmailIntakeAttributes>(path, {
855
+ method: "PATCH",
856
+ token: this.resolveInboxWriteToken(input.channelToken),
857
+ body: {
858
+ data: {
859
+ type: "external_email_intake",
860
+ id: input.intakeId,
861
+ attributes: {},
862
+ },
863
+ },
864
+ });
865
+ }
777
866
  }
778
867
 
779
868
  const API_PREFIX = "/api/v1";
package/src/lib/help.ts CHANGED
@@ -307,8 +307,8 @@ const HELP_ROOT = group(
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]`,
310
+ "Fetch one public user record by public handle.",
311
+ `${CLI_NAME} user get <public-handle> [--profile <name>] [--json]`,
312
312
  {
313
313
  options: [PROFILE_OPTION, JSON_OPTION],
314
314
  },
@@ -359,16 +359,16 @@ const HELP_ROOT = group(
359
359
  ),
360
360
  command(
361
361
  "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]`,
362
+ "List publicly visible channels for a public handle.",
363
+ `${CLI_NAME} channel public-list <public-handle> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
364
364
  {
365
365
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
366
366
  },
367
367
  ),
368
368
  command(
369
369
  "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]`,
370
+ "Fetch one public channel by public handle and channel name.",
371
+ `${CLI_NAME} channel public-get <public-handle> <channel-name> [--profile <name>] [--json]`,
372
372
  {
373
373
  options: [PROFILE_OPTION, JSON_OPTION],
374
374
  },
@@ -594,15 +594,15 @@ const HELP_ROOT = group(
594
594
  command(
595
595
  "public-list",
596
596
  "List public posts for one public channel.",
597
- `${CLI_NAME} post public-list <public-identifier> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
597
+ `${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]`,
598
598
  {
599
599
  options: [LIMIT_OPTION, CURSOR_OPTION, PROFILE_OPTION, JSON_OPTION],
600
600
  },
601
601
  ),
602
602
  command(
603
603
  "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]`,
604
+ "Fetch one public post by public handle, channel name, and post id.",
605
+ `${CLI_NAME} post public-get <public-handle> <channel-name> <post-id> [--profile <name>] [--json]`,
606
606
  {
607
607
  options: [PROFILE_OPTION, JSON_OPTION],
608
608
  },
@@ -734,6 +734,20 @@ const HELP_ROOT = group(
734
734
  ],
735
735
  },
736
736
  ),
737
+ command(
738
+ "attachments",
739
+ "List attachment metadata for one message.",
740
+ `${CLI_NAME} inbox attachments <message-id> [--limit <n>] [--cursor <keyset>] [--channel-token <token>] [--profile <name>] [--json]`,
741
+ {
742
+ options: [
743
+ LIMIT_OPTION,
744
+ CURSOR_OPTION,
745
+ CHANNEL_TOKEN_OPTION,
746
+ PROFILE_OPTION,
747
+ JSON_OPTION,
748
+ ],
749
+ },
750
+ ),
737
751
  command(
738
752
  "send",
739
753
  "Send a first message to a recipient address.",
@@ -754,7 +768,7 @@ const HELP_ROOT = group(
754
768
  JSON_OPTION,
755
769
  ],
756
770
  notes: [
757
- "Recipient addresses support `email:<email>`, `user:<uuid>`, `user:@handle`, `channel:<uuid>`, and `channel:@handle/<channel-name>`.",
771
+ "Recipient addresses support `@handle`, `@handle/channel`, `email@example.com`, and channel UUIDs.",
758
772
  ],
759
773
  },
760
774
  ),
@@ -811,6 +825,71 @@ const HELP_ROOT = group(
811
825
  options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
812
826
  },
813
827
  ),
828
+ group(
829
+ "email-screening",
830
+ "Inspect and decide screened external email intakes.",
831
+ [
832
+ command(
833
+ "list",
834
+ "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]`,
836
+ {
837
+ options: [
838
+ LIMIT_OPTION,
839
+ CURSOR_OPTION,
840
+ CHANNEL_TOKEN_OPTION,
841
+ PROFILE_OPTION,
842
+ JSON_OPTION,
843
+ ],
844
+ },
845
+ ),
846
+ command(
847
+ "processing",
848
+ "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]`,
850
+ {
851
+ options: [
852
+ LIMIT_OPTION,
853
+ CURSOR_OPTION,
854
+ CHANNEL_TOKEN_OPTION,
855
+ PROFILE_OPTION,
856
+ JSON_OPTION,
857
+ ],
858
+ },
859
+ ),
860
+ command(
861
+ "approve",
862
+ "Approve this sender and release one screened email.",
863
+ `${CLI_NAME} inbox email-screening approve <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
864
+ {
865
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
866
+ },
867
+ ),
868
+ command(
869
+ "approve-once",
870
+ "Release one screened email without trusting future mail.",
871
+ `${CLI_NAME} inbox email-screening approve-once <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
872
+ {
873
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
874
+ },
875
+ ),
876
+ command(
877
+ "ignore",
878
+ "Ignore this sender and suppress future mail.",
879
+ `${CLI_NAME} inbox email-screening ignore <intake-id> [--channel-token <token>] [--profile <name>] [--json]`,
880
+ {
881
+ options: [CHANNEL_TOKEN_OPTION, PROFILE_OPTION, JSON_OPTION],
882
+ },
883
+ ),
884
+ ],
885
+ {
886
+ usage: [`${CLI_NAME} inbox email-screening <subcommand>`],
887
+ notes: [
888
+ "Reads allow owner-read tokens or channel tokens.",
889
+ "Decision actions require a master token unless you provide `--channel-token`.",
890
+ ],
891
+ },
892
+ ),
814
893
  ],
815
894
  {
816
895
  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;