@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.
Files changed (46) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/src/commands/auth/access-keys.ts +206 -0
  4. package/src/commands/auth.ts +3 -196
  5. package/src/commands/channel/render.ts +224 -0
  6. package/src/commands/channel/tokens.ts +145 -0
  7. package/src/commands/channel/validation.ts +11 -0
  8. package/src/commands/channel.ts +11 -340
  9. package/src/commands/doctor/checks.ts +123 -0
  10. package/src/commands/doctor/render.ts +140 -0
  11. package/src/commands/doctor/suggestions.ts +42 -0
  12. package/src/commands/doctor/types.ts +75 -0
  13. package/src/commands/doctor.ts +12 -371
  14. package/src/commands/feed.ts +15 -178
  15. package/src/commands/inbox/content.ts +31 -0
  16. package/src/commands/inbox/filters.ts +70 -0
  17. package/src/commands/inbox/messages.ts +69 -0
  18. package/src/commands/inbox/participants.ts +152 -0
  19. package/src/commands/inbox/render.ts +13 -0
  20. package/src/commands/inbox/resource-output.ts +217 -0
  21. package/src/commands/inbox/schema.ts +185 -0
  22. package/src/commands/inbox/screening.ts +76 -0
  23. package/src/commands/inbox/sync-scopes.ts +59 -0
  24. package/src/commands/inbox/thread-output.ts +344 -0
  25. package/src/commands/inbox/watch.ts +203 -0
  26. package/src/commands/inbox.ts +37 -1243
  27. package/src/commands/post.ts +9 -114
  28. package/src/lib/args.ts +1 -0
  29. package/src/lib/cache/scopes.ts +216 -0
  30. package/src/lib/cache/store.ts +195 -0
  31. package/src/lib/cache/types.ts +31 -0
  32. package/src/lib/cache.ts +18 -436
  33. package/src/lib/client/auth.ts +122 -0
  34. package/src/lib/client/channel-keys.ts +57 -0
  35. package/src/lib/client/channels.ts +364 -0
  36. package/src/lib/client/core.ts +133 -0
  37. package/src/lib/client/feed.ts +76 -0
  38. package/src/lib/client/inbox.ts +361 -0
  39. package/src/lib/client/posts.ts +213 -0
  40. package/src/lib/client/raw-api.ts +33 -0
  41. package/src/lib/client/users.ts +88 -0
  42. package/src/lib/client.ts +177 -913
  43. package/src/lib/help.ts +26 -0
  44. package/src/lib/json_api.ts +74 -9
  45. package/src/lib/polling.ts +146 -0
  46. package/src/lib/post-output.ts +55 -0
@@ -3,14 +3,10 @@ import {
3
3
  assertSinceFlags,
4
4
  cacheFlags,
5
5
  cacheResult,
6
- changeResponseMeta,
7
6
  feedMyScope,
8
7
  feedSearchScope,
9
- prepareCachePlan,
10
- saveCacheTimestamp,
11
- type CachePlan,
12
- type CacheResult,
13
8
  type CacheScope,
9
+ changeResponseMeta,
14
10
  } from "../lib/cache";
15
11
  import {
16
12
  channelFlag,
@@ -21,10 +17,16 @@ import {
21
17
  } from "../lib/args";
22
18
  import { createCommandContext, type CommandContext } from "../lib/context";
23
19
  import { CliError } from "../lib/errors";
24
- import { formatTimestamp, renderFields, renderPagination } from "../lib/human";
25
- import { printJson, printValue, type Io } from "../lib/output";
26
- import { paginatedJson, paginationInfo } from "../lib/pagination";
27
- import type { ChangeCheckResponse, LatestFirstOrder, PostAttributes } from "../types/api";
20
+ import type { Io } from "../lib/output";
21
+ import {
22
+ maybePrepareCachePlan,
23
+ maybeSaveCacheTimestamp,
24
+ parseLatestFirstOrder,
25
+ printChangeCheckResponse,
26
+ requiredSince,
27
+ resolvedSince,
28
+ } from "../lib/polling";
29
+ import { printPostCollection } from "../lib/post-output";
28
30
 
29
31
  export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
30
32
  const subcommand = args.positionals[0];
@@ -52,9 +54,9 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
52
54
  response.nextCursor === undefined,
53
55
  );
54
56
 
55
- printFeedResponse(
57
+ printPostCollection(
56
58
  args,
57
- context,
59
+ context.outputMode,
58
60
  io,
59
61
  response,
60
62
  cacheResult(cachePlan, savedServerTimestamp),
@@ -90,9 +92,9 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
90
92
  response.nextCursor === undefined,
91
93
  );
92
94
 
93
- printFeedResponse(
95
+ printPostCollection(
94
96
  args,
95
- context,
97
+ context.outputMode,
96
98
  io,
97
99
  response,
98
100
  cacheResult(cachePlan, savedServerTimestamp),
@@ -140,135 +142,6 @@ async function resolveChannelId(
140
142
  return channel ? context.client.resolveChannelId(channel) : undefined;
141
143
  }
142
144
 
143
- function printFeedResponse(
144
- args: ParsedArgs,
145
- context: CommandContext,
146
- io: Io,
147
- response: {
148
- items: Array<{ id: string; attributes: PostAttributes }>;
149
- nextCursor?: string;
150
- meta?: Record<string, unknown>;
151
- },
152
- cache?: CacheResult,
153
- ): void {
154
- if (context.outputMode === "json") {
155
- printJson(
156
- io,
157
- paginatedJson(args, {
158
- items: response.items,
159
- nextCursor: response.nextCursor,
160
- meta: response.meta,
161
- ...(cache ? { cache } : {}),
162
- }),
163
- );
164
- return;
165
- }
166
-
167
- const rows = response.items.map((item) => ({
168
- id: item.id,
169
- source: item.attributes.source,
170
- date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
171
- body: item.attributes.body,
172
- }));
173
- printValue(io, context.outputMode, rows);
174
-
175
- const pagination = paginationInfo(args, response.nextCursor);
176
- const message = renderPagination(
177
- pagination?.nextCursor,
178
- pagination?.nextCommand,
179
- );
180
-
181
- if (message) {
182
- io.stdout(message);
183
- }
184
-
185
- printCacheNote(io, cache);
186
- }
187
-
188
- function requiredSince(args: ParsedArgs, cachePlan?: CachePlan): string {
189
- const since = stringFlag(args.flags, "since");
190
-
191
- if (since) {
192
- return since;
193
- }
194
-
195
- if (cachePlan?.previousServerTimestamp) {
196
- return cachePlan.previousServerTimestamp;
197
- }
198
-
199
- if (cacheFlags(args).sinceCache) {
200
- throw new CliError(
201
- "No cached server timestamp for this feed scope. Run a read command with `--save-cache` first.",
202
- 2,
203
- );
204
- }
205
-
206
- if (!since) {
207
- throw new CliError("Missing `--since`", 2);
208
- }
209
-
210
- return since;
211
- }
212
-
213
- function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
214
- if (!value) {
215
- return undefined;
216
- }
217
-
218
- if (value === "latest" || value === "oldest") {
219
- return value;
220
- }
221
-
222
- throw new CliError("--order must be one of: latest, oldest", 2);
223
- }
224
-
225
- function printChangeCheckResponse(
226
- context: CommandContext,
227
- io: Io,
228
- response: ChangeCheckResponse,
229
- cache?: CacheResult,
230
- ): void {
231
- printValue(
232
- io,
233
- context.outputMode,
234
- context.outputMode === "json"
235
- ? { ...response, ...(cache ? { cache } : {}) }
236
- : renderFields([
237
- ["Has updates", response.has_updates ? "yes" : "no"],
238
- ["Server time", formatTimestamp(response.server_time)],
239
- [
240
- "Recommended poll",
241
- response.recommended_poll_after_ms === undefined
242
- ? undefined
243
- : `${response.recommended_poll_after_ms}ms`,
244
- ],
245
- ...cacheFields(cache),
246
- ]),
247
- );
248
- }
249
-
250
- async function maybePrepareCachePlan(
251
- args: ParsedArgs,
252
- context: CommandContext,
253
- scope: CacheScope | undefined,
254
- ): Promise<CachePlan | undefined> {
255
- return cacheFlags(args).sinceCache && scope
256
- ? prepareCachePlan(context, scope)
257
- : undefined;
258
- }
259
-
260
- async function maybeSaveCacheTimestamp(
261
- args: ParsedArgs,
262
- context: CommandContext,
263
- scope: CacheScope | undefined,
264
- meta: Record<string, unknown> | undefined,
265
- shouldSave: boolean,
266
- ): Promise<string | undefined> {
267
- return cacheFlags(args).saveCache && scope && shouldSave
268
- ? saveCacheTimestamp(context, scope, meta)
269
- : undefined;
270
- }
271
-
272
145
  async function maybeFeedMyScope(
273
146
  args: ParsedArgs,
274
147
  context: CommandContext,
@@ -304,39 +177,3 @@ async function maybeFeedSearchScope(
304
177
  before: stringFlag(args.flags, "before"),
305
178
  });
306
179
  }
307
-
308
- function resolvedSince(
309
- args: ParsedArgs,
310
- cachePlan: CachePlan | undefined,
311
- ): string | undefined {
312
- return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
313
- }
314
-
315
- function printCacheNote(io: Io, cache: CacheResult | undefined): void {
316
- if (!cache) {
317
- return;
318
- }
319
-
320
- const details = [
321
- cache.previousServerTimestamp
322
- ? `used ${cache.previousServerTimestamp}`
323
- : cache.hit
324
- ? "used cache"
325
- : "no cached timestamp",
326
- cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
327
- ].filter(Boolean);
328
-
329
- io.stdout(`Cache: ${details.join("; ")}.`);
330
- }
331
-
332
- function cacheFields(cache: CacheResult | undefined): Array<[string, string | undefined]> {
333
- if (!cache) {
334
- return [];
335
- }
336
-
337
- return [
338
- ["Cache scope", cache.scopeKey],
339
- ["Cached timestamp", cache.previousServerTimestamp],
340
- ["Saved timestamp", cache.savedServerTimestamp],
341
- ];
342
- }
@@ -0,0 +1,31 @@
1
+ import { booleanFlag, type ParsedArgs } from "../../lib/args";
2
+ import { resolveBodyInput } from "../../lib/body-input";
3
+ import { CliError } from "../../lib/errors";
4
+ import { resolveJsonInput } from "../../lib/json-input";
5
+
6
+ export async function resolveMessageContent(args: ParsedArgs): Promise<{
7
+ body?: string;
8
+ payload?: Record<string, unknown>;
9
+ }> {
10
+ if (booleanFlag(args.flags, "stdin") && booleanFlag(args.flags, "payloadStdin")) {
11
+ throw new CliError("Use only one of `--stdin` or `--payload-stdin`", 2);
12
+ }
13
+
14
+ const body = await resolveBodyInput({ flags: args.flags });
15
+ const payload = await resolveJsonInput({
16
+ flags: args.flags,
17
+ inlineKey: "payload",
18
+ fileKey: "payloadFile",
19
+ stdinKey: "payloadStdin",
20
+ label: "Payload",
21
+ });
22
+
23
+ if (body === undefined && payload === undefined) {
24
+ throw new CliError(
25
+ "Provide at least one of `--body`, `--body-file`, `--stdin`, `--payload`, `--payload-file`, or `--payload-stdin`",
26
+ 2,
27
+ );
28
+ }
29
+
30
+ return { body, payload };
31
+ }
@@ -0,0 +1,70 @@
1
+ import { CliError } from "../../lib/errors";
2
+ import type {
3
+ ExternalEmailAcceptance,
4
+ MailboxFilter,
5
+ ParticipantScope,
6
+ ThreadStatusFilter,
7
+ } from "../../types/api";
8
+
9
+ export function parseStatusFilter(
10
+ value: string | undefined,
11
+ ): ThreadStatusFilter | undefined {
12
+ if (!value) {
13
+ return undefined;
14
+ }
15
+
16
+ if (value === "pending" || value === "open" || value === "blocked" || value === "all") {
17
+ return value;
18
+ }
19
+
20
+ throw new CliError("--status must be one of: pending, open, blocked, all", 2);
21
+ }
22
+
23
+ export function parseMailboxFilter(
24
+ value: string | undefined,
25
+ ): MailboxFilter | undefined {
26
+ if (!value) {
27
+ return undefined;
28
+ }
29
+
30
+ if (value === "account" || value === "channel" || value === "all") {
31
+ return value;
32
+ }
33
+
34
+ throw new CliError("--mailbox must be one of: account, channel, all", 2);
35
+ }
36
+
37
+ export function parseParticipantScope(
38
+ value: string | undefined,
39
+ ): ParticipantScope | undefined {
40
+ if (!value) {
41
+ return undefined;
42
+ }
43
+
44
+ if (value === "account" || value === "owner") {
45
+ return value;
46
+ }
47
+
48
+ throw new CliError("--participant-scope must be one of: account, owner", 2);
49
+ }
50
+
51
+ export function parseExternalEmailAcceptance(value: string): ExternalEmailAcceptance {
52
+ if (
53
+ value === "screen_unknown_senders" ||
54
+ value === "screen-unknown-senders"
55
+ ) {
56
+ return "screen_unknown_senders";
57
+ }
58
+
59
+ if (
60
+ value === "accept_valid_typed_email" ||
61
+ value === "accept-valid-typed-email"
62
+ ) {
63
+ return "accept_valid_typed_email";
64
+ }
65
+
66
+ throw new CliError(
67
+ "External email acceptance policy must be one of: screen-unknown-senders, accept-valid-typed-email",
68
+ 2,
69
+ );
70
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ assertSinceFlags,
3
+ cacheResult,
4
+ changeResponseMeta,
5
+ } from "../../lib/cache";
6
+ import {
7
+ booleanFlag,
8
+ requiredPositional,
9
+ stringFlag,
10
+ type ParsedArgs,
11
+ } from "../../lib/args";
12
+ import type { CommandContext } from "../../lib/context";
13
+ import { CliError } from "../../lib/errors";
14
+ import type { Io } from "../../lib/output";
15
+ import {
16
+ maybePrepareCachePlan,
17
+ maybeSaveCacheTimestamp,
18
+ printChangeCheckResponse,
19
+ requiredSince,
20
+ } from "../../lib/polling";
21
+ import { maybeInboxMessagesScope } from "./sync-scopes";
22
+
23
+ export async function runInboxMessagesCommand(
24
+ context: CommandContext,
25
+ args: ParsedArgs,
26
+ io: Io,
27
+ ): Promise<void> {
28
+ const subcommand = args.positionals[1];
29
+
30
+ switch (subcommand) {
31
+ case "changes": {
32
+ assertSinceFlags(args);
33
+ const threadId = requiredPositional(args.positionals, 2, "Missing thread id");
34
+ const channelToken = stringFlag(args.flags, "channelToken");
35
+ const cacheScope = await maybeInboxMessagesScope(
36
+ args,
37
+ context,
38
+ channelToken,
39
+ threadId,
40
+ );
41
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
42
+ const response = await context.client.checkThreadMessageChanges({
43
+ threadId,
44
+ since: requiredSince(args, cachePlan, "inbox message"),
45
+ query: stringFlag(args.flags, "query"),
46
+ hasAttachment: booleanFlag(args.flags, "hasAttachment") || undefined,
47
+ channelToken,
48
+ });
49
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
50
+ args,
51
+ context,
52
+ cacheScope,
53
+ changeResponseMeta(response),
54
+ response.has_updates === false,
55
+ );
56
+
57
+ printChangeCheckResponse(
58
+ context,
59
+ io,
60
+ response,
61
+ cacheResult(cachePlan, savedServerTimestamp),
62
+ );
63
+ return;
64
+ }
65
+
66
+ default:
67
+ throw new CliError("Unknown inbox messages subcommand", 2);
68
+ }
69
+ }
@@ -0,0 +1,152 @@
1
+ import { stringFlag, type ParsedArgs } from "../../lib/args";
2
+ import type { CommandContext } from "../../lib/context";
3
+ import { CliError } from "../../lib/errors";
4
+ import type { InboxRecipient, InboxSender } from "../../types/api";
5
+
6
+ export async function resolveSender(
7
+ context: CommandContext,
8
+ args: ParsedArgs,
9
+ ): Promise<InboxSender | undefined> {
10
+ const from = stringFlag(args.flags, "from");
11
+ const channelToken = stringFlag(args.flags, "channelToken");
12
+
13
+ if (!from) {
14
+ return undefined;
15
+ }
16
+
17
+ if (looksLikeUuid(from)) {
18
+ return channelSender(from);
19
+ }
20
+
21
+ if (channelToken) {
22
+ const whoami = await context.client.whoami(channelToken);
23
+
24
+ if (whoami.actor.type === "channel") {
25
+ if (whoami.actor.name === from) {
26
+ return channelSender(whoami.actor.id);
27
+ }
28
+
29
+ throw new CliError(
30
+ "With `--channel-token`, `--from` must match the authenticated channel name or UUID.",
31
+ 2,
32
+ );
33
+ }
34
+ }
35
+
36
+ return channelSender(await context.client.resolveChannelId(from));
37
+ }
38
+
39
+ export async function parseRecipient(
40
+ context: CommandContext,
41
+ value: string,
42
+ ): Promise<InboxRecipient> {
43
+ if (looksLikeUuid(value)) {
44
+ if (await publicUserExists(context, value)) {
45
+ return {
46
+ type: "user",
47
+ address: {
48
+ kind: "id",
49
+ value,
50
+ },
51
+ };
52
+ }
53
+
54
+ return {
55
+ type: "channel",
56
+ address: {
57
+ kind: "id",
58
+ value,
59
+ },
60
+ };
61
+ }
62
+
63
+ if (value.startsWith("@")) {
64
+ const handleChannel = parseHandleChannel(value);
65
+
66
+ if (handleChannel) {
67
+ return {
68
+ type: "channel",
69
+ address: {
70
+ kind: "owner_handle_and_channel_name",
71
+ owner_handle: handleChannel.ownerHandle,
72
+ channel_name: handleChannel.channelName,
73
+ },
74
+ };
75
+ }
76
+
77
+ return {
78
+ type: "user",
79
+ address: {
80
+ kind: "handle",
81
+ value,
82
+ },
83
+ };
84
+ }
85
+
86
+ throw new CliError(
87
+ "Recipient must use one of: @handle, @handle/channel, user UUID, or channel UUID",
88
+ 2,
89
+ );
90
+ }
91
+
92
+ export async function getPublicInboxSchema(
93
+ context: CommandContext,
94
+ target: string,
95
+ ) {
96
+ const handleChannel = parseHandleChannel(target);
97
+
98
+ if (handleChannel) {
99
+ return context.client.getPublicChannelInboxSchema(
100
+ handleChannel.ownerHandle,
101
+ handleChannel.channelName,
102
+ );
103
+ }
104
+
105
+ if (target.startsWith("@")) {
106
+ return context.client.getPublicAccountInboxSchema(target);
107
+ }
108
+
109
+ throw new CliError("Schema target must be `@handle` or `@handle/channel`", 2);
110
+ }
111
+
112
+ async function publicUserExists(
113
+ context: CommandContext,
114
+ id: string,
115
+ ): Promise<boolean> {
116
+ const response = await context.client.listPublicUsersById([id]);
117
+ return response.items.some((user) => user.id === id);
118
+ }
119
+
120
+ function channelSender(channelId: string): InboxSender {
121
+ return {
122
+ type: "channel",
123
+ address: {
124
+ kind: "id",
125
+ value: channelId,
126
+ },
127
+ };
128
+ }
129
+
130
+ function parseHandleChannel(
131
+ value: string,
132
+ ): { ownerHandle: string; channelName: string } | undefined {
133
+ const [ownerHandle, channelName, ...extra] = value.split("/");
134
+
135
+ if (
136
+ extra.length > 0 ||
137
+ !ownerHandle ||
138
+ !channelName ||
139
+ !ownerHandle.startsWith("@")
140
+ ) {
141
+ return undefined;
142
+ }
143
+
144
+ return { ownerHandle, channelName };
145
+ }
146
+
147
+ const UUID_PATTERN =
148
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
149
+
150
+ function looksLikeUuid(value: string): boolean {
151
+ return UUID_PATTERN.test(value);
152
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ ownerIdsForThreadDisplay,
3
+ printThreadCollection,
4
+ publicUserHandlesById,
5
+ renderThreadAction,
6
+ renderThreadWithMessages,
7
+ } from "./thread-output";
8
+ export {
9
+ printEmailIntakeAction,
10
+ printEmailIntakeCollection,
11
+ printSchemaResource,
12
+ renderAttachmentCollection,
13
+ } from "./resource-output";