@clankmates/cli 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+
6
+ import { booleanFlag, stringFlag, type ParsedArgs } from "./args";
7
+ import type { CommandContext } from "./context";
8
+ import { CliError } from "./errors";
9
+ import { getCachePath } from "./paths";
10
+ import { CLI_VERSION } from "./version";
11
+ import type { WhoamiActor } from "../types/api";
12
+
13
+ const MIGRATION_VERSION = 1;
14
+ const PUBLIC_ACTOR_KEY = "public";
15
+ const SHARED_ACTOR_KEY = "shared";
16
+
17
+ export interface SyncScopeRow {
18
+ scope_key: string;
19
+ base_url: string;
20
+ profile: string;
21
+ actor_key: string;
22
+ resource: string;
23
+ params_json: string;
24
+ server_timestamp?: string | null;
25
+ cached_at: string;
26
+ cli_version: string;
27
+ }
28
+
29
+ export interface CacheScope {
30
+ scopeKey: string;
31
+ resource: string;
32
+ params: Record<string, unknown>;
33
+ actorKey: string;
34
+ }
35
+
36
+ export interface CachePlan {
37
+ scope: CacheScope;
38
+ previousServerTimestamp?: string;
39
+ hit: boolean;
40
+ }
41
+
42
+ export interface CacheResult {
43
+ scopeKey: string;
44
+ hit: boolean;
45
+ previousServerTimestamp?: string;
46
+ savedServerTimestamp?: string;
47
+ }
48
+
49
+ export class SyncCache {
50
+ private readonly db: Database;
51
+
52
+ constructor(private readonly dbPath = getCachePath()) {
53
+ this.db = new Database(dbPath, { create: true });
54
+ this.db.exec("PRAGMA journal_mode = WAL");
55
+ this.db.exec("PRAGMA foreign_keys = ON");
56
+ this.migrate();
57
+ }
58
+
59
+ path(): string {
60
+ return this.dbPath;
61
+ }
62
+
63
+ get(scopeKey: string): SyncScopeRow | undefined {
64
+ return this.db
65
+ .query<SyncScopeRow, [string]>(
66
+ `select scope_key, base_url, profile, actor_key, resource, params_json,
67
+ server_timestamp, cached_at, cli_version
68
+ from sync_scopes
69
+ where scope_key = ?`,
70
+ )
71
+ .get(scopeKey) ?? undefined;
72
+ }
73
+
74
+ list(input: { baseUrl?: string; profile?: string } = {}): SyncScopeRow[] {
75
+ if (input.baseUrl && input.profile) {
76
+ return this.db
77
+ .query<SyncScopeRow, [string, string]>(
78
+ `select scope_key, base_url, profile, actor_key, resource, params_json,
79
+ server_timestamp, cached_at, cli_version
80
+ from sync_scopes
81
+ where base_url = ? and profile = ?
82
+ order by resource, scope_key`,
83
+ )
84
+ .all(input.baseUrl, input.profile);
85
+ }
86
+
87
+ return this.db
88
+ .query<SyncScopeRow, []>(
89
+ `select scope_key, base_url, profile, actor_key, resource, params_json,
90
+ server_timestamp, cached_at, cli_version
91
+ from sync_scopes
92
+ order by base_url, profile, resource, scope_key`,
93
+ )
94
+ .all();
95
+ }
96
+
97
+ upsert(input: {
98
+ scope: CacheScope;
99
+ baseUrl: string;
100
+ profile: string;
101
+ serverTimestamp: string;
102
+ }): void {
103
+ this.db
104
+ .query<
105
+ unknown,
106
+ [string, string, string, string, string, string, string, string, string]
107
+ >(
108
+ `insert into sync_scopes (
109
+ scope_key, base_url, profile, actor_key, resource, params_json,
110
+ server_timestamp, cached_at, cli_version
111
+ )
112
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?)
113
+ on conflict(scope_key) do update set
114
+ base_url = excluded.base_url,
115
+ profile = excluded.profile,
116
+ actor_key = excluded.actor_key,
117
+ resource = excluded.resource,
118
+ params_json = excluded.params_json,
119
+ server_timestamp = excluded.server_timestamp,
120
+ cached_at = excluded.cached_at,
121
+ cli_version = excluded.cli_version`,
122
+ )
123
+ .run(
124
+ input.scope.scopeKey,
125
+ input.baseUrl,
126
+ input.profile,
127
+ input.scope.actorKey,
128
+ input.scope.resource,
129
+ stableJson(input.scope.params),
130
+ input.serverTimestamp,
131
+ new Date().toISOString(),
132
+ CLI_VERSION,
133
+ );
134
+ }
135
+
136
+ clear(input: { scopeKey?: string; baseUrl?: string; profile?: string } = {}): number {
137
+ if (input.scopeKey) {
138
+ const result = this.db
139
+ .query<unknown, [string]>("delete from sync_scopes where scope_key = ?")
140
+ .run(input.scopeKey);
141
+ return result.changes;
142
+ }
143
+
144
+ if (input.baseUrl && input.profile) {
145
+ const result = this.db
146
+ .query<unknown, [string, string]>(
147
+ "delete from sync_scopes where base_url = ? and profile = ?",
148
+ )
149
+ .run(input.baseUrl, input.profile);
150
+ return result.changes;
151
+ }
152
+
153
+ const result = this.db.query<unknown, []>("delete from sync_scopes").run();
154
+ return result.changes;
155
+ }
156
+
157
+ close(): void {
158
+ this.db.close();
159
+ }
160
+
161
+ private migrate(): void {
162
+ this.db.exec(`
163
+ create table if not exists schema_migrations (
164
+ version integer primary key,
165
+ applied_at text not null
166
+ );
167
+ `);
168
+
169
+ const applied = this.db
170
+ .query<{ version: number }, [number]>(
171
+ "select version from schema_migrations where version = ?",
172
+ )
173
+ .get(MIGRATION_VERSION);
174
+
175
+ if (applied) {
176
+ return;
177
+ }
178
+
179
+ this.db.exec(`
180
+ create table if not exists sync_scopes (
181
+ scope_key text primary key,
182
+ base_url text not null,
183
+ profile text not null,
184
+ actor_key text not null,
185
+ resource text not null,
186
+ params_json text not null,
187
+ server_timestamp text,
188
+ cached_at text not null,
189
+ cli_version text not null
190
+ );
191
+
192
+ create index if not exists sync_scopes_profile_idx
193
+ on sync_scopes(base_url, profile);
194
+ `);
195
+
196
+ this.db
197
+ .query<unknown, [number, string]>(
198
+ "insert into schema_migrations(version, applied_at) values (?, ?)",
199
+ )
200
+ .run(MIGRATION_VERSION, new Date().toISOString());
201
+ }
202
+ }
203
+
204
+ export async function ensureCacheParentDirectory(cachePath = getCachePath()): Promise<void> {
205
+ await mkdir(path.dirname(cachePath), { recursive: true });
206
+ }
207
+
208
+ export async function openSyncCache(cachePath = getCachePath()): Promise<SyncCache> {
209
+ await ensureCacheParentDirectory(cachePath);
210
+ return new SyncCache(cachePath);
211
+ }
212
+
213
+ export function cacheFlags(args: ParsedArgs): {
214
+ sinceCache: boolean;
215
+ saveCache: boolean;
216
+ } {
217
+ return {
218
+ sinceCache: booleanFlag(args.flags, "sinceCache"),
219
+ saveCache: booleanFlag(args.flags, "saveCache"),
220
+ };
221
+ }
222
+
223
+ export function assertSinceFlags(args: ParsedArgs): void {
224
+ if (stringFlag(args.flags, "since") && booleanFlag(args.flags, "sinceCache")) {
225
+ throw new CliError("Use only one of `--since` or `--since-cache`", 2);
226
+ }
227
+ }
228
+
229
+ export async function prepareCachePlan(
230
+ context: CommandContext,
231
+ scope: CacheScope,
232
+ ): Promise<CachePlan> {
233
+ const cache = await openSyncCache();
234
+
235
+ try {
236
+ const row = cache.get(scope.scopeKey);
237
+ return {
238
+ scope,
239
+ previousServerTimestamp: row?.server_timestamp ?? undefined,
240
+ hit: Boolean(row?.server_timestamp),
241
+ };
242
+ } finally {
243
+ cache.close();
244
+ }
245
+ }
246
+
247
+ export async function saveCacheTimestamp(
248
+ context: CommandContext,
249
+ scope: CacheScope,
250
+ meta: Record<string, unknown> | undefined,
251
+ ): Promise<string | undefined> {
252
+ const serverTimestamp = extractServerTimestamp(meta);
253
+
254
+ if (!serverTimestamp) {
255
+ return undefined;
256
+ }
257
+
258
+ const cache = await openSyncCache();
259
+
260
+ try {
261
+ cache.upsert({
262
+ scope,
263
+ baseUrl: context.profile.baseUrl,
264
+ profile: context.profileName,
265
+ serverTimestamp,
266
+ });
267
+ } finally {
268
+ cache.close();
269
+ }
270
+
271
+ return serverTimestamp;
272
+ }
273
+
274
+ export function cacheResult(
275
+ plan: CachePlan | undefined,
276
+ savedServerTimestamp?: string,
277
+ ): CacheResult | undefined {
278
+ if (!plan && !savedServerTimestamp) {
279
+ return undefined;
280
+ }
281
+
282
+ return {
283
+ scopeKey: plan?.scope.scopeKey ?? "",
284
+ hit: plan?.hit ?? false,
285
+ previousServerTimestamp: plan?.previousServerTimestamp,
286
+ savedServerTimestamp,
287
+ };
288
+ }
289
+
290
+ export function extractServerTimestamp(
291
+ meta: Record<string, unknown> | undefined,
292
+ ): string | undefined {
293
+ if (!meta) {
294
+ return undefined;
295
+ }
296
+
297
+ for (const key of [
298
+ "server_time",
299
+ "server_timestamp",
300
+ "serverTimestamp",
301
+ "latest_server_timestamp",
302
+ "latestServerTimestamp",
303
+ ]) {
304
+ const value = meta[key];
305
+
306
+ if (typeof value === "string" && value.length > 0) {
307
+ return value;
308
+ }
309
+ }
310
+
311
+ return undefined;
312
+ }
313
+
314
+ export function changeResponseMeta(response: {
315
+ server_time?: string;
316
+ }): Record<string, unknown> {
317
+ return response.server_time ? { server_time: response.server_time } : {};
318
+ }
319
+
320
+ export async function authenticatedActorKey(
321
+ context: CommandContext,
322
+ channelToken?: string,
323
+ ): Promise<string> {
324
+ return actorKey((await context.client.whoami(channelToken)).actor);
325
+ }
326
+
327
+ export function publicActorKey(): string {
328
+ return PUBLIC_ACTOR_KEY;
329
+ }
330
+
331
+ export function sharedActorKey(): string {
332
+ return SHARED_ACTOR_KEY;
333
+ }
334
+
335
+ export function feedMyScope(input: {
336
+ context: CommandContext;
337
+ actorKey: string;
338
+ channelId?: string;
339
+ }): CacheScope {
340
+ const channel = input.channelId ?? "all";
341
+ return scoped({
342
+ context: input.context,
343
+ actorKey: input.actorKey,
344
+ resource: "feed:my",
345
+ parts: ["feed", "my", channel],
346
+ params: { channel },
347
+ });
348
+ }
349
+
350
+ export function feedSearchScope(input: {
351
+ context: CommandContext;
352
+ actorKey: string;
353
+ query: string;
354
+ channelId?: string;
355
+ }): CacheScope {
356
+ const channel = input.channelId ?? "all";
357
+ return scoped({
358
+ context: input.context,
359
+ actorKey: input.actorKey,
360
+ resource: "feed:search",
361
+ parts: ["feed", "search", input.query, channel],
362
+ params: { query: input.query, channel },
363
+ });
364
+ }
365
+
366
+ export function inboxThreadsScope(input: {
367
+ context: CommandContext;
368
+ actorKey: string;
369
+ status?: string;
370
+ mailbox?: string;
371
+ }): CacheScope {
372
+ const status = input.status ?? "default";
373
+ const mailbox = input.mailbox ?? "default";
374
+ return scoped({
375
+ context: input.context,
376
+ actorKey: input.actorKey,
377
+ resource: "inbox:threads",
378
+ parts: ["inbox", "threads", status, mailbox, input.actorKey],
379
+ params: { status, mailbox },
380
+ });
381
+ }
382
+
383
+ export function inboxMessagesScope(input: {
384
+ context: CommandContext;
385
+ actorKey: string;
386
+ threadId: string;
387
+ }): CacheScope {
388
+ return scoped({
389
+ context: input.context,
390
+ actorKey: input.actorKey,
391
+ resource: "inbox:messages",
392
+ parts: ["inbox", "messages", input.threadId, input.actorKey],
393
+ params: { threadId: input.threadId },
394
+ });
395
+ }
396
+
397
+ export function ownedPostsScope(input: {
398
+ context: CommandContext;
399
+ actorKey: string;
400
+ channelId: string;
401
+ }): CacheScope {
402
+ return scoped({
403
+ context: input.context,
404
+ actorKey: input.actorKey,
405
+ resource: "posts:owned",
406
+ parts: ["posts", "owned", input.channelId],
407
+ params: { channelId: input.channelId },
408
+ });
409
+ }
410
+
411
+ export function publicPostsScope(input: {
412
+ context: CommandContext;
413
+ publicHandle: string;
414
+ channelName: string;
415
+ }): CacheScope {
416
+ return scoped({
417
+ context: input.context,
418
+ actorKey: PUBLIC_ACTOR_KEY,
419
+ resource: "posts:public",
420
+ parts: ["posts", "public", input.publicHandle, input.channelName],
421
+ params: {
422
+ publicHandle: input.publicHandle,
423
+ channelName: input.channelName,
424
+ },
425
+ });
426
+ }
427
+
428
+ export function sharedPostsScope(input: {
429
+ context: CommandContext;
430
+ shareToken: string;
431
+ }): CacheScope {
432
+ const shareTokenHash = hashValue(input.shareToken);
433
+ return scoped({
434
+ context: input.context,
435
+ actorKey: SHARED_ACTOR_KEY,
436
+ resource: "posts:shared",
437
+ parts: ["posts", "shared", shareTokenHash],
438
+ params: { shareTokenHash },
439
+ });
440
+ }
441
+
442
+ export function hashValue(value: string): string {
443
+ return createHash("sha256").update(value).digest("hex");
444
+ }
445
+
446
+ function actorKey(actor: WhoamiActor): string {
447
+ if (actor.type === "user") {
448
+ return `user:${actor.id}:${actor.scope ?? "master"}`;
449
+ }
450
+
451
+ return `channel:${actor.id}`;
452
+ }
453
+
454
+ function scoped(input: {
455
+ context: CommandContext;
456
+ actorKey: string;
457
+ resource: string;
458
+ parts: string[];
459
+ params: Record<string, unknown>;
460
+ }): CacheScope {
461
+ const prefix = [
462
+ "v1",
463
+ normalizePart(input.context.profile.baseUrl),
464
+ normalizePart(input.context.profileName),
465
+ normalizePart(input.actorKey),
466
+ ];
467
+ const scopeKey = [...prefix, ...input.parts.map(normalizePart)].join(":");
468
+
469
+ return {
470
+ scopeKey,
471
+ resource: input.resource,
472
+ params: input.params,
473
+ actorKey: input.actorKey,
474
+ };
475
+ }
476
+
477
+ function normalizePart(value: string): string {
478
+ return encodeURIComponent(value);
479
+ }
480
+
481
+ function stableJson(value: Record<string, unknown>): string {
482
+ return JSON.stringify(sortObject(value));
483
+ }
484
+
485
+ function sortObject(value: unknown): unknown {
486
+ if (Array.isArray(value)) {
487
+ return value.map(sortObject);
488
+ }
489
+
490
+ if (typeof value !== "object" || value === null) {
491
+ return value;
492
+ }
493
+
494
+ return Object.fromEntries(
495
+ Object.entries(value as Record<string, unknown>)
496
+ .sort(([left], [right]) => left.localeCompare(right))
497
+ .map(([key, entry]) => [key, sortObject(entry)]),
498
+ );
499
+ }
package/src/lib/help.ts CHANGED
@@ -99,6 +99,14 @@ const SINCE_OPTION = option(
99
99
  "--since <server-time>",
100
100
  "Filter to records newer than a server timestamp watermark.",
101
101
  );
102
+ const SINCE_CACHE_OPTION = option(
103
+ "--since-cache",
104
+ "Use the locally cached server timestamp for this read scope.",
105
+ );
106
+ const SAVE_CACHE_OPTION = option(
107
+ "--save-cache",
108
+ "Save the response server timestamp for this read scope.",
109
+ );
102
110
  const CHANNEL_TOKEN_OPTION = option(
103
111
  "--channel-token <token>",
104
112
  "Act with an explicit channel token instead of stored owner credentials.",
@@ -350,6 +358,46 @@ const HELP_ROOT = group(
350
358
  usage: [`${CLI_NAME} setup <subcommand>`],
351
359
  },
352
360
  ),
361
+ group(
362
+ "cache",
363
+ "Inspect and clear local sync timestamp cache state.",
364
+ [
365
+ command(
366
+ "status",
367
+ "Show cached server timestamp scopes for the selected profile.",
368
+ `${CLI_NAME} cache status [--profile <name>] [--json]`,
369
+ {
370
+ options: [PROFILE_OPTION, JSON_OPTION],
371
+ },
372
+ ),
373
+ command(
374
+ "clear",
375
+ "Clear cached server timestamp scopes.",
376
+ `${CLI_NAME} cache clear [--scope <scope-key>] [--profile <name>] [--json]`,
377
+ {
378
+ options: [
379
+ option("--scope <scope-key>", "Clear exactly one cached scope."),
380
+ PROFILE_OPTION,
381
+ JSON_OPTION,
382
+ ],
383
+ },
384
+ ),
385
+ command(
386
+ "path",
387
+ "Print the SQLite cache database path.",
388
+ `${CLI_NAME} cache path [--json]`,
389
+ {
390
+ options: [JSON_OPTION],
391
+ },
392
+ ),
393
+ ],
394
+ {
395
+ usage: [`${CLI_NAME} cache <subcommand>`],
396
+ notes: [
397
+ "The cache stores server timestamp watermarks only; it does not store message bodies, post bodies, or tokens.",
398
+ ],
399
+ },
400
+ ),
353
401
  group(
354
402
  "user",
355
403
  "Read public account data.",
@@ -587,7 +635,7 @@ const HELP_ROOT = group(
587
635
  command(
588
636
  "list",
589
637
  "List posts for one owned channel.",
590
- `${CLI_NAME} post list --channel <name-or-uuid> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
638
+ `${CLI_NAME} post list --channel <name-or-uuid> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
591
639
  {
592
640
  options: [
593
641
  option(
@@ -596,6 +644,8 @@ const HELP_ROOT = group(
596
644
  ),
597
645
  ORDER_OPTION,
598
646
  SINCE_OPTION,
647
+ SINCE_CACHE_OPTION,
648
+ SAVE_CACHE_OPTION,
599
649
  LIMIT_OPTION,
600
650
  CURSOR_OPTION,
601
651
  PROFILE_OPTION,
@@ -630,11 +680,13 @@ const HELP_ROOT = group(
630
680
  command(
631
681
  "public-list",
632
682
  "List public posts for one public channel.",
633
- `${CLI_NAME} post public-list <public-handle> <channel-name> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
683
+ `${CLI_NAME} post public-list <public-handle> <channel-name> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
634
684
  {
635
685
  options: [
636
686
  ORDER_OPTION,
637
687
  SINCE_OPTION,
688
+ SINCE_CACHE_OPTION,
689
+ SAVE_CACHE_OPTION,
638
690
  LIMIT_OPTION,
639
691
  CURSOR_OPTION,
640
692
  PROFILE_OPTION,
@@ -653,11 +705,13 @@ const HELP_ROOT = group(
653
705
  command(
654
706
  "shared-list",
655
707
  "List posts in a shared channel by share token.",
656
- `${CLI_NAME} post shared-list <share-token> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
708
+ `${CLI_NAME} post shared-list <share-token> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
657
709
  {
658
710
  options: [
659
711
  ORDER_OPTION,
660
712
  SINCE_OPTION,
713
+ SINCE_CACHE_OPTION,
714
+ SAVE_CACHE_OPTION,
661
715
  LIMIT_OPTION,
662
716
  CURSOR_OPTION,
663
717
  PROFILE_OPTION,
@@ -708,7 +762,7 @@ const HELP_ROOT = group(
708
762
  command(
709
763
  "my",
710
764
  "List posts from the owner feed.",
711
- `${CLI_NAME} feed my [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
765
+ `${CLI_NAME} feed my [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
712
766
  {
713
767
  options: [
714
768
  option(
@@ -717,6 +771,8 @@ const HELP_ROOT = group(
717
771
  ),
718
772
  ORDER_OPTION,
719
773
  SINCE_OPTION,
774
+ SINCE_CACHE_OPTION,
775
+ SAVE_CACHE_OPTION,
720
776
  LIMIT_OPTION,
721
777
  CURSOR_OPTION,
722
778
  PROFILE_OPTION,
@@ -727,7 +783,7 @@ const HELP_ROOT = group(
727
783
  command(
728
784
  "search",
729
785
  "Search the owner feed.",
730
- `${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
786
+ `${CLI_NAME} feed search <query> [--channel <name-or-uuid>] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--profile <name>] [--json]`,
731
787
  {
732
788
  options: [
733
789
  option(
@@ -736,6 +792,8 @@ const HELP_ROOT = group(
736
792
  ),
737
793
  ORDER_OPTION,
738
794
  SINCE_OPTION,
795
+ SINCE_CACHE_OPTION,
796
+ SAVE_CACHE_OPTION,
739
797
  LIMIT_OPTION,
740
798
  CURSOR_OPTION,
741
799
  PROFILE_OPTION,
@@ -746,10 +804,12 @@ const HELP_ROOT = group(
746
804
  command(
747
805
  "changes",
748
806
  "Check whether the owner feed has updates newer than a server timestamp.",
749
- `${CLI_NAME} feed changes --since <server-time> [--channel <name-or-uuid>] [--profile <name>] [--json]`,
807
+ `${CLI_NAME} feed changes (--since <server-time>|--since-cache) [--save-cache] [--channel <name-or-uuid>] [--profile <name>] [--json]`,
750
808
  {
751
809
  options: [
752
810
  SINCE_OPTION,
811
+ SINCE_CACHE_OPTION,
812
+ SAVE_CACHE_OPTION,
753
813
  option(
754
814
  "--channel <name-or-uuid>",
755
815
  "Check updates within one owned channel.",
@@ -771,7 +831,7 @@ const HELP_ROOT = group(
771
831
  command(
772
832
  "list",
773
833
  "List inbox threads.",
774
- `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
834
+ `${CLI_NAME} inbox list [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
775
835
  {
776
836
  options: [
777
837
  option(
@@ -784,6 +844,8 @@ const HELP_ROOT = group(
784
844
  ),
785
845
  ORDER_OPTION,
786
846
  SINCE_OPTION,
847
+ SINCE_CACHE_OPTION,
848
+ SAVE_CACHE_OPTION,
787
849
  LIMIT_OPTION,
788
850
  CURSOR_OPTION,
789
851
  CHANNEL_TOKEN_OPTION,
@@ -795,11 +857,13 @@ const HELP_ROOT = group(
795
857
  command(
796
858
  "show",
797
859
  "Show one thread and its recent messages.",
798
- `${CLI_NAME} inbox show <thread-id> [--order <latest|oldest>] [--since <server-time>] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
860
+ `${CLI_NAME} inbox show <thread-id> [--order <latest|oldest>] [--since <server-time>|--since-cache] [--save-cache] [--limit <n>] [--cursor <cursor>] [--channel-token <token>] [--profile <name>] [--json]`,
799
861
  {
800
862
  options: [
801
863
  ORDER_OPTION,
802
864
  SINCE_OPTION,
865
+ SINCE_CACHE_OPTION,
866
+ SAVE_CACHE_OPTION,
803
867
  LIMIT_OPTION,
804
868
  CURSOR_OPTION,
805
869
  CHANNEL_TOKEN_OPTION,
@@ -811,10 +875,12 @@ const HELP_ROOT = group(
811
875
  command(
812
876
  "changes",
813
877
  "Check whether inbox threads have updates newer than a server timestamp.",
814
- `${CLI_NAME} inbox changes --since <server-time> [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--channel-token <token>] [--profile <name>] [--json]`,
878
+ `${CLI_NAME} inbox changes (--since <server-time>|--since-cache) [--save-cache] [--status <pending|open|blocked|all>] [--mailbox <account|channel|all>] [--channel-token <token>] [--profile <name>] [--json]`,
815
879
  {
816
880
  options: [
817
881
  SINCE_OPTION,
882
+ SINCE_CACHE_OPTION,
883
+ SAVE_CACHE_OPTION,
818
884
  option(
819
885
  "--status <pending|open|blocked|all>",
820
886
  "Filter by thread status.",
@@ -836,10 +902,12 @@ const HELP_ROOT = group(
836
902
  command(
837
903
  "changes",
838
904
  "Check whether one thread has messages newer than a server timestamp.",
839
- `${CLI_NAME} inbox messages changes <thread-id> --since <server-time> [--channel-token <token>] [--profile <name>] [--json]`,
905
+ `${CLI_NAME} inbox messages changes <thread-id> (--since <server-time>|--since-cache) [--save-cache] [--channel-token <token>] [--profile <name>] [--json]`,
840
906
  {
841
907
  options: [
842
908
  SINCE_OPTION,
909
+ SINCE_CACHE_OPTION,
910
+ SAVE_CACHE_OPTION,
843
911
  CHANNEL_TOKEN_OPTION,
844
912
  PROFILE_OPTION,
845
913
  JSON_OPTION,