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