@clankmates/cli 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/src/commands/auth/access-keys.ts +206 -0
  4. package/src/commands/auth.ts +3 -196
  5. package/src/commands/channel/render.ts +224 -0
  6. package/src/commands/channel/tokens.ts +145 -0
  7. package/src/commands/channel/validation.ts +11 -0
  8. package/src/commands/channel.ts +11 -340
  9. package/src/commands/doctor/checks.ts +123 -0
  10. package/src/commands/doctor/render.ts +140 -0
  11. package/src/commands/doctor/suggestions.ts +42 -0
  12. package/src/commands/doctor/types.ts +75 -0
  13. package/src/commands/doctor.ts +12 -371
  14. package/src/commands/feed.ts +15 -178
  15. package/src/commands/inbox/content.ts +31 -0
  16. package/src/commands/inbox/filters.ts +70 -0
  17. package/src/commands/inbox/messages.ts +69 -0
  18. package/src/commands/inbox/participants.ts +152 -0
  19. package/src/commands/inbox/render.ts +13 -0
  20. package/src/commands/inbox/resource-output.ts +217 -0
  21. package/src/commands/inbox/schema.ts +185 -0
  22. package/src/commands/inbox/screening.ts +76 -0
  23. package/src/commands/inbox/sync-scopes.ts +59 -0
  24. package/src/commands/inbox/thread-output.ts +344 -0
  25. package/src/commands/inbox/watch.ts +203 -0
  26. package/src/commands/inbox.ts +37 -1243
  27. package/src/commands/post.ts +9 -114
  28. package/src/lib/args.ts +1 -0
  29. package/src/lib/cache/scopes.ts +216 -0
  30. package/src/lib/cache/store.ts +195 -0
  31. package/src/lib/cache/types.ts +31 -0
  32. package/src/lib/cache.ts +18 -436
  33. package/src/lib/client/auth.ts +122 -0
  34. package/src/lib/client/channel-keys.ts +57 -0
  35. package/src/lib/client/channels.ts +364 -0
  36. package/src/lib/client/core.ts +133 -0
  37. package/src/lib/client/feed.ts +76 -0
  38. package/src/lib/client/inbox.ts +361 -0
  39. package/src/lib/client/posts.ts +213 -0
  40. package/src/lib/client/raw-api.ts +33 -0
  41. package/src/lib/client/users.ts +88 -0
  42. package/src/lib/client.ts +177 -913
  43. package/src/lib/help.ts +26 -0
  44. package/src/lib/json_api.ts +74 -9
  45. package/src/lib/polling.ts +146 -0
  46. package/src/lib/post-output.ts +55 -0
@@ -1,16 +1,7 @@
1
1
  import {
2
2
  assertSinceFlags,
3
- authenticatedActorKey,
4
- cacheFlags,
5
3
  cacheResult,
6
4
  changeResponseMeta,
7
- inboxMessagesScope,
8
- inboxThreadsScope,
9
- prepareCachePlan,
10
- saveCacheTimestamp,
11
- type CachePlan,
12
- type CacheResult,
13
- type CacheScope,
14
5
  } from "../lib/cache";
15
6
  import {
16
7
  booleanFlag,
@@ -19,35 +10,45 @@ import {
19
10
  stringFlag,
20
11
  type ParsedArgs,
21
12
  } from "../lib/args";
22
- import { resolveBodyInput } from "../lib/body-input";
23
13
  import { createCommandContext, type CommandContext } from "../lib/context";
24
14
  import { CliError } from "../lib/errors";
15
+ import { printValue, type Io } from "../lib/output";
16
+ import { paginatedJson } from "../lib/pagination";
25
17
  import {
26
- formatTimestamp,
27
- indent,
28
- joinBlocks,
29
- renderFields,
30
- renderPagination,
31
- renderSection,
32
- shortId,
33
- } from "../lib/human";
34
- import { resolveJsonInput } from "../lib/json-input";
35
- import { printJson, printValue, type Io } from "../lib/output";
36
- import { paginatedJson, paginationInfo } from "../lib/pagination";
18
+ maybePrepareCachePlan,
19
+ maybeSaveCacheTimestamp,
20
+ parseLatestFirstOrder,
21
+ printChangeCheckResponse,
22
+ requiredSince,
23
+ resolvedSince,
24
+ } from "../lib/polling";
25
+ import { resolveMessageContent } from "./inbox/content";
26
+ import {
27
+ parseMailboxFilter,
28
+ parseParticipantScope,
29
+ parseStatusFilter,
30
+ } from "./inbox/filters";
31
+ import {
32
+ parseRecipient,
33
+ resolveSender,
34
+ } from "./inbox/participants";
35
+ import {
36
+ ownerIdsForThreadDisplay,
37
+ publicUserHandlesById,
38
+ printThreadCollection,
39
+ renderAttachmentCollection,
40
+ renderThreadAction,
41
+ renderThreadWithMessages,
42
+ } from "./inbox/render";
43
+ import { runInboxMessagesCommand } from "./inbox/messages";
44
+ import { runSchemaCommand } from "./inbox/schema";
45
+ import { runScreeningCommand } from "./inbox/screening";
46
+ import { maybeInboxMessagesScope, maybeInboxThreadsScope } from "./inbox/sync-scopes";
47
+ import { runInboxWatchCommand } from "./inbox/watch";
37
48
  import type {
38
- ChangeCheckResponse,
39
- ExternalEmailAcceptance,
40
- ExternalEmailIntakeAttributes,
41
- InboxRecipient,
42
- InboxSender,
43
- LatestFirstOrder,
44
49
  MailboxFilter,
45
- MessageAttachmentAttributes,
46
- MessageAttributes,
47
50
  ParticipantScope,
48
- ThreadAttributes,
49
51
  ThreadStatusFilter,
50
- WhoamiActor,
51
52
  } from "../types/api";
52
53
 
53
54
  export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
@@ -204,6 +205,11 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
204
205
  return;
205
206
  }
206
207
 
208
+ case "watch": {
209
+ await runInboxWatchCommand(context, args, io);
210
+ return;
211
+ }
212
+
207
213
  case "messages": {
208
214
  await runInboxMessagesCommand(context, args, io);
209
215
  return;
@@ -360,1215 +366,3 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
360
366
  throw new CliError("Unknown inbox subcommand", 2);
361
367
  }
362
368
  }
363
-
364
- async function resolveMessageContent(args: ParsedArgs): Promise<{
365
- body?: string;
366
- payload?: Record<string, unknown>;
367
- }> {
368
- if (booleanFlag(args.flags, "stdin") && booleanFlag(args.flags, "payloadStdin")) {
369
- throw new CliError("Use only one of `--stdin` or `--payload-stdin`", 2);
370
- }
371
-
372
- const body = await resolveBodyInput({ flags: args.flags });
373
- const payload = await resolveJsonInput({
374
- flags: args.flags,
375
- inlineKey: "payload",
376
- fileKey: "payloadFile",
377
- stdinKey: "payloadStdin",
378
- label: "Payload",
379
- });
380
-
381
- if (body === undefined && payload === undefined) {
382
- throw new CliError(
383
- "Provide at least one of `--body`, `--body-file`, `--stdin`, `--payload`, `--payload-file`, or `--payload-stdin`",
384
- 2,
385
- );
386
- }
387
-
388
- return { body, payload };
389
- }
390
-
391
- async function runSchemaCommand(
392
- context: CommandContext,
393
- args: ParsedArgs,
394
- io: Io,
395
- ): Promise<void> {
396
- const subcommand = args.positionals[1];
397
-
398
- switch (subcommand) {
399
- case "show": {
400
- const target = requiredPositional(
401
- args.positionals,
402
- 2,
403
- "Missing schema target",
404
- );
405
- const schema = await getPublicInboxSchema(context, target);
406
- printSchemaResource(context, io, schema);
407
- return;
408
- }
409
-
410
- case "set": {
411
- const scope = requiredPositional(
412
- args.positionals,
413
- 2,
414
- "Missing schema scope",
415
- );
416
- const inboxSchema = await requiredInboxSchema(args);
417
-
418
- if (scope === "account") {
419
- printSchemaResource(
420
- context,
421
- io,
422
- await context.client.setAccountInboxSchema(inboxSchema),
423
- "Updated account inbox schema",
424
- );
425
- return;
426
- }
427
-
428
- if (scope === "channel") {
429
- const channelRef = requiredPositional(
430
- args.positionals,
431
- 3,
432
- "Missing channel name or id",
433
- );
434
- const channelId = await context.client.resolveChannelId(channelRef);
435
- printSchemaResource(
436
- context,
437
- io,
438
- await context.client.setChannelInboxSchema({
439
- channelId,
440
- inboxSchema,
441
- }),
442
- "Updated channel inbox schema",
443
- );
444
- return;
445
- }
446
-
447
- throw new CliError("Schema scope must be `account` or `channel`", 2);
448
- }
449
-
450
- case "remove": {
451
- const scope = requiredPositional(
452
- args.positionals,
453
- 2,
454
- "Missing schema scope",
455
- );
456
-
457
- if (scope === "account") {
458
- printSchemaResource(
459
- context,
460
- io,
461
- await context.client.removeAccountInboxSchema(),
462
- "Removed account inbox schema",
463
- );
464
- return;
465
- }
466
-
467
- if (scope === "channel") {
468
- const channelRef = requiredPositional(
469
- args.positionals,
470
- 3,
471
- "Missing channel name or id",
472
- );
473
- const channelId = await context.client.resolveChannelId(channelRef);
474
- printSchemaResource(
475
- context,
476
- io,
477
- await context.client.removeChannelInboxSchema(channelId),
478
- "Removed channel inbox schema",
479
- );
480
- return;
481
- }
482
-
483
- throw new CliError("Schema scope must be `account` or `channel`", 2);
484
- }
485
-
486
- case "acceptance": {
487
- const scope = requiredPositional(
488
- args.positionals,
489
- 2,
490
- "Missing schema acceptance scope",
491
- );
492
-
493
- if (scope === "account") {
494
- const externalEmailAcceptance = parseExternalEmailAcceptance(
495
- requiredPositional(
496
- args.positionals,
497
- 3,
498
- "Missing external email acceptance policy",
499
- ),
500
- );
501
-
502
- printSchemaResource(
503
- context,
504
- io,
505
- await context.client.setAccountExternalEmailAcceptance(
506
- externalEmailAcceptance,
507
- ),
508
- "Updated account inbox acceptance",
509
- );
510
- return;
511
- }
512
-
513
- if (scope === "channel") {
514
- const channelRef = requiredPositional(
515
- args.positionals,
516
- 3,
517
- "Missing channel name or id",
518
- );
519
- const externalEmailAcceptance = parseExternalEmailAcceptance(
520
- requiredPositional(
521
- args.positionals,
522
- 4,
523
- "Missing external email acceptance policy",
524
- ),
525
- );
526
- const channelId = await context.client.resolveChannelId(channelRef);
527
- printSchemaResource(
528
- context,
529
- io,
530
- await context.client.setChannelExternalEmailAcceptance({
531
- channelId,
532
- externalEmailAcceptance,
533
- }),
534
- "Updated channel inbox acceptance",
535
- );
536
- return;
537
- }
538
-
539
- throw new CliError("Schema acceptance scope must be `account` or `channel`", 2);
540
- }
541
-
542
- default:
543
- throw new CliError("Unknown inbox schema subcommand", 2);
544
- }
545
- }
546
-
547
- async function requiredInboxSchema(
548
- args: ParsedArgs,
549
- ): Promise<Record<string, unknown>> {
550
- const schema = await resolveJsonInput({
551
- flags: args.flags,
552
- inlineKey: "schema",
553
- fileKey: "schemaFile",
554
- stdinKey: "schemaStdin",
555
- label: "Inbox schema",
556
- });
557
-
558
- if (!schema) {
559
- throw new CliError(
560
- "Provide exactly one of `--schema`, `--schema-file`, or `--schema-stdin`",
561
- 2,
562
- );
563
- }
564
-
565
- return schema;
566
- }
567
-
568
- function parseExternalEmailAcceptance(value: string): ExternalEmailAcceptance {
569
- if (
570
- value === "screen_unknown_senders" ||
571
- value === "screen-unknown-senders"
572
- ) {
573
- return "screen_unknown_senders";
574
- }
575
-
576
- if (
577
- value === "accept_valid_typed_email" ||
578
- value === "accept-valid-typed-email"
579
- ) {
580
- return "accept_valid_typed_email";
581
- }
582
-
583
- throw new CliError(
584
- "External email acceptance policy must be one of: screen-unknown-senders, accept-valid-typed-email",
585
- 2,
586
- );
587
- }
588
-
589
- async function runScreeningCommand(
590
- context: CommandContext,
591
- args: ParsedArgs,
592
- io: Io,
593
- ): Promise<void> {
594
- const subcommand = args.positionals[1];
595
- const channelToken = stringFlag(args.flags, "channelToken");
596
-
597
- switch (subcommand) {
598
- case "list": {
599
- const response = await context.client.listEmailScreeningIntakes({
600
- limit: integerFlag(args.flags, "limit", { label: "--limit" }),
601
- cursor: stringFlag(args.flags, "cursor"),
602
- channelToken,
603
- });
604
-
605
- printEmailIntakeCollection(args, context, io, response);
606
- return;
607
- }
608
-
609
- case "processing": {
610
- const response = await context.client.listEmailProcessingIntakes({
611
- limit: integerFlag(args.flags, "limit", { label: "--limit" }),
612
- cursor: stringFlag(args.flags, "cursor"),
613
- channelToken,
614
- });
615
-
616
- printEmailIntakeCollection(args, context, io, response);
617
- return;
618
- }
619
-
620
- case "approve": {
621
- const intake = await context.client.approveEmailIntake({
622
- intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
623
- channelToken,
624
- });
625
-
626
- printEmailIntakeAction(context, io, "Approved email intake", intake);
627
- return;
628
- }
629
-
630
- case "approve-once": {
631
- const intake = await context.client.approveEmailIntakeOnce({
632
- intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
633
- channelToken,
634
- });
635
-
636
- printEmailIntakeAction(context, io, "Approved email intake once", intake);
637
- return;
638
- }
639
-
640
- case "ignore": {
641
- const intake = await context.client.ignoreEmailIntake({
642
- intakeId: requiredPositional(args.positionals, 2, "Missing intake id"),
643
- channelToken,
644
- });
645
-
646
- printEmailIntakeAction(context, io, "Ignored email intake", intake);
647
- return;
648
- }
649
-
650
- default:
651
- throw new CliError("Unknown inbox screening subcommand", 2);
652
- }
653
- }
654
-
655
- async function resolveSender(
656
- context: CommandContext,
657
- args: ParsedArgs,
658
- ): Promise<InboxSender | undefined> {
659
- const from = stringFlag(args.flags, "from");
660
- const channelToken = stringFlag(args.flags, "channelToken");
661
-
662
- if (!from) {
663
- return undefined;
664
- }
665
-
666
- if (looksLikeUuid(from)) {
667
- return channelSender(from);
668
- }
669
-
670
- if (channelToken) {
671
- const whoami = await context.client.whoami(channelToken);
672
-
673
- if (whoami.actor.type === "channel") {
674
- if (whoami.actor.name === from) {
675
- return channelSender(whoami.actor.id);
676
- }
677
-
678
- throw new CliError(
679
- "With `--channel-token`, `--from` must match the authenticated channel name or UUID.",
680
- 2,
681
- );
682
- }
683
- }
684
-
685
- return channelSender(await context.client.resolveChannelId(from));
686
- }
687
-
688
- const UUID_PATTERN =
689
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
690
-
691
- function looksLikeUuid(value: string): boolean {
692
- return UUID_PATTERN.test(value);
693
- }
694
-
695
- function channelSender(channelId: string): InboxSender {
696
- return {
697
- type: "channel",
698
- address: {
699
- kind: "id",
700
- value: channelId,
701
- },
702
- };
703
- }
704
-
705
- function parseStatusFilter(value: string | undefined): ThreadStatusFilter | undefined {
706
- if (!value) {
707
- return undefined;
708
- }
709
-
710
- if (value === "pending" || value === "open" || value === "blocked" || value === "all") {
711
- return value;
712
- }
713
-
714
- throw new CliError("--status must be one of: pending, open, blocked, all", 2);
715
- }
716
-
717
- function parseMailboxFilter(value: string | undefined): MailboxFilter | undefined {
718
- if (!value) {
719
- return undefined;
720
- }
721
-
722
- if (value === "account" || value === "channel" || value === "all") {
723
- return value;
724
- }
725
-
726
- throw new CliError("--mailbox must be one of: account, channel, all", 2);
727
- }
728
-
729
- function parseParticipantScope(value: string | undefined): ParticipantScope | undefined {
730
- if (!value) {
731
- return undefined;
732
- }
733
-
734
- if (value === "account" || value === "owner") {
735
- return value;
736
- }
737
-
738
- throw new CliError("--participant-scope must be one of: account, owner", 2);
739
- }
740
-
741
- function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
742
- if (!value) {
743
- return undefined;
744
- }
745
-
746
- if (value === "latest" || value === "oldest") {
747
- return value;
748
- }
749
-
750
- throw new CliError("--order must be one of: latest, oldest", 2);
751
- }
752
-
753
- function requiredSince(
754
- args: ParsedArgs,
755
- cachePlan?: CachePlan,
756
- label = "resource",
757
- ): string {
758
- const since = stringFlag(args.flags, "since");
759
-
760
- if (since) {
761
- return since;
762
- }
763
-
764
- if (cachePlan?.previousServerTimestamp) {
765
- return cachePlan.previousServerTimestamp;
766
- }
767
-
768
- if (cacheFlags(args).sinceCache) {
769
- throw new CliError(
770
- `No cached server timestamp for this ${label} scope. Run a read command with \`--save-cache\` first.`,
771
- 2,
772
- );
773
- }
774
-
775
- if (!since) {
776
- throw new CliError("Missing `--since`", 2);
777
- }
778
-
779
- return since;
780
- }
781
-
782
- async function runInboxMessagesCommand(
783
- context: CommandContext,
784
- args: ParsedArgs,
785
- io: Io,
786
- ): Promise<void> {
787
- const subcommand = args.positionals[1];
788
-
789
- switch (subcommand) {
790
- case "changes": {
791
- assertSinceFlags(args);
792
- const threadId = requiredPositional(args.positionals, 2, "Missing thread id");
793
- const channelToken = stringFlag(args.flags, "channelToken");
794
- const cacheScope = await maybeInboxMessagesScope(
795
- args,
796
- context,
797
- channelToken,
798
- threadId,
799
- );
800
- const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
801
- const response = await context.client.checkThreadMessageChanges({
802
- threadId,
803
- since: requiredSince(args, cachePlan, "inbox message"),
804
- query: stringFlag(args.flags, "query"),
805
- hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
806
- channelToken,
807
- });
808
- const savedServerTimestamp = await maybeSaveCacheTimestamp(
809
- args,
810
- context,
811
- cacheScope,
812
- changeResponseMeta(response),
813
- response.has_updates === false,
814
- );
815
-
816
- printChangeCheckResponse(
817
- context,
818
- io,
819
- response,
820
- cacheResult(cachePlan, savedServerTimestamp),
821
- );
822
- return;
823
- }
824
-
825
- default:
826
- throw new CliError("Unknown inbox messages subcommand", 2);
827
- }
828
- }
829
-
830
- function printChangeCheckResponse(
831
- context: CommandContext,
832
- io: Io,
833
- response: ChangeCheckResponse,
834
- cache?: CacheResult,
835
- ): void {
836
- printValue(
837
- io,
838
- context.outputMode,
839
- context.outputMode === "json"
840
- ? { ...response, ...(cache ? { cache } : {}) }
841
- : renderFields([
842
- ["Has updates", response.has_updates ? "yes" : "no"],
843
- ["Server time", formatTimestamp(response.server_time)],
844
- [
845
- "Recommended poll",
846
- response.recommended_poll_after_ms === undefined
847
- ? undefined
848
- : `${response.recommended_poll_after_ms}ms`,
849
- ],
850
- ...cacheFields(cache),
851
- ]),
852
- );
853
- }
854
-
855
- async function parseRecipient(
856
- context: CommandContext,
857
- value: string,
858
- ): Promise<InboxRecipient> {
859
- if (looksLikeUuid(value)) {
860
- if (await publicUserExists(context, value)) {
861
- return {
862
- type: "user",
863
- address: {
864
- kind: "id",
865
- value,
866
- },
867
- };
868
- }
869
-
870
- return {
871
- type: "channel",
872
- address: {
873
- kind: "id",
874
- value,
875
- },
876
- };
877
- }
878
-
879
- if (value.startsWith("@")) {
880
- const handleChannel = parseHandleChannel(value);
881
-
882
- if (handleChannel) {
883
- return {
884
- type: "channel",
885
- address: {
886
- kind: "owner_handle_and_channel_name",
887
- owner_handle: handleChannel.ownerHandle,
888
- channel_name: handleChannel.channelName,
889
- },
890
- };
891
- }
892
-
893
- return {
894
- type: "user",
895
- address: {
896
- kind: "handle",
897
- value,
898
- },
899
- };
900
- }
901
-
902
- throw new CliError(
903
- "Recipient must use one of: @handle, @handle/channel, user UUID, or channel UUID",
904
- 2,
905
- );
906
- }
907
-
908
- async function publicUserExists(
909
- context: CommandContext,
910
- id: string,
911
- ): Promise<boolean> {
912
- const response = await context.client.listPublicUsersById([id]);
913
- return response.items.some((user) => user.id === id);
914
- }
915
-
916
- function parseHandleChannel(
917
- value: string,
918
- ): { ownerHandle: string; channelName: string } | undefined {
919
- const [ownerHandle, channelName, ...extra] = value.split("/");
920
-
921
- if (
922
- extra.length > 0 ||
923
- !ownerHandle ||
924
- !channelName ||
925
- !ownerHandle.startsWith("@")
926
- ) {
927
- return undefined;
928
- }
929
-
930
- return { ownerHandle, channelName };
931
- }
932
-
933
- async function getPublicInboxSchema(
934
- context: CommandContext,
935
- target: string,
936
- ) {
937
- const handleChannel = parseHandleChannel(target);
938
-
939
- if (handleChannel) {
940
- return context.client.getPublicChannelInboxSchema(
941
- handleChannel.ownerHandle,
942
- handleChannel.channelName,
943
- );
944
- }
945
-
946
- if (target.startsWith("@")) {
947
- return context.client.getPublicAccountInboxSchema(target);
948
- }
949
-
950
- throw new CliError("Schema target must be `@handle` or `@handle/channel`", 2);
951
- }
952
-
953
- async function printThreadCollection(
954
- args: ParsedArgs,
955
- context: CommandContext,
956
- io: Io,
957
- response: {
958
- items: Array<{ id: string; attributes: ThreadAttributes }>;
959
- nextCursor?: string;
960
- meta?: Record<string, unknown>;
961
- },
962
- channelToken?: string,
963
- cache?: CacheResult,
964
- ): Promise<void> {
965
- if (context.outputMode === "json") {
966
- printJson(
967
- io,
968
- paginatedJson(args, {
969
- items: response.items,
970
- nextCursor: response.nextCursor,
971
- meta: response.meta,
972
- ...(cache ? { cache } : {}),
973
- }),
974
- );
975
- return Promise.resolve();
976
- }
977
-
978
- const actor = (await context.client.whoami(channelToken)).actor;
979
- const peers = response.items.map((item) => threadPeer(item.attributes, actor));
980
- const ownerIds = ownerIdsForThreadList(peers);
981
- const publicUsers =
982
- ownerIds.length === 0
983
- ? new Map<string, string>()
984
- : publicUserHandlesById(
985
- (await context.client.listPublicUsersById(ownerIds)).items,
986
- );
987
-
988
- printValue(
989
- io,
990
- context.outputMode,
991
- response.items.map((item, index) => ({
992
- id: item.id,
993
- with: formatThreadPeer(peers[index], publicUsers),
994
- mailboxType: item.attributes.mailbox_type,
995
- status: item.attributes.status,
996
- lastMessageAt: item.attributes.last_message_at ?? "",
997
- openedAt: item.attributes.opened_at ?? "",
998
- expiresAt: item.attributes.expires_at ?? "",
999
- })),
1000
- );
1001
- const pagination = paginationInfo(args, response.nextCursor);
1002
- const message = renderPagination(
1003
- pagination?.nextCursor,
1004
- pagination?.nextCommand,
1005
- );
1006
-
1007
- if (message) {
1008
- io.stdout(message);
1009
- }
1010
-
1011
- printCacheNote(io, cache);
1012
- }
1013
-
1014
- function renderThreadWithMessages(
1015
- args: ParsedArgs,
1016
- thread: { id: string; attributes: ThreadAttributes },
1017
- messages: {
1018
- items: Array<{ id: string; attributes: MessageAttributes }>;
1019
- nextCursor?: string;
1020
- },
1021
- publicUsers: Map<string, string>,
1022
- cache?: CacheResult,
1023
- ): string {
1024
- const attrs = thread.attributes;
1025
- const messageBlocks =
1026
- messages.items.length === 0
1027
- ? "No messages."
1028
- : messages.items
1029
- .map((message) => renderMessage(message, publicUsers))
1030
- .join("\n\n");
1031
- const pagination = paginationInfo(args, messages.nextCursor);
1032
-
1033
- return joinBlocks([
1034
- `Thread ${thread.id}`,
1035
- renderFields([
1036
- ["Mailbox", attrs.mailbox_type],
1037
- ["Status", attrs.status],
1038
- ["Last message", formatTimestamp(attrs.last_message_at)],
1039
- ["Opened", formatTimestamp(attrs.opened_at)],
1040
- [
1041
- "Expires",
1042
- attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
1043
- ],
1044
- ]),
1045
- renderSection(
1046
- "Participants",
1047
- renderFields([
1048
- ["A owner", formatUserActor(attrs.participant_a_owner_id, publicUsers)],
1049
- ["A channel", formatActor("channel", attrs.participant_a_channel_id)],
1050
- ["A seen", formatTimestamp(attrs.participant_a_seen_at)],
1051
- [
1052
- "A archived",
1053
- attrs.participant_a_archived_at
1054
- ? formatTimestamp(attrs.participant_a_archived_at)
1055
- : undefined,
1056
- ],
1057
- [
1058
- "A blocked",
1059
- attrs.participant_a_blocked_at
1060
- ? formatTimestamp(attrs.participant_a_blocked_at)
1061
- : undefined,
1062
- ],
1063
- [
1064
- "A resolved",
1065
- attrs.participant_a_resolved_at
1066
- ? formatTimestamp(attrs.participant_a_resolved_at)
1067
- : undefined,
1068
- ],
1069
- ["B owner", formatUserActor(attrs.participant_b_owner_id, publicUsers)],
1070
- ["B channel", formatActor("channel", attrs.participant_b_channel_id)],
1071
- ["B seen", formatTimestamp(attrs.participant_b_seen_at)],
1072
- [
1073
- "B archived",
1074
- attrs.participant_b_archived_at
1075
- ? formatTimestamp(attrs.participant_b_archived_at)
1076
- : undefined,
1077
- ],
1078
- [
1079
- "B blocked",
1080
- attrs.participant_b_blocked_at
1081
- ? formatTimestamp(attrs.participant_b_blocked_at)
1082
- : undefined,
1083
- ],
1084
- [
1085
- "B resolved",
1086
- attrs.participant_b_resolved_at
1087
- ? formatTimestamp(attrs.participant_b_resolved_at)
1088
- : undefined,
1089
- ],
1090
- ]),
1091
- ),
1092
- renderSection("Messages", messageBlocks),
1093
- renderPagination(pagination?.nextCursor, pagination?.nextCommand),
1094
- renderCacheNote(cache),
1095
- ]);
1096
- }
1097
-
1098
- function renderMessage(
1099
- message: {
1100
- id: string;
1101
- attributes: MessageAttributes;
1102
- },
1103
- publicUsers: Map<string, string>,
1104
- ): string {
1105
- const attrs = message.attributes;
1106
- const headingParts = [
1107
- formatTimestamp(attrs.inserted_at),
1108
- shortId(message.id),
1109
- formatUserActor(attrs.sender_owner_id, publicUsers),
1110
- formatActor("channel", attrs.sender_channel_id),
1111
- formatActor("email-sender", attrs.external_email_sender_id),
1112
- ].filter((part) => part !== "-");
1113
- const contextPost = attrs.context_post_id
1114
- ? `Context post: ${attrs.context_post_id}\n`
1115
- : "";
1116
- const payload = attrs.payload
1117
- ? `\n\nPayload\n${indent(JSON.stringify(attrs.payload, null, 2))}`
1118
- : "";
1119
-
1120
- return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}${payload}`;
1121
- }
1122
-
1123
- function printSchemaResource(
1124
- context: CommandContext,
1125
- io: Io,
1126
- resource: {
1127
- id: string;
1128
- attributes: {
1129
- public_handle?: string | null;
1130
- name?: string | null;
1131
- inbox_schema?: Record<string, unknown> | null;
1132
- inbox_schema_hash?: string | null;
1133
- inbox_schema_updated_at?: string | null;
1134
- external_email_acceptance?: ExternalEmailAcceptance | null;
1135
- };
1136
- },
1137
- action?: string,
1138
- ): void {
1139
- printValue(
1140
- io,
1141
- context.outputMode,
1142
- context.outputMode === "json"
1143
- ? resource
1144
- : renderSchemaResource(resource, action),
1145
- );
1146
- }
1147
-
1148
- async function maybePrepareCachePlan(
1149
- args: ParsedArgs,
1150
- context: CommandContext,
1151
- scope: CacheScope | undefined,
1152
- ): Promise<CachePlan | undefined> {
1153
- return cacheFlags(args).sinceCache && scope
1154
- ? prepareCachePlan(context, scope)
1155
- : undefined;
1156
- }
1157
-
1158
- async function maybeSaveCacheTimestamp(
1159
- args: ParsedArgs,
1160
- context: CommandContext,
1161
- scope: CacheScope | undefined,
1162
- meta: Record<string, unknown> | undefined,
1163
- shouldSave: boolean,
1164
- ): Promise<string | undefined> {
1165
- return cacheFlags(args).saveCache && scope && shouldSave
1166
- ? saveCacheTimestamp(context, scope, meta)
1167
- : undefined;
1168
- }
1169
-
1170
- async function maybeInboxThreadsScope(
1171
- args: ParsedArgs,
1172
- context: CommandContext,
1173
- channelToken: string | undefined,
1174
- status: ThreadStatusFilter | undefined,
1175
- mailbox: MailboxFilter | undefined,
1176
- participantScope: ParticipantScope | undefined,
1177
- ): Promise<CacheScope | undefined> {
1178
- if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
1179
- return undefined;
1180
- }
1181
-
1182
- return inboxThreadsScope({
1183
- context,
1184
- actorKey: await authenticatedActorKey(context, channelToken),
1185
- status,
1186
- mailbox,
1187
- participant: stringFlag(args.flags, "participant"),
1188
- participantScope,
1189
- query: stringFlag(args.flags, "query"),
1190
- hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
1191
- before: stringFlag(args.flags, "before"),
1192
- });
1193
- }
1194
-
1195
- async function maybeInboxMessagesScope(
1196
- args: ParsedArgs,
1197
- context: CommandContext,
1198
- channelToken: string | undefined,
1199
- threadId: string,
1200
- ): Promise<CacheScope | undefined> {
1201
- if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
1202
- return undefined;
1203
- }
1204
-
1205
- return inboxMessagesScope({
1206
- context,
1207
- actorKey: await authenticatedActorKey(context, channelToken),
1208
- threadId,
1209
- query: stringFlag(args.flags, "query"),
1210
- hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
1211
- before: stringFlag(args.flags, "before"),
1212
- });
1213
- }
1214
-
1215
- function resolvedSince(
1216
- args: ParsedArgs,
1217
- cachePlan: CachePlan | undefined,
1218
- ): string | undefined {
1219
- return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
1220
- }
1221
-
1222
- function printCacheNote(io: Io, cache: CacheResult | undefined): void {
1223
- const note = renderCacheNote(cache);
1224
-
1225
- if (note) {
1226
- io.stdout(note);
1227
- }
1228
- }
1229
-
1230
- function renderCacheNote(cache: CacheResult | undefined): string | undefined {
1231
- if (!cache) {
1232
- return undefined;
1233
- }
1234
-
1235
- const details = [
1236
- cache.previousServerTimestamp
1237
- ? `used ${cache.previousServerTimestamp}`
1238
- : cache.hit
1239
- ? "used cache"
1240
- : "no cached timestamp",
1241
- cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
1242
- ].filter(Boolean);
1243
-
1244
- return `Cache: ${details.join("; ")}.`;
1245
- }
1246
-
1247
- function cacheFields(cache: CacheResult | undefined): Array<[string, string | undefined]> {
1248
- if (!cache) {
1249
- return [];
1250
- }
1251
-
1252
- return [
1253
- ["Cache scope", cache.scopeKey],
1254
- ["Cached timestamp", cache.previousServerTimestamp],
1255
- ["Saved timestamp", cache.savedServerTimestamp],
1256
- ];
1257
- }
1258
-
1259
- function renderSchemaResource(
1260
- resource: {
1261
- id: string;
1262
- attributes: {
1263
- public_handle?: string | null;
1264
- name?: string | null;
1265
- inbox_schema?: Record<string, unknown> | null;
1266
- inbox_schema_hash?: string | null;
1267
- inbox_schema_updated_at?: string | null;
1268
- external_email_acceptance?: ExternalEmailAcceptance | null;
1269
- };
1270
- },
1271
- action?: string,
1272
- ): string {
1273
- const attrs = resource.attributes;
1274
- const schema = attrs.inbox_schema
1275
- ? JSON.stringify(attrs.inbox_schema, null, 2)
1276
- : "No inbox schema.";
1277
-
1278
- return joinBlocks([
1279
- action,
1280
- `Inbox schema ${resource.id}`,
1281
- renderFields([
1282
- ["Handle", attrs.public_handle],
1283
- ["Channel", attrs.name],
1284
- ["Email acceptance", attrs.external_email_acceptance],
1285
- ["Hash", attrs.inbox_schema_hash],
1286
- ["Updated", formatTimestamp(attrs.inbox_schema_updated_at)],
1287
- ]),
1288
- renderSection("Schema", indent(schema)),
1289
- ]);
1290
- }
1291
-
1292
- function printEmailIntakeCollection(
1293
- args: ParsedArgs,
1294
- context: CommandContext,
1295
- io: Io,
1296
- response: {
1297
- items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
1298
- nextCursor?: string;
1299
- },
1300
- ): void {
1301
- printValue(
1302
- io,
1303
- context.outputMode,
1304
- context.outputMode === "json"
1305
- ? paginatedJson(args, {
1306
- items: response.items,
1307
- nextCursor: response.nextCursor,
1308
- })
1309
- : renderEmailIntakeCollection(args, response),
1310
- );
1311
- }
1312
-
1313
- function printEmailIntakeAction(
1314
- context: CommandContext,
1315
- io: Io,
1316
- action: string,
1317
- intake: { id: string; attributes: ExternalEmailIntakeAttributes },
1318
- ): void {
1319
- printValue(
1320
- io,
1321
- context.outputMode,
1322
- context.outputMode === "json"
1323
- ? intake
1324
- : joinBlocks([`${action}: ${intake.id}`, renderEmailIntake(intake)]),
1325
- );
1326
- }
1327
-
1328
- function renderEmailIntakeCollection(
1329
- args: ParsedArgs,
1330
- response: {
1331
- items: Array<{ id: string; attributes: ExternalEmailIntakeAttributes }>;
1332
- nextCursor?: string;
1333
- },
1334
- ): string {
1335
- const body =
1336
- response.items.length === 0
1337
- ? "No email intakes."
1338
- : response.items.map((intake) => renderEmailIntake(intake)).join("\n\n");
1339
-
1340
- const pagination = paginationInfo(args, response.nextCursor);
1341
- return joinBlocks([
1342
- body,
1343
- renderPagination(pagination?.nextCursor, pagination?.nextCommand),
1344
- ]);
1345
- }
1346
-
1347
- function renderEmailIntake(intake: {
1348
- id: string;
1349
- attributes: ExternalEmailIntakeAttributes;
1350
- }): string {
1351
- const attrs = intake.attributes;
1352
-
1353
- return joinBlocks([
1354
- `Email intake ${intake.id}`,
1355
- renderFields([
1356
- ["Subject", attrs.subject ?? ""],
1357
- ["Status", attrs.status ?? ""],
1358
- ["Decision", attrs.decision ?? ""],
1359
- ["Sender", formatActor("email-sender", attrs.external_email_sender_id)],
1360
- ["Target channel", formatActor("channel", attrs.target_channel_id)],
1361
- ["Raw recipient", attrs.raw_recipient ?? ""],
1362
- ["Received count", formatOptionalNumber(attrs.received_count)],
1363
- ["Attachment count", formatOptionalNumber(attrs.attachment_count)],
1364
- ["Attachments withheld", formatOptionalBoolean(attrs.attachments_withheld)],
1365
- ["Last received", formatTimestamp(attrs.last_received_at)],
1366
- ["Released", formatTimestamp(attrs.released_at)],
1367
- ["Released thread", formatActor("thread", attrs.released_thread_id)],
1368
- ]),
1369
- attrs.body ? renderSection("Body", indent(attrs.body)) : "",
1370
- ]);
1371
- }
1372
-
1373
- function renderAttachmentCollection(
1374
- args: ParsedArgs,
1375
- response: {
1376
- items: Array<{ id: string; attributes: MessageAttachmentAttributes }>;
1377
- nextCursor?: string;
1378
- },
1379
- ): string {
1380
- const body =
1381
- response.items.length === 0
1382
- ? "No attachments."
1383
- : response.items
1384
- .map((attachment) => {
1385
- const attrs = attachment.attributes;
1386
-
1387
- return joinBlocks([
1388
- `Attachment ${attachment.id}`,
1389
- renderFields([
1390
- ["Name", attrs.name ?? ""],
1391
- ["Content type", attrs.content_type ?? ""],
1392
- ["Content length", formatOptionalNumber(attrs.content_length)],
1393
- ["Message", formatActor("message", attrs.message_id)],
1394
- ]),
1395
- ]);
1396
- })
1397
- .join("\n\n");
1398
-
1399
- const pagination = paginationInfo(args, response.nextCursor);
1400
- return joinBlocks([
1401
- body,
1402
- renderPagination(pagination?.nextCursor, pagination?.nextCommand),
1403
- ]);
1404
- }
1405
-
1406
- function renderThreadAction(
1407
- action: string,
1408
- thread: { id: string; attributes: ThreadAttributes },
1409
- ): string {
1410
- const attrs = thread.attributes;
1411
-
1412
- return joinBlocks([
1413
- `${action}: ${thread.id}`,
1414
- renderFields([
1415
- ["Mailbox", attrs.mailbox_type],
1416
- ["Status", attrs.status],
1417
- ["Last message", formatTimestamp(attrs.last_message_at)],
1418
- ["Opened", formatTimestamp(attrs.opened_at)],
1419
- [
1420
- "Expires",
1421
- attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
1422
- ],
1423
- ]),
1424
- ]);
1425
- }
1426
-
1427
- function formatActor(
1428
- kind: "user" | "channel" | "email-sender" | "thread" | "message",
1429
- id?: string | null,
1430
- ): string {
1431
- if (!id) {
1432
- return "-";
1433
- }
1434
-
1435
- return `${kind}:${shortId(id)}`;
1436
- }
1437
-
1438
- function formatOptionalBoolean(value?: boolean | null): string {
1439
- if (value === undefined || value === null) {
1440
- return "";
1441
- }
1442
-
1443
- return value ? "yes" : "no";
1444
- }
1445
-
1446
- function formatOptionalNumber(value?: number | null): string {
1447
- if (value === undefined || value === null) {
1448
- return "";
1449
- }
1450
-
1451
- return String(value);
1452
- }
1453
-
1454
- function formatUserActor(
1455
- id: string | null | undefined,
1456
- publicUsers: Map<string, string>,
1457
- ): string {
1458
- if (!id) {
1459
- return "-";
1460
- }
1461
-
1462
- return publicUsers.get(id) ?? formatActor("user", id);
1463
- }
1464
-
1465
- function ownerIdsForThreadDisplay(
1466
- thread: { attributes: ThreadAttributes },
1467
- messages: Array<{ attributes: MessageAttributes }>,
1468
- ): string[] {
1469
- const ids = [
1470
- thread.attributes.participant_a_owner_id,
1471
- thread.attributes.participant_b_owner_id,
1472
- ...messages.map((message) => message.attributes.sender_owner_id),
1473
- ];
1474
-
1475
- return Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
1476
- }
1477
-
1478
- function ownerIdsForThreadList(
1479
- peers: Array<ThreadPeer>,
1480
- ): string[] {
1481
- return Array.from(
1482
- new Set(
1483
- peers
1484
- .map((peer) => peer.ownerId)
1485
- .filter((id): id is string => Boolean(id)),
1486
- ),
1487
- );
1488
- }
1489
-
1490
- interface ThreadPeer {
1491
- ownerId?: string | null;
1492
- channelId?: string | null;
1493
- }
1494
-
1495
- function threadPeer(attrs: ThreadAttributes, actor: WhoamiActor): ThreadPeer {
1496
- const side = threadActorSide(attrs, actor);
1497
-
1498
- if (side === "a") {
1499
- return {
1500
- ownerId: attrs.participant_b_owner_id,
1501
- channelId: attrs.participant_b_channel_id,
1502
- };
1503
- }
1504
-
1505
- if (side === "b") {
1506
- return {
1507
- ownerId: attrs.participant_a_owner_id,
1508
- channelId: attrs.participant_a_channel_id,
1509
- };
1510
- }
1511
-
1512
- return {
1513
- ownerId: attrs.participant_a_owner_id,
1514
- channelId: attrs.participant_a_channel_id,
1515
- };
1516
- }
1517
-
1518
- function threadActorSide(
1519
- attrs: ThreadAttributes,
1520
- actor: WhoamiActor,
1521
- ): "a" | "b" | undefined {
1522
- if (actor.type === "channel") {
1523
- if (attrs.participant_a_channel_id === actor.id) {
1524
- return "a";
1525
- }
1526
-
1527
- if (attrs.participant_b_channel_id === actor.id) {
1528
- return "b";
1529
- }
1530
- }
1531
-
1532
- const ownerId = actor.type === "user" ? actor.id : actor.owner_id;
1533
-
1534
- if (attrs.participant_a_owner_id === ownerId) {
1535
- return "a";
1536
- }
1537
-
1538
- if (attrs.participant_b_owner_id === ownerId) {
1539
- return "b";
1540
- }
1541
-
1542
- return undefined;
1543
- }
1544
-
1545
- function formatThreadPeer(
1546
- peer: ThreadPeer | undefined,
1547
- publicUsers: Map<string, string>,
1548
- ): string {
1549
- if (!peer) {
1550
- return "-";
1551
- }
1552
-
1553
- if (peer.channelId) {
1554
- return formatActor("channel", peer.channelId);
1555
- }
1556
-
1557
- return formatUserActor(peer.ownerId, publicUsers);
1558
- }
1559
-
1560
- function publicUserHandlesById(
1561
- users: Array<{ id: string; attributes: { public_handle?: string | null } }>,
1562
- ): Map<string, string> {
1563
- const handles = new Map<string, string>();
1564
-
1565
- for (const user of users) {
1566
- const handle = user.attributes.public_handle?.trim();
1567
-
1568
- if (handle) {
1569
- handles.set(user.id, `@${handle}`);
1570
- }
1571
- }
1572
-
1573
- return handles;
1574
- }