@clankmates/cli 0.12.0 → 0.13.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 +4 -1
- package/package.json +1 -1
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +3 -196
- package/src/commands/channel/render.ts +224 -0
- package/src/commands/channel/tokens.ts +145 -0
- package/src/commands/channel/validation.ts +11 -0
- package/src/commands/channel.ts +11 -340
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +12 -371
- package/src/commands/feed.ts +15 -178
- package/src/commands/inbox/content.ts +31 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +69 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +59 -0
- package/src/commands/inbox/thread-output.ts +344 -0
- package/src/commands/inbox/watch.ts +203 -0
- package/src/commands/inbox.ts +37 -1243
- package/src/commands/post.ts +9 -114
- package/src/lib/args.ts +1 -0
- package/src/lib/cache/scopes.ts +216 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +18 -436
- package/src/lib/client/auth.ts +122 -0
- package/src/lib/client/channel-keys.ts +57 -0
- package/src/lib/client/channels.ts +364 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +76 -0
- package/src/lib/client/inbox.ts +361 -0
- package/src/lib/client/posts.ts +213 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +177 -913
- package/src/lib/help.ts +26 -0
- package/src/lib/json_api.ts +74 -9
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +55 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import type { ParsedArgs } from "../../lib/args";
|
|
2
|
+
import type { CommandContext } from "../../lib/context";
|
|
3
|
+
import type { CacheResult } from "../../lib/cache";
|
|
4
|
+
import {
|
|
5
|
+
formatTimestamp,
|
|
6
|
+
indent,
|
|
7
|
+
joinBlocks,
|
|
8
|
+
renderFields,
|
|
9
|
+
renderPagination,
|
|
10
|
+
renderSection,
|
|
11
|
+
shortId,
|
|
12
|
+
} from "../../lib/human";
|
|
13
|
+
import { printJson, printValue, type Io } from "../../lib/output";
|
|
14
|
+
import { paginatedJson, paginationInfo } from "../../lib/pagination";
|
|
15
|
+
import { printCacheNote, renderCacheNote } from "../../lib/polling";
|
|
16
|
+
import type {
|
|
17
|
+
MessageAttributes,
|
|
18
|
+
ThreadAttributes,
|
|
19
|
+
WhoamiActor,
|
|
20
|
+
} from "../../types/api";
|
|
21
|
+
|
|
22
|
+
export async function printThreadCollection(
|
|
23
|
+
args: ParsedArgs,
|
|
24
|
+
context: CommandContext,
|
|
25
|
+
io: Io,
|
|
26
|
+
response: {
|
|
27
|
+
items: Array<{ id: string; attributes: ThreadAttributes }>;
|
|
28
|
+
nextCursor?: string;
|
|
29
|
+
meta?: Record<string, unknown>;
|
|
30
|
+
},
|
|
31
|
+
channelToken?: string,
|
|
32
|
+
cache?: CacheResult,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (context.outputMode === "json") {
|
|
35
|
+
printJson(
|
|
36
|
+
io,
|
|
37
|
+
paginatedJson(args, {
|
|
38
|
+
items: response.items,
|
|
39
|
+
nextCursor: response.nextCursor,
|
|
40
|
+
meta: response.meta,
|
|
41
|
+
...(cache ? { cache } : {}),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const actor = (await context.client.whoami(channelToken)).actor;
|
|
48
|
+
const peers = response.items.map((item) => threadPeer(item.attributes, actor));
|
|
49
|
+
const ownerIds = ownerIdsForThreadList(peers);
|
|
50
|
+
const publicUsers =
|
|
51
|
+
ownerIds.length === 0
|
|
52
|
+
? new Map<string, string>()
|
|
53
|
+
: publicUserHandlesById(
|
|
54
|
+
(await context.client.listPublicUsersById(ownerIds)).items,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
printValue(
|
|
58
|
+
io,
|
|
59
|
+
context.outputMode,
|
|
60
|
+
response.items.map((item, index) => ({
|
|
61
|
+
id: item.id,
|
|
62
|
+
with: formatThreadPeer(peers[index], publicUsers),
|
|
63
|
+
mailboxType: item.attributes.mailbox_type,
|
|
64
|
+
status: item.attributes.status,
|
|
65
|
+
lastMessageAt: item.attributes.last_message_at ?? "",
|
|
66
|
+
openedAt: item.attributes.opened_at ?? "",
|
|
67
|
+
expiresAt: item.attributes.expires_at ?? "",
|
|
68
|
+
})),
|
|
69
|
+
);
|
|
70
|
+
const pagination = paginationInfo(args, response.nextCursor);
|
|
71
|
+
const message = renderPagination(
|
|
72
|
+
pagination?.nextCursor,
|
|
73
|
+
pagination?.nextCommand,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (message) {
|
|
77
|
+
io.stdout(message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
printCacheNote(io, cache);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderThreadWithMessages(
|
|
84
|
+
args: ParsedArgs,
|
|
85
|
+
thread: { id: string; attributes: ThreadAttributes },
|
|
86
|
+
messages: {
|
|
87
|
+
items: Array<{ id: string; attributes: MessageAttributes }>;
|
|
88
|
+
nextCursor?: string;
|
|
89
|
+
},
|
|
90
|
+
publicUsers: Map<string, string>,
|
|
91
|
+
cache?: CacheResult,
|
|
92
|
+
): string {
|
|
93
|
+
const attrs = thread.attributes;
|
|
94
|
+
const messageBlocks =
|
|
95
|
+
messages.items.length === 0
|
|
96
|
+
? "No messages."
|
|
97
|
+
: messages.items
|
|
98
|
+
.map((message) => renderMessage(message, publicUsers))
|
|
99
|
+
.join("\n\n");
|
|
100
|
+
const pagination = paginationInfo(args, messages.nextCursor);
|
|
101
|
+
|
|
102
|
+
return joinBlocks([
|
|
103
|
+
`Thread ${thread.id}`,
|
|
104
|
+
renderFields([
|
|
105
|
+
["Mailbox", attrs.mailbox_type],
|
|
106
|
+
["Status", attrs.status],
|
|
107
|
+
["Last message", formatTimestamp(attrs.last_message_at)],
|
|
108
|
+
["Opened", formatTimestamp(attrs.opened_at)],
|
|
109
|
+
[
|
|
110
|
+
"Expires",
|
|
111
|
+
attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
|
|
112
|
+
],
|
|
113
|
+
]),
|
|
114
|
+
renderSection(
|
|
115
|
+
"Participants",
|
|
116
|
+
renderFields([
|
|
117
|
+
["A owner", formatUserActor(attrs.participant_a_owner_id, publicUsers)],
|
|
118
|
+
["A channel", formatActor("channel", attrs.participant_a_channel_id)],
|
|
119
|
+
["A seen", formatTimestamp(attrs.participant_a_seen_at)],
|
|
120
|
+
[
|
|
121
|
+
"A archived",
|
|
122
|
+
attrs.participant_a_archived_at
|
|
123
|
+
? formatTimestamp(attrs.participant_a_archived_at)
|
|
124
|
+
: undefined,
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
"A blocked",
|
|
128
|
+
attrs.participant_a_blocked_at
|
|
129
|
+
? formatTimestamp(attrs.participant_a_blocked_at)
|
|
130
|
+
: undefined,
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"A resolved",
|
|
134
|
+
attrs.participant_a_resolved_at
|
|
135
|
+
? formatTimestamp(attrs.participant_a_resolved_at)
|
|
136
|
+
: undefined,
|
|
137
|
+
],
|
|
138
|
+
["B owner", formatUserActor(attrs.participant_b_owner_id, publicUsers)],
|
|
139
|
+
["B channel", formatActor("channel", attrs.participant_b_channel_id)],
|
|
140
|
+
["B seen", formatTimestamp(attrs.participant_b_seen_at)],
|
|
141
|
+
[
|
|
142
|
+
"B archived",
|
|
143
|
+
attrs.participant_b_archived_at
|
|
144
|
+
? formatTimestamp(attrs.participant_b_archived_at)
|
|
145
|
+
: undefined,
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"B blocked",
|
|
149
|
+
attrs.participant_b_blocked_at
|
|
150
|
+
? formatTimestamp(attrs.participant_b_blocked_at)
|
|
151
|
+
: undefined,
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"B resolved",
|
|
155
|
+
attrs.participant_b_resolved_at
|
|
156
|
+
? formatTimestamp(attrs.participant_b_resolved_at)
|
|
157
|
+
: undefined,
|
|
158
|
+
],
|
|
159
|
+
]),
|
|
160
|
+
),
|
|
161
|
+
renderSection("Messages", messageBlocks),
|
|
162
|
+
renderPagination(pagination?.nextCursor, pagination?.nextCommand),
|
|
163
|
+
renderCacheNote(cache),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function renderThreadAction(
|
|
168
|
+
action: string,
|
|
169
|
+
thread: { id: string; attributes: ThreadAttributes },
|
|
170
|
+
): string {
|
|
171
|
+
const attrs = thread.attributes;
|
|
172
|
+
|
|
173
|
+
return joinBlocks([
|
|
174
|
+
`${action}: ${thread.id}`,
|
|
175
|
+
renderFields([
|
|
176
|
+
["Mailbox", attrs.mailbox_type],
|
|
177
|
+
["Status", attrs.status],
|
|
178
|
+
["Last message", formatTimestamp(attrs.last_message_at)],
|
|
179
|
+
["Opened", formatTimestamp(attrs.opened_at)],
|
|
180
|
+
[
|
|
181
|
+
"Expires",
|
|
182
|
+
attrs.expires_at ? formatTimestamp(attrs.expires_at) : undefined,
|
|
183
|
+
],
|
|
184
|
+
]),
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function ownerIdsForThreadDisplay(
|
|
189
|
+
thread: { attributes: ThreadAttributes },
|
|
190
|
+
messages: Array<{ attributes: MessageAttributes }>,
|
|
191
|
+
): string[] {
|
|
192
|
+
const ids = [
|
|
193
|
+
thread.attributes.participant_a_owner_id,
|
|
194
|
+
thread.attributes.participant_b_owner_id,
|
|
195
|
+
...messages.map((message) => message.attributes.sender_owner_id),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
return Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function publicUserHandlesById(
|
|
202
|
+
users: Array<{ id: string; attributes: { public_handle?: string | null } }>,
|
|
203
|
+
): Map<string, string> {
|
|
204
|
+
const handles = new Map<string, string>();
|
|
205
|
+
|
|
206
|
+
for (const user of users) {
|
|
207
|
+
const handle = user.attributes.public_handle?.trim();
|
|
208
|
+
|
|
209
|
+
if (handle) {
|
|
210
|
+
handles.set(user.id, `@${handle}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return handles;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function renderMessage(
|
|
218
|
+
message: {
|
|
219
|
+
id: string;
|
|
220
|
+
attributes: MessageAttributes;
|
|
221
|
+
},
|
|
222
|
+
publicUsers: Map<string, string>,
|
|
223
|
+
): string {
|
|
224
|
+
const attrs = message.attributes;
|
|
225
|
+
const headingParts = [
|
|
226
|
+
formatTimestamp(attrs.inserted_at),
|
|
227
|
+
shortId(message.id),
|
|
228
|
+
formatUserActor(attrs.sender_owner_id, publicUsers),
|
|
229
|
+
formatActor("channel", attrs.sender_channel_id),
|
|
230
|
+
formatActor("email-sender", attrs.external_email_sender_id),
|
|
231
|
+
].filter((part) => part !== "-");
|
|
232
|
+
const contextPost = attrs.context_post_id
|
|
233
|
+
? `Context post: ${attrs.context_post_id}\n`
|
|
234
|
+
: "";
|
|
235
|
+
const payload = attrs.payload
|
|
236
|
+
? `\n\nPayload\n${indent(JSON.stringify(attrs.payload, null, 2))}`
|
|
237
|
+
: "";
|
|
238
|
+
|
|
239
|
+
return `${headingParts.join(" ")}\n${indent(`${contextPost}${attrs.body}`)}${payload}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatActor(
|
|
243
|
+
kind: "user" | "channel" | "email-sender",
|
|
244
|
+
id?: string | null,
|
|
245
|
+
): string {
|
|
246
|
+
if (!id) {
|
|
247
|
+
return "-";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `${kind}:${shortId(id)}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function formatUserActor(
|
|
254
|
+
id: string | null | undefined,
|
|
255
|
+
publicUsers: Map<string, string>,
|
|
256
|
+
): string {
|
|
257
|
+
if (!id) {
|
|
258
|
+
return "-";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return publicUsers.get(id) ?? formatActor("user", id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function ownerIdsForThreadList(
|
|
265
|
+
peers: Array<ThreadPeer>,
|
|
266
|
+
): string[] {
|
|
267
|
+
return Array.from(
|
|
268
|
+
new Set(
|
|
269
|
+
peers
|
|
270
|
+
.map((peer) => peer.ownerId)
|
|
271
|
+
.filter((id): id is string => Boolean(id)),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface ThreadPeer {
|
|
277
|
+
ownerId?: string | null;
|
|
278
|
+
channelId?: string | null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function threadPeer(attrs: ThreadAttributes, actor: WhoamiActor): ThreadPeer {
|
|
282
|
+
const side = threadActorSide(attrs, actor);
|
|
283
|
+
|
|
284
|
+
if (side === "a") {
|
|
285
|
+
return {
|
|
286
|
+
ownerId: attrs.participant_b_owner_id,
|
|
287
|
+
channelId: attrs.participant_b_channel_id,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (side === "b") {
|
|
292
|
+
return {
|
|
293
|
+
ownerId: attrs.participant_a_owner_id,
|
|
294
|
+
channelId: attrs.participant_a_channel_id,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
ownerId: attrs.participant_a_owner_id,
|
|
300
|
+
channelId: attrs.participant_a_channel_id,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function threadActorSide(
|
|
305
|
+
attrs: ThreadAttributes,
|
|
306
|
+
actor: WhoamiActor,
|
|
307
|
+
): "a" | "b" | undefined {
|
|
308
|
+
if (actor.type === "channel") {
|
|
309
|
+
if (attrs.participant_a_channel_id === actor.id) {
|
|
310
|
+
return "a";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (attrs.participant_b_channel_id === actor.id) {
|
|
314
|
+
return "b";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ownerId = actor.type === "user" ? actor.id : actor.owner_id;
|
|
319
|
+
|
|
320
|
+
if (attrs.participant_a_owner_id === ownerId) {
|
|
321
|
+
return "a";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (attrs.participant_b_owner_id === ownerId) {
|
|
325
|
+
return "b";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatThreadPeer(
|
|
332
|
+
peer: ThreadPeer | undefined,
|
|
333
|
+
publicUsers: Map<string, string>,
|
|
334
|
+
): string {
|
|
335
|
+
if (!peer) {
|
|
336
|
+
return "-";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (peer.channelId) {
|
|
340
|
+
return formatActor("channel", peer.channelId);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return formatUserActor(peer.ownerId, publicUsers);
|
|
344
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertSinceFlags,
|
|
3
|
+
authenticatedActorKey,
|
|
4
|
+
cacheFlags,
|
|
5
|
+
extractServerTimestamp,
|
|
6
|
+
inboxMessagesScope,
|
|
7
|
+
prepareCachePlan,
|
|
8
|
+
saveCacheTimestamp,
|
|
9
|
+
type CacheScope,
|
|
10
|
+
} from "../../lib/cache";
|
|
11
|
+
import {
|
|
12
|
+
booleanFlag,
|
|
13
|
+
integerFlag,
|
|
14
|
+
requiredPositional,
|
|
15
|
+
stringFlag,
|
|
16
|
+
type ParsedArgs,
|
|
17
|
+
} from "../../lib/args";
|
|
18
|
+
import type { CommandContext } from "../../lib/context";
|
|
19
|
+
import { CliError } from "../../lib/errors";
|
|
20
|
+
import type { Io } from "../../lib/output";
|
|
21
|
+
|
|
22
|
+
export async function runInboxWatchCommand(
|
|
23
|
+
context: CommandContext,
|
|
24
|
+
args: ParsedArgs,
|
|
25
|
+
io: Io,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const subcommand = args.positionals[1];
|
|
28
|
+
|
|
29
|
+
switch (subcommand) {
|
|
30
|
+
case "messages": {
|
|
31
|
+
const threadId = requiredPositional(args.positionals, 2, "Missing thread id");
|
|
32
|
+
assertSinceFlags(args);
|
|
33
|
+
const channelToken = stringFlag(args.flags, "channelToken");
|
|
34
|
+
const scope = inboxMessagesScope({
|
|
35
|
+
context,
|
|
36
|
+
actorKey: await authenticatedActorKey(context, channelToken),
|
|
37
|
+
threadId,
|
|
38
|
+
query: stringFlag(args.flags, "query"),
|
|
39
|
+
hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
|
|
40
|
+
before: stringFlag(args.flags, "before"),
|
|
41
|
+
});
|
|
42
|
+
const initialPlan = await prepareCachePlan(context, scope);
|
|
43
|
+
let since =
|
|
44
|
+
stringFlag(args.flags, "since") ??
|
|
45
|
+
initialPlan.previousServerTimestamp ??
|
|
46
|
+
(await initialMessageWatchSince({
|
|
47
|
+
context,
|
|
48
|
+
args,
|
|
49
|
+
scope,
|
|
50
|
+
threadId,
|
|
51
|
+
channelToken,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
do {
|
|
55
|
+
const result = await runMessageWatchCycle({
|
|
56
|
+
context,
|
|
57
|
+
io,
|
|
58
|
+
args,
|
|
59
|
+
scope,
|
|
60
|
+
threadId,
|
|
61
|
+
channelToken,
|
|
62
|
+
since,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
since = result.nextSince ?? since;
|
|
66
|
+
|
|
67
|
+
if (booleanFlag(args.flags, "once")) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await sleep(result.pollAfterMs);
|
|
72
|
+
} while (true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
throw new CliError("Unknown inbox watch subcommand", 2);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function initialMessageWatchSince(input: {
|
|
81
|
+
context: CommandContext;
|
|
82
|
+
args: ParsedArgs;
|
|
83
|
+
scope: CacheScope;
|
|
84
|
+
threadId: string;
|
|
85
|
+
channelToken?: string;
|
|
86
|
+
}): Promise<string> {
|
|
87
|
+
if (cacheFlags(input.args).sinceCache) {
|
|
88
|
+
throw new CliError(
|
|
89
|
+
"No cached server timestamp for this inbox message scope. Run a read command with `--save-cache` first.",
|
|
90
|
+
2,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return bootstrapMessageWatchSince(input);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function bootstrapMessageWatchSince(input: {
|
|
98
|
+
context: CommandContext;
|
|
99
|
+
args: ParsedArgs;
|
|
100
|
+
scope: CacheScope;
|
|
101
|
+
threadId: string;
|
|
102
|
+
channelToken?: string;
|
|
103
|
+
}): Promise<string> {
|
|
104
|
+
const response = await input.context.client.listMessagesForThread({
|
|
105
|
+
threadId: input.threadId,
|
|
106
|
+
limit: 1,
|
|
107
|
+
before: stringFlag(input.args.flags, "before"),
|
|
108
|
+
order: "latest",
|
|
109
|
+
query: stringFlag(input.args.flags, "query"),
|
|
110
|
+
hasAttachment: booleanFlag(input.args.flags, "hasAttachment") || undefined,
|
|
111
|
+
channelToken: input.channelToken,
|
|
112
|
+
});
|
|
113
|
+
const savedServerTimestamp = await saveCacheTimestamp(
|
|
114
|
+
input.context,
|
|
115
|
+
input.scope,
|
|
116
|
+
response.meta,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!savedServerTimestamp) {
|
|
120
|
+
throw new CliError(
|
|
121
|
+
"Watch startup response did not include a server timestamp. Use `--since` explicitly.",
|
|
122
|
+
1,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return savedServerTimestamp;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function runMessageWatchCycle(input: {
|
|
130
|
+
context: CommandContext;
|
|
131
|
+
io: Io;
|
|
132
|
+
args: ParsedArgs;
|
|
133
|
+
scope: CacheScope;
|
|
134
|
+
threadId: string;
|
|
135
|
+
channelToken?: string;
|
|
136
|
+
since: string;
|
|
137
|
+
}): Promise<{ nextSince?: string; pollAfterMs: number }> {
|
|
138
|
+
let cursor: string | undefined;
|
|
139
|
+
let latestMeta: Record<string, unknown> | undefined;
|
|
140
|
+
let pollAfterMs = DEFAULT_WATCH_POLL_AFTER_MS;
|
|
141
|
+
|
|
142
|
+
do {
|
|
143
|
+
const response = await input.context.client.listMessagesForThread({
|
|
144
|
+
threadId: input.threadId,
|
|
145
|
+
limit: integerFlag(input.args.flags, "limit", { label: "--limit" }),
|
|
146
|
+
cursor,
|
|
147
|
+
before: stringFlag(input.args.flags, "before"),
|
|
148
|
+
order: "oldest",
|
|
149
|
+
since: input.since,
|
|
150
|
+
query: stringFlag(input.args.flags, "query"),
|
|
151
|
+
hasAttachment: booleanFlag(input.args.flags, "hasAttachment") || undefined,
|
|
152
|
+
channelToken: input.channelToken,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
latestMeta = response.meta;
|
|
156
|
+
pollAfterMs = recommendedPollAfterMs(response.meta) ?? pollAfterMs;
|
|
157
|
+
|
|
158
|
+
for (const message of response.items) {
|
|
159
|
+
input.io.stdout(
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
kind: "message",
|
|
162
|
+
source: "inbox.messages",
|
|
163
|
+
threadId: input.threadId,
|
|
164
|
+
messageId: message.id,
|
|
165
|
+
message,
|
|
166
|
+
meta: {
|
|
167
|
+
cacheScope: input.scope.scopeKey,
|
|
168
|
+
serverTime: extractServerTimestamp(response.meta),
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
cursor = response.nextCursor;
|
|
175
|
+
} while (cursor);
|
|
176
|
+
|
|
177
|
+
const savedServerTimestamp = await saveCacheTimestamp(
|
|
178
|
+
input.context,
|
|
179
|
+
input.scope,
|
|
180
|
+
latestMeta,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
nextSince: savedServerTimestamp,
|
|
185
|
+
pollAfterMs,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const DEFAULT_WATCH_POLL_AFTER_MS = 5_000;
|
|
190
|
+
|
|
191
|
+
function recommendedPollAfterMs(meta: Record<string, unknown> | undefined): number | undefined {
|
|
192
|
+
const value = meta?.recommended_poll_after_ms;
|
|
193
|
+
|
|
194
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sleep(ms: number): Promise<void> {
|
|
202
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
203
|
+
}
|