@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.
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/feed.ts +188 -11
- package/src/commands/inbox.ts +246 -16
- package/src/commands/post.ts +182 -20
- package/src/lib/args.ts +4 -0
- package/src/lib/cache.ts +499 -0
- package/src/lib/help.ts +78 -10
- package/src/lib/pagination.ts +11 -0
- package/src/lib/paths.ts +26 -0
package/src/lib/cache.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|