@clankmates/cli 0.11.0 → 0.12.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.
@@ -1,3 +1,17 @@
1
+ import {
2
+ assertSinceFlags,
3
+ authenticatedActorKey,
4
+ cacheFlags,
5
+ cacheResult,
6
+ changeResponseMeta,
7
+ inboxMessagesScope,
8
+ inboxThreadsScope,
9
+ prepareCachePlan,
10
+ saveCacheTimestamp,
11
+ type CachePlan,
12
+ type CacheResult,
13
+ type CacheScope,
14
+ } from "../lib/cache";
1
15
  import {
2
16
  booleanFlag,
3
17
  integerFlag,
@@ -30,6 +44,7 @@ import type {
30
44
  MailboxFilter,
31
45
  MessageAttachmentAttributes,
32
46
  MessageAttributes,
47
+ ParticipantScope,
33
48
  ThreadAttributes,
34
49
  ThreadStatusFilter,
35
50
  WhoamiActor,
@@ -42,32 +57,85 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
42
57
  switch (subcommand) {
43
58
  case "list": {
44
59
  const channelToken = stringFlag(args.flags, "channelToken");
60
+ assertSinceFlags(args);
61
+ const status = parseStatusFilter(stringFlag(args.flags, "status"));
62
+ const mailbox = parseMailboxFilter(stringFlag(args.flags, "mailbox"));
63
+ const participantScope = parseParticipantScope(
64
+ stringFlag(args.flags, "participantScope"),
65
+ );
66
+ const cacheScope = await maybeInboxThreadsScope(
67
+ args,
68
+ context,
69
+ channelToken,
70
+ status,
71
+ mailbox,
72
+ participantScope,
73
+ );
74
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
45
75
  const response = await context.client.listInboxThreads({
46
- status: parseStatusFilter(stringFlag(args.flags, "status")),
47
- mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
76
+ status,
77
+ mailbox,
48
78
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
49
79
  cursor: stringFlag(args.flags, "cursor"),
80
+ before: stringFlag(args.flags, "before"),
50
81
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
51
- since: stringFlag(args.flags, "since"),
82
+ since: resolvedSince(args, cachePlan),
83
+ participant: stringFlag(args.flags, "participant"),
84
+ participantScope,
85
+ query: stringFlag(args.flags, "query"),
86
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
52
87
  channelToken,
53
88
  });
89
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
90
+ args,
91
+ context,
92
+ cacheScope,
93
+ response.meta,
94
+ response.nextCursor === undefined,
95
+ );
54
96
 
55
- await printThreadCollection(args, context, io, response, channelToken);
97
+ await printThreadCollection(
98
+ args,
99
+ context,
100
+ io,
101
+ response,
102
+ channelToken,
103
+ cacheResult(cachePlan, savedServerTimestamp),
104
+ );
56
105
  return;
57
106
  }
58
107
 
59
108
  case "show": {
60
109
  const threadId = requiredPositional(args.positionals, 1, "Missing thread id");
61
110
  const channelToken = stringFlag(args.flags, "channelToken");
111
+ assertSinceFlags(args);
112
+ const cacheScope = await maybeInboxMessagesScope(
113
+ args,
114
+ context,
115
+ channelToken,
116
+ threadId,
117
+ );
118
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
62
119
  const thread = await context.client.getThread(threadId, channelToken);
63
120
  const messages = await context.client.listMessagesForThread({
64
121
  threadId,
65
122
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
66
123
  cursor: stringFlag(args.flags, "cursor"),
124
+ before: stringFlag(args.flags, "before"),
67
125
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
68
- since: stringFlag(args.flags, "since"),
126
+ since: resolvedSince(args, cachePlan),
127
+ query: stringFlag(args.flags, "query"),
128
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
69
129
  channelToken,
70
130
  });
131
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
132
+ args,
133
+ context,
134
+ cacheScope,
135
+ messages.meta,
136
+ messages.nextCursor === undefined,
137
+ );
138
+ const cache = cacheResult(cachePlan, savedServerTimestamp);
71
139
  const ownerIds = ownerIdsForThreadDisplay(thread, messages.items);
72
140
  const publicUsers =
73
141
  context.outputMode === "json" || ownerIds.length === 0
@@ -85,22 +153,54 @@ export async function runInboxCommand(args: ParsedArgs, io: Io): Promise<void> {
85
153
  messages: messages.items,
86
154
  nextCursor: messages.nextCursor,
87
155
  meta: messages.meta,
156
+ ...(cache ? { cache } : {}),
88
157
  })
89
- : renderThreadWithMessages(args, thread, messages, publicUsers),
158
+ : renderThreadWithMessages(args, thread, messages, publicUsers, cache),
90
159
  );
91
160
  return;
92
161
  }
93
162
 
94
163
  case "changes": {
95
164
  const channelToken = stringFlag(args.flags, "channelToken");
165
+ assertSinceFlags(args);
166
+ const status = parseStatusFilter(stringFlag(args.flags, "status"));
167
+ const mailbox = parseMailboxFilter(stringFlag(args.flags, "mailbox"));
168
+ const participantScope = parseParticipantScope(
169
+ stringFlag(args.flags, "participantScope"),
170
+ );
171
+ const cacheScope = await maybeInboxThreadsScope(
172
+ args,
173
+ context,
174
+ channelToken,
175
+ status,
176
+ mailbox,
177
+ participantScope,
178
+ );
179
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
96
180
  const response = await context.client.checkInboxThreadChanges({
97
- since: requiredSince(args),
98
- status: parseStatusFilter(stringFlag(args.flags, "status")),
99
- mailbox: parseMailboxFilter(stringFlag(args.flags, "mailbox")),
181
+ since: requiredSince(args, cachePlan, "inbox thread"),
182
+ status,
183
+ mailbox,
184
+ participant: stringFlag(args.flags, "participant"),
185
+ participantScope,
186
+ query: stringFlag(args.flags, "query"),
187
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
100
188
  channelToken,
101
189
  });
190
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
191
+ args,
192
+ context,
193
+ cacheScope,
194
+ changeResponseMeta(response),
195
+ response.has_updates === false,
196
+ );
102
197
 
103
- printChangeCheckResponse(context, io, response);
198
+ printChangeCheckResponse(
199
+ context,
200
+ io,
201
+ response,
202
+ cacheResult(cachePlan, savedServerTimestamp),
203
+ );
104
204
  return;
105
205
  }
106
206
 
@@ -626,6 +726,18 @@ function parseMailboxFilter(value: string | undefined): MailboxFilter | undefine
626
726
  throw new CliError("--mailbox must be one of: account, channel, all", 2);
627
727
  }
628
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
+
629
741
  function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
630
742
  if (!value) {
631
743
  return undefined;
@@ -638,9 +750,28 @@ function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | un
638
750
  throw new CliError("--order must be one of: latest, oldest", 2);
639
751
  }
640
752
 
641
- function requiredSince(args: ParsedArgs): string {
753
+ function requiredSince(
754
+ args: ParsedArgs,
755
+ cachePlan?: CachePlan,
756
+ label = "resource",
757
+ ): string {
642
758
  const since = stringFlag(args.flags, "since");
643
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
+
644
775
  if (!since) {
645
776
  throw new CliError("Missing `--since`", 2);
646
777
  }
@@ -657,13 +788,37 @@ async function runInboxMessagesCommand(
657
788
 
658
789
  switch (subcommand) {
659
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);
660
801
  const response = await context.client.checkThreadMessageChanges({
661
- threadId: requiredPositional(args.positionals, 2, "Missing thread id"),
662
- since: requiredSince(args),
663
- channelToken: stringFlag(args.flags, "channelToken"),
802
+ threadId,
803
+ since: requiredSince(args, cachePlan, "inbox message"),
804
+ query: stringFlag(args.flags, "query"),
805
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
806
+ channelToken,
664
807
  });
808
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
809
+ args,
810
+ context,
811
+ cacheScope,
812
+ changeResponseMeta(response),
813
+ response.has_updates === false,
814
+ );
665
815
 
666
- printChangeCheckResponse(context, io, response);
816
+ printChangeCheckResponse(
817
+ context,
818
+ io,
819
+ response,
820
+ cacheResult(cachePlan, savedServerTimestamp),
821
+ );
667
822
  return;
668
823
  }
669
824
 
@@ -676,12 +831,13 @@ function printChangeCheckResponse(
676
831
  context: CommandContext,
677
832
  io: Io,
678
833
  response: ChangeCheckResponse,
834
+ cache?: CacheResult,
679
835
  ): void {
680
836
  printValue(
681
837
  io,
682
838
  context.outputMode,
683
839
  context.outputMode === "json"
684
- ? response
840
+ ? { ...response, ...(cache ? { cache } : {}) }
685
841
  : renderFields([
686
842
  ["Has updates", response.has_updates ? "yes" : "no"],
687
843
  ["Server time", formatTimestamp(response.server_time)],
@@ -691,6 +847,7 @@ function printChangeCheckResponse(
691
847
  ? undefined
692
848
  : `${response.recommended_poll_after_ms}ms`,
693
849
  ],
850
+ ...cacheFields(cache),
694
851
  ]),
695
852
  );
696
853
  }
@@ -803,6 +960,7 @@ async function printThreadCollection(
803
960
  meta?: Record<string, unknown>;
804
961
  },
805
962
  channelToken?: string,
963
+ cache?: CacheResult,
806
964
  ): Promise<void> {
807
965
  if (context.outputMode === "json") {
808
966
  printJson(
@@ -811,6 +969,7 @@ async function printThreadCollection(
811
969
  items: response.items,
812
970
  nextCursor: response.nextCursor,
813
971
  meta: response.meta,
972
+ ...(cache ? { cache } : {}),
814
973
  }),
815
974
  );
816
975
  return Promise.resolve();
@@ -848,6 +1007,8 @@ async function printThreadCollection(
848
1007
  if (message) {
849
1008
  io.stdout(message);
850
1009
  }
1010
+
1011
+ printCacheNote(io, cache);
851
1012
  }
852
1013
 
853
1014
  function renderThreadWithMessages(
@@ -858,6 +1019,7 @@ function renderThreadWithMessages(
858
1019
  nextCursor?: string;
859
1020
  },
860
1021
  publicUsers: Map<string, string>,
1022
+ cache?: CacheResult,
861
1023
  ): string {
862
1024
  const attrs = thread.attributes;
863
1025
  const messageBlocks =
@@ -929,6 +1091,7 @@ function renderThreadWithMessages(
929
1091
  ),
930
1092
  renderSection("Messages", messageBlocks),
931
1093
  renderPagination(pagination?.nextCursor, pagination?.nextCommand),
1094
+ renderCacheNote(cache),
932
1095
  ]);
933
1096
  }
934
1097
 
@@ -982,6 +1145,117 @@ function printSchemaResource(
982
1145
  );
983
1146
  }
984
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
+
985
1259
  function renderSchemaResource(
986
1260
  resource: {
987
1261
  id: string;