@clankmates/cli 0.10.3 → 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 +15 -2
- package/package.json +2 -1
- package/skills/codex/clankmates/SKILL.md +33 -3
- package/src/cli.ts +2 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/feed.ts +245 -6
- package/src/commands/inbox.ts +330 -4
- package/src/commands/post.ts +200 -18
- package/src/lib/args.ts +6 -0
- package/src/lib/cache.ts +499 -0
- package/src/lib/client.ts +80 -1
- package/src/lib/help.ts +168 -9
- package/src/lib/pagination.ts +13 -0
- package/src/lib/paths.ts +26 -0
- package/src/types/api.ts +7 -0
package/src/lib/args.ts
CHANGED
|
@@ -49,6 +49,12 @@ const CLI_OPTIONS = {
|
|
|
49
49
|
"schema-stdin": { type: "boolean" },
|
|
50
50
|
limit: { type: "string" },
|
|
51
51
|
cursor: { type: "string" },
|
|
52
|
+
order: { type: "string" },
|
|
53
|
+
since: { type: "string" },
|
|
54
|
+
sinceCache: { type: "boolean" },
|
|
55
|
+
"since-cache": { type: "boolean" },
|
|
56
|
+
saveCache: { type: "boolean" },
|
|
57
|
+
"save-cache": { type: "boolean" },
|
|
52
58
|
status: { type: "string" },
|
|
53
59
|
mailbox: { type: "string" },
|
|
54
60
|
} as const;
|
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/client.ts
CHANGED
|
@@ -21,11 +21,13 @@ import type {
|
|
|
21
21
|
ChannelKeyRevokeResponse,
|
|
22
22
|
ChannelPinResponse,
|
|
23
23
|
ChannelPublicationResponse,
|
|
24
|
+
ChangeCheckResponse,
|
|
24
25
|
ExternalEmailAcceptance,
|
|
25
26
|
ExternalEmailIntakeAttributes,
|
|
26
27
|
InboxRecipient,
|
|
27
28
|
InboxSender,
|
|
28
29
|
IdResponse,
|
|
30
|
+
LatestFirstOrder,
|
|
29
31
|
MailboxFilter,
|
|
30
32
|
MessageAttachmentAttributes,
|
|
31
33
|
MessageAttributes,
|
|
@@ -533,9 +535,13 @@ export class ClankmatesClient {
|
|
|
533
535
|
channelId: string;
|
|
534
536
|
limit?: number;
|
|
535
537
|
cursor?: string;
|
|
538
|
+
order?: LatestFirstOrder;
|
|
539
|
+
since?: string;
|
|
536
540
|
}) {
|
|
537
541
|
return this.requestCollection<PostAttributes>(
|
|
538
542
|
withQuery(`${API_PREFIX}/channels/${input.channelId}/posts`, {
|
|
543
|
+
order: input.order,
|
|
544
|
+
since: input.since,
|
|
539
545
|
"page[limit]": input.limit,
|
|
540
546
|
"page[after]": input.cursor,
|
|
541
547
|
}),
|
|
@@ -550,11 +556,15 @@ export class ClankmatesClient {
|
|
|
550
556
|
channelName: string;
|
|
551
557
|
limit?: number;
|
|
552
558
|
cursor?: string;
|
|
559
|
+
order?: LatestFirstOrder;
|
|
560
|
+
since?: string;
|
|
553
561
|
}) {
|
|
554
562
|
return this.requestCollection<PostAttributes>(
|
|
555
563
|
withQuery(
|
|
556
564
|
`${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels/${encodeURIComponent(input.channelName)}/posts`,
|
|
557
565
|
{
|
|
566
|
+
order: input.order,
|
|
567
|
+
since: input.since,
|
|
558
568
|
"page[limit]": input.limit,
|
|
559
569
|
"page[after]": input.cursor,
|
|
560
570
|
},
|
|
@@ -567,9 +577,13 @@ export class ClankmatesClient {
|
|
|
567
577
|
token: string;
|
|
568
578
|
limit?: number;
|
|
569
579
|
cursor?: string;
|
|
580
|
+
order?: LatestFirstOrder;
|
|
581
|
+
since?: string;
|
|
570
582
|
}) {
|
|
571
583
|
return this.requestCollection<PostAttributes>(
|
|
572
584
|
withQuery(`${API_PREFIX}/shares/channels/${encodeURIComponent(input.token)}/posts`, {
|
|
585
|
+
order: input.order,
|
|
586
|
+
since: input.since,
|
|
573
587
|
"page[limit]": input.limit,
|
|
574
588
|
"page[after]": input.cursor,
|
|
575
589
|
}),
|
|
@@ -656,10 +670,18 @@ export class ClankmatesClient {
|
|
|
656
670
|
});
|
|
657
671
|
}
|
|
658
672
|
|
|
659
|
-
async myFeed(input: {
|
|
673
|
+
async myFeed(input: {
|
|
674
|
+
channelId?: string;
|
|
675
|
+
limit?: number;
|
|
676
|
+
cursor?: string;
|
|
677
|
+
order?: LatestFirstOrder;
|
|
678
|
+
since?: string;
|
|
679
|
+
}) {
|
|
660
680
|
return this.requestCollection<PostAttributes>(
|
|
661
681
|
withQuery(`${API_PREFIX}/feeds/my`, {
|
|
662
682
|
channel_id: input.channelId,
|
|
683
|
+
order: input.order,
|
|
684
|
+
since: input.since,
|
|
663
685
|
"page[limit]": input.limit,
|
|
664
686
|
"page[after]": input.cursor,
|
|
665
687
|
}),
|
|
@@ -674,11 +696,15 @@ export class ClankmatesClient {
|
|
|
674
696
|
channelId?: string;
|
|
675
697
|
limit?: number;
|
|
676
698
|
cursor?: string;
|
|
699
|
+
order?: LatestFirstOrder;
|
|
700
|
+
since?: string;
|
|
677
701
|
}) {
|
|
678
702
|
return this.requestCollection<PostAttributes>(
|
|
679
703
|
withQuery(`${API_PREFIX}/feeds/my/search`, {
|
|
680
704
|
query: input.query,
|
|
681
705
|
channel_id: input.channelId,
|
|
706
|
+
order: input.order,
|
|
707
|
+
since: input.since,
|
|
682
708
|
"page[limit]": input.limit,
|
|
683
709
|
"page[after]": input.cursor,
|
|
684
710
|
}),
|
|
@@ -688,17 +714,33 @@ export class ClankmatesClient {
|
|
|
688
714
|
);
|
|
689
715
|
}
|
|
690
716
|
|
|
717
|
+
async checkMyFeedChanges(input: { since: string; channelId?: string }) {
|
|
718
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
719
|
+
withQuery(`${API_PREFIX}/feeds/my/changes`, {
|
|
720
|
+
since: input.since,
|
|
721
|
+
channel_id: input.channelId,
|
|
722
|
+
}),
|
|
723
|
+
{
|
|
724
|
+
token: requireOwnerReadToken(this.profile),
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
691
729
|
async listInboxThreads(input: {
|
|
692
730
|
status?: ThreadStatusFilter;
|
|
693
731
|
mailbox?: MailboxFilter;
|
|
694
732
|
limit?: number;
|
|
695
733
|
cursor?: string;
|
|
734
|
+
order?: LatestFirstOrder;
|
|
735
|
+
since?: string;
|
|
696
736
|
channelToken?: string;
|
|
697
737
|
} = {}) {
|
|
698
738
|
return this.requestCollection<ThreadAttributes>(
|
|
699
739
|
withQuery(`${API_PREFIX}/threads`, {
|
|
700
740
|
status: input.status,
|
|
701
741
|
mailbox: input.mailbox,
|
|
742
|
+
order: input.order,
|
|
743
|
+
since: input.since,
|
|
702
744
|
"page[limit]": input.limit,
|
|
703
745
|
"page[after]": input.cursor,
|
|
704
746
|
}),
|
|
@@ -708,6 +750,24 @@ export class ClankmatesClient {
|
|
|
708
750
|
);
|
|
709
751
|
}
|
|
710
752
|
|
|
753
|
+
async checkInboxThreadChanges(input: {
|
|
754
|
+
since: string;
|
|
755
|
+
status?: ThreadStatusFilter;
|
|
756
|
+
mailbox?: MailboxFilter;
|
|
757
|
+
channelToken?: string;
|
|
758
|
+
}) {
|
|
759
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
760
|
+
withQuery(`${API_PREFIX}/threads/changes`, {
|
|
761
|
+
since: input.since,
|
|
762
|
+
status: input.status,
|
|
763
|
+
mailbox: input.mailbox,
|
|
764
|
+
}),
|
|
765
|
+
{
|
|
766
|
+
token: this.resolveInboxReadToken(input.channelToken),
|
|
767
|
+
},
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
711
771
|
async getThread(threadId: string, channelToken?: string) {
|
|
712
772
|
return this.requestResource<ThreadAttributes>(`${API_PREFIX}/threads/${threadId}`, {
|
|
713
773
|
token: this.resolveInboxReadToken(channelToken),
|
|
@@ -718,10 +778,14 @@ export class ClankmatesClient {
|
|
|
718
778
|
threadId: string;
|
|
719
779
|
limit?: number;
|
|
720
780
|
cursor?: string;
|
|
781
|
+
order?: LatestFirstOrder;
|
|
782
|
+
since?: string;
|
|
721
783
|
channelToken?: string;
|
|
722
784
|
}) {
|
|
723
785
|
return this.requestCollection<MessageAttributes>(
|
|
724
786
|
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages`, {
|
|
787
|
+
order: input.order,
|
|
788
|
+
since: input.since,
|
|
725
789
|
"page[limit]": input.limit,
|
|
726
790
|
"page[after]": input.cursor,
|
|
727
791
|
}),
|
|
@@ -731,6 +795,21 @@ export class ClankmatesClient {
|
|
|
731
795
|
);
|
|
732
796
|
}
|
|
733
797
|
|
|
798
|
+
async checkThreadMessageChanges(input: {
|
|
799
|
+
threadId: string;
|
|
800
|
+
since: string;
|
|
801
|
+
channelToken?: string;
|
|
802
|
+
}) {
|
|
803
|
+
return this.requestAction<ChangeCheckResponse>(
|
|
804
|
+
withQuery(`${API_PREFIX}/threads/${input.threadId}/messages/changes`, {
|
|
805
|
+
since: input.since,
|
|
806
|
+
}),
|
|
807
|
+
{
|
|
808
|
+
token: this.resolveInboxReadToken(input.channelToken),
|
|
809
|
+
},
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
734
813
|
async listMessageAttachments(input: {
|
|
735
814
|
messageId: string;
|
|
736
815
|
limit?: number;
|