@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.
- package/README.md +14 -3
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/feed.ts +192 -11
- package/src/commands/inbox.ts +290 -16
- package/src/commands/post.ts +195 -20
- package/src/lib/args.ts +11 -0
- package/src/lib/cache.ts +553 -0
- package/src/lib/client.ts +40 -1
- package/src/lib/help.ts +109 -10
- package/src/lib/pagination.ts +16 -0
- package/src/lib/paths.ts +26 -0
- package/src/types/api.ts +1 -0
package/src/lib/cache.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
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
|
+
before?: string;
|
|
340
|
+
}): CacheScope {
|
|
341
|
+
const channel = input.channelId ?? "all";
|
|
342
|
+
const before = input.before ?? "default";
|
|
343
|
+
return scoped({
|
|
344
|
+
context: input.context,
|
|
345
|
+
actorKey: input.actorKey,
|
|
346
|
+
resource: "feed:my",
|
|
347
|
+
parts: ["feed", "my", channel, before],
|
|
348
|
+
params: { channel, before },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function feedSearchScope(input: {
|
|
353
|
+
context: CommandContext;
|
|
354
|
+
actorKey: string;
|
|
355
|
+
query: string;
|
|
356
|
+
channelId?: string;
|
|
357
|
+
before?: string;
|
|
358
|
+
}): CacheScope {
|
|
359
|
+
const channel = input.channelId ?? "all";
|
|
360
|
+
const before = input.before ?? "default";
|
|
361
|
+
return scoped({
|
|
362
|
+
context: input.context,
|
|
363
|
+
actorKey: input.actorKey,
|
|
364
|
+
resource: "feed:search",
|
|
365
|
+
parts: ["feed", "search", input.query, channel, before],
|
|
366
|
+
params: { query: input.query, channel, before },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function inboxThreadsScope(input: {
|
|
371
|
+
context: CommandContext;
|
|
372
|
+
actorKey: string;
|
|
373
|
+
status?: string;
|
|
374
|
+
mailbox?: string;
|
|
375
|
+
participant?: string;
|
|
376
|
+
participantScope?: string;
|
|
377
|
+
query?: string;
|
|
378
|
+
hasAttachment?: boolean;
|
|
379
|
+
before?: string;
|
|
380
|
+
}): CacheScope {
|
|
381
|
+
const status = input.status ?? "default";
|
|
382
|
+
const mailbox = input.mailbox ?? "default";
|
|
383
|
+
const participant = input.participant ?? "default";
|
|
384
|
+
const participantScope = input.participantScope ?? "default";
|
|
385
|
+
const query = input.query ?? "default";
|
|
386
|
+
const hasAttachment = input.hasAttachment ?? false;
|
|
387
|
+
const before = input.before ?? "default";
|
|
388
|
+
return scoped({
|
|
389
|
+
context: input.context,
|
|
390
|
+
actorKey: input.actorKey,
|
|
391
|
+
resource: "inbox:threads",
|
|
392
|
+
parts: [
|
|
393
|
+
"inbox",
|
|
394
|
+
"threads",
|
|
395
|
+
status,
|
|
396
|
+
mailbox,
|
|
397
|
+
participant,
|
|
398
|
+
participantScope,
|
|
399
|
+
query,
|
|
400
|
+
String(hasAttachment),
|
|
401
|
+
before,
|
|
402
|
+
input.actorKey,
|
|
403
|
+
],
|
|
404
|
+
params: {
|
|
405
|
+
status,
|
|
406
|
+
mailbox,
|
|
407
|
+
participant,
|
|
408
|
+
participantScope,
|
|
409
|
+
query,
|
|
410
|
+
hasAttachment,
|
|
411
|
+
before,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function inboxMessagesScope(input: {
|
|
417
|
+
context: CommandContext;
|
|
418
|
+
actorKey: string;
|
|
419
|
+
threadId: string;
|
|
420
|
+
query?: string;
|
|
421
|
+
hasAttachment?: boolean;
|
|
422
|
+
before?: string;
|
|
423
|
+
}): CacheScope {
|
|
424
|
+
const query = input.query ?? "default";
|
|
425
|
+
const hasAttachment = input.hasAttachment ?? false;
|
|
426
|
+
const before = input.before ?? "default";
|
|
427
|
+
return scoped({
|
|
428
|
+
context: input.context,
|
|
429
|
+
actorKey: input.actorKey,
|
|
430
|
+
resource: "inbox:messages",
|
|
431
|
+
parts: [
|
|
432
|
+
"inbox",
|
|
433
|
+
"messages",
|
|
434
|
+
input.threadId,
|
|
435
|
+
query,
|
|
436
|
+
String(hasAttachment),
|
|
437
|
+
before,
|
|
438
|
+
input.actorKey,
|
|
439
|
+
],
|
|
440
|
+
params: { threadId: input.threadId, query, hasAttachment, before },
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function ownedPostsScope(input: {
|
|
445
|
+
context: CommandContext;
|
|
446
|
+
actorKey: string;
|
|
447
|
+
channelId: string;
|
|
448
|
+
before?: string;
|
|
449
|
+
}): CacheScope {
|
|
450
|
+
const before = input.before ?? "default";
|
|
451
|
+
return scoped({
|
|
452
|
+
context: input.context,
|
|
453
|
+
actorKey: input.actorKey,
|
|
454
|
+
resource: "posts:owned",
|
|
455
|
+
parts: ["posts", "owned", input.channelId, before],
|
|
456
|
+
params: { channelId: input.channelId, before },
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function publicPostsScope(input: {
|
|
461
|
+
context: CommandContext;
|
|
462
|
+
publicHandle: string;
|
|
463
|
+
channelName: string;
|
|
464
|
+
before?: string;
|
|
465
|
+
}): CacheScope {
|
|
466
|
+
const before = input.before ?? "default";
|
|
467
|
+
return scoped({
|
|
468
|
+
context: input.context,
|
|
469
|
+
actorKey: PUBLIC_ACTOR_KEY,
|
|
470
|
+
resource: "posts:public",
|
|
471
|
+
parts: ["posts", "public", input.publicHandle, input.channelName, before],
|
|
472
|
+
params: {
|
|
473
|
+
publicHandle: input.publicHandle,
|
|
474
|
+
channelName: input.channelName,
|
|
475
|
+
before,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function sharedPostsScope(input: {
|
|
481
|
+
context: CommandContext;
|
|
482
|
+
shareToken: string;
|
|
483
|
+
before?: string;
|
|
484
|
+
}): CacheScope {
|
|
485
|
+
const shareTokenHash = hashValue(input.shareToken);
|
|
486
|
+
const before = input.before ?? "default";
|
|
487
|
+
return scoped({
|
|
488
|
+
context: input.context,
|
|
489
|
+
actorKey: SHARED_ACTOR_KEY,
|
|
490
|
+
resource: "posts:shared",
|
|
491
|
+
parts: ["posts", "shared", shareTokenHash, before],
|
|
492
|
+
params: { shareTokenHash, before },
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function hashValue(value: string): string {
|
|
497
|
+
return createHash("sha256").update(value).digest("hex");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function actorKey(actor: WhoamiActor): string {
|
|
501
|
+
if (actor.type === "user") {
|
|
502
|
+
return `user:${actor.id}:${actor.scope ?? "master"}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return `channel:${actor.id}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function scoped(input: {
|
|
509
|
+
context: CommandContext;
|
|
510
|
+
actorKey: string;
|
|
511
|
+
resource: string;
|
|
512
|
+
parts: string[];
|
|
513
|
+
params: Record<string, unknown>;
|
|
514
|
+
}): CacheScope {
|
|
515
|
+
const prefix = [
|
|
516
|
+
"v1",
|
|
517
|
+
normalizePart(input.context.profile.baseUrl),
|
|
518
|
+
normalizePart(input.context.profileName),
|
|
519
|
+
normalizePart(input.actorKey),
|
|
520
|
+
];
|
|
521
|
+
const scopeKey = [...prefix, ...input.parts.map(normalizePart)].join(":");
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
scopeKey,
|
|
525
|
+
resource: input.resource,
|
|
526
|
+
params: input.params,
|
|
527
|
+
actorKey: input.actorKey,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function normalizePart(value: string): string {
|
|
532
|
+
return encodeURIComponent(value);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function stableJson(value: Record<string, unknown>): string {
|
|
536
|
+
return JSON.stringify(sortObject(value));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function sortObject(value: unknown): unknown {
|
|
540
|
+
if (Array.isArray(value)) {
|
|
541
|
+
return value.map(sortObject);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (typeof value !== "object" || value === null) {
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return Object.fromEntries(
|
|
549
|
+
Object.entries(value as Record<string, unknown>)
|
|
550
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
551
|
+
.map(([key, entry]) => [key, sortObject(entry)]),
|
|
552
|
+
);
|
|
553
|
+
}
|
package/src/lib/client.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
MailboxFilter,
|
|
32
32
|
MessageAttachmentAttributes,
|
|
33
33
|
MessageAttributes,
|
|
34
|
+
ParticipantScope,
|
|
34
35
|
PostAttributes,
|
|
35
36
|
ProfileConfig,
|
|
36
37
|
ShareTokenResponse,
|
|
@@ -535,6 +536,7 @@ export class ClankmatesClient {
|
|
|
535
536
|
channelId: string;
|
|
536
537
|
limit?: number;
|
|
537
538
|
cursor?: string;
|
|
539
|
+
before?: string;
|
|
538
540
|
order?: LatestFirstOrder;
|
|
539
541
|
since?: string;
|
|
540
542
|
}) {
|
|
@@ -542,6 +544,7 @@ export class ClankmatesClient {
|
|
|
542
544
|
withQuery(`${API_PREFIX}/channels/${input.channelId}/posts`, {
|
|
543
545
|
order: input.order,
|
|
544
546
|
since: input.since,
|
|
547
|
+
before: input.before,
|
|
545
548
|
"page[limit]": input.limit,
|
|
546
549
|
"page[after]": input.cursor,
|
|
547
550
|
}),
|
|
@@ -556,6 +559,7 @@ export class ClankmatesClient {
|
|
|
556
559
|
channelName: string;
|
|
557
560
|
limit?: number;
|
|
558
561
|
cursor?: string;
|
|
562
|
+
before?: string;
|
|
559
563
|
order?: LatestFirstOrder;
|
|
560
564
|
since?: string;
|
|
561
565
|
}) {
|
|
@@ -565,6 +569,7 @@ export class ClankmatesClient {
|
|
|
565
569
|
{
|
|
566
570
|
order: input.order,
|
|
567
571
|
since: input.since,
|
|
572
|
+
before: input.before,
|
|
568
573
|
"page[limit]": input.limit,
|
|
569
574
|
"page[after]": input.cursor,
|
|
570
575
|
},
|
|
@@ -577,6 +582,7 @@ export class ClankmatesClient {
|
|
|
577
582
|
token: string;
|
|
578
583
|
limit?: number;
|
|
579
584
|
cursor?: string;
|
|
585
|
+
before?: string;
|
|
580
586
|
order?: LatestFirstOrder;
|
|
581
587
|
since?: string;
|
|
582
588
|
}) {
|
|
@@ -584,6 +590,7 @@ export class ClankmatesClient {
|
|
|
584
590
|
withQuery(`${API_PREFIX}/shares/channels/${encodeURIComponent(input.token)}/posts`, {
|
|
585
591
|
order: input.order,
|
|
586
592
|
since: input.since,
|
|
593
|
+
before: input.before,
|
|
587
594
|
"page[limit]": input.limit,
|
|
588
595
|
"page[after]": input.cursor,
|
|
589
596
|
}),
|
|
@@ -674,6 +681,7 @@ export class ClankmatesClient {
|
|
|
674
681
|
channelId?: string;
|
|
675
682
|
limit?: number;
|
|
676
683
|
cursor?: string;
|
|
684
|
+
before?: string;
|
|
677
685
|
order?: LatestFirstOrder;
|
|
678
686
|
since?: string;
|
|
679
687
|
}) {
|
|
@@ -682,6 +690,7 @@ export class ClankmatesClient {
|
|
|
682
690
|
channel_id: input.channelId,
|
|
683
691
|
order: input.order,
|
|
684
692
|
since: input.since,
|
|
693
|
+
before: input.before,
|
|
685
694
|
"page[limit]": input.limit,
|
|
686
695
|
"page[after]": input.cursor,
|
|
687
696
|
}),
|
|
@@ -696,6 +705,7 @@ export class ClankmatesClient {
|
|
|
696
705
|
channelId?: string;
|
|
697
706
|
limit?: number;
|
|
698
707
|
cursor?: string;
|
|
708
|
+
before?: string;
|
|
699
709
|
order?: LatestFirstOrder;
|
|
700
710
|
since?: string;
|
|
701
711
|
}) {
|
|
@@ -705,6 +715,7 @@ export class ClankmatesClient {
|
|
|
705
715
|
channel_id: input.channelId,
|
|
706
716
|
order: input.order,
|
|
707
717
|
since: input.since,
|
|
718
|
+
before: input.before,
|
|
708
719
|
"page[limit]": input.limit,
|
|
709
720
|
"page[after]": input.cursor,
|
|
710
721
|
}),
|
|
@@ -731,8 +742,13 @@ export class ClankmatesClient {
|
|
|
731
742
|
mailbox?: MailboxFilter;
|
|
732
743
|
limit?: number;
|
|
733
744
|
cursor?: string;
|
|
745
|
+
before?: string;
|
|
734
746
|
order?: LatestFirstOrder;
|
|
735
747
|
since?: string;
|
|
748
|
+
participant?: string;
|
|
749
|
+
participantScope?: ParticipantScope;
|
|
750
|
+
query?: string;
|
|
751
|
+
hasAttachment?: boolean;
|
|
736
752
|
channelToken?: string;
|
|
737
753
|
} = {}) {
|
|
738
754
|
return this.requestCollection<ThreadAttributes>(
|
|
@@ -741,6 +757,11 @@ export class ClankmatesClient {
|
|
|
741
757
|
mailbox: input.mailbox,
|
|
742
758
|
order: input.order,
|
|
743
759
|
since: input.since,
|
|
760
|
+
before: input.before,
|
|
761
|
+
participant: input.participant,
|
|
762
|
+
participant_scope: input.participantScope,
|
|
763
|
+
query: input.query,
|
|
764
|
+
has_attachment: input.hasAttachment,
|
|
744
765
|
"page[limit]": input.limit,
|
|
745
766
|
"page[after]": input.cursor,
|
|
746
767
|
}),
|
|
@@ -754,6 +775,10 @@ export class ClankmatesClient {
|
|
|
754
775
|
since: string;
|
|
755
776
|
status?: ThreadStatusFilter;
|
|
756
777
|
mailbox?: MailboxFilter;
|
|
778
|
+
participant?: string;
|
|
779
|
+
participantScope?: ParticipantScope;
|
|
780
|
+
query?: string;
|
|
781
|
+
hasAttachment?: boolean;
|
|
757
782
|
channelToken?: string;
|
|
758
783
|
}) {
|
|
759
784
|
return this.requestAction<ChangeCheckResponse>(
|
|
@@ -761,6 +786,10 @@ export class ClankmatesClient {
|
|
|
761
786
|
since: input.since,
|
|
762
787
|
status: input.status,
|
|
763
788
|
mailbox: input.mailbox,
|
|
789
|
+
participant: input.participant,
|
|
790
|
+
participant_scope: input.participantScope,
|
|
791
|
+
query: input.query,
|
|
792
|
+
has_attachment: input.hasAttachment,
|
|
764
793
|
}),
|
|
765
794
|
{
|
|
766
795
|
token: this.resolveInboxReadToken(input.channelToken),
|
|
@@ -778,14 +807,20 @@ export class ClankmatesClient {
|
|
|
778
807
|
threadId: string;
|
|
779
808
|
limit?: number;
|
|
780
809
|
cursor?: string;
|
|
810
|
+
before?: string;
|
|
781
811
|
order?: LatestFirstOrder;
|
|
782
812
|
since?: string;
|
|
813
|
+
query?: string;
|
|
814
|
+
hasAttachment?: boolean;
|
|
783
815
|
channelToken?: string;
|
|
784
816
|
}) {
|
|
785
817
|
return this.requestCollection<MessageAttributes>(
|
|
786
818
|
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages`, {
|
|
787
819
|
order: input.order,
|
|
788
820
|
since: input.since,
|
|
821
|
+
before: input.before,
|
|
822
|
+
query: input.query,
|
|
823
|
+
has_attachment: input.hasAttachment,
|
|
789
824
|
"page[limit]": input.limit,
|
|
790
825
|
"page[after]": input.cursor,
|
|
791
826
|
}),
|
|
@@ -798,11 +833,15 @@ export class ClankmatesClient {
|
|
|
798
833
|
async checkThreadMessageChanges(input: {
|
|
799
834
|
threadId: string;
|
|
800
835
|
since: string;
|
|
836
|
+
query?: string;
|
|
837
|
+
hasAttachment?: boolean;
|
|
801
838
|
channelToken?: string;
|
|
802
839
|
}) {
|
|
803
840
|
return this.requestAction<ChangeCheckResponse>(
|
|
804
841
|
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages/changes`, {
|
|
805
842
|
since: input.since,
|
|
843
|
+
query: input.query,
|
|
844
|
+
has_attachment: input.hasAttachment,
|
|
806
845
|
}),
|
|
807
846
|
{
|
|
808
847
|
token: this.resolveInboxReadToken(input.channelToken),
|
|
@@ -1132,7 +1171,7 @@ function looksLikeUuid(value: string): boolean {
|
|
|
1132
1171
|
|
|
1133
1172
|
function withQuery(
|
|
1134
1173
|
path: string,
|
|
1135
|
-
params: Record<string, string | number | undefined>,
|
|
1174
|
+
params: Record<string, string | number | boolean | undefined>,
|
|
1136
1175
|
): string {
|
|
1137
1176
|
const search = new URLSearchParams();
|
|
1138
1177
|
|