@clankmates/cli 0.11.1 → 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 (49) hide show
  1. package/README.md +7 -3
  2. package/package.json +1 -1
  3. package/skills/codex/clankmates/SKILL.md +4 -3
  4. package/src/commands/auth/access-keys.ts +206 -0
  5. package/src/commands/auth.ts +3 -196
  6. package/src/commands/channel/render.ts +224 -0
  7. package/src/commands/channel/tokens.ts +145 -0
  8. package/src/commands/channel/validation.ts +11 -0
  9. package/src/commands/channel.ts +11 -340
  10. package/src/commands/doctor/checks.ts +123 -0
  11. package/src/commands/doctor/render.ts +140 -0
  12. package/src/commands/doctor/suggestions.ts +42 -0
  13. package/src/commands/doctor/types.ts +75 -0
  14. package/src/commands/doctor.ts +12 -371
  15. package/src/commands/feed.ts +19 -178
  16. package/src/commands/inbox/content.ts +31 -0
  17. package/src/commands/inbox/filters.ts +70 -0
  18. package/src/commands/inbox/messages.ts +69 -0
  19. package/src/commands/inbox/participants.ts +152 -0
  20. package/src/commands/inbox/render.ts +13 -0
  21. package/src/commands/inbox/resource-output.ts +217 -0
  22. package/src/commands/inbox/schema.ts +185 -0
  23. package/src/commands/inbox/screening.ts +76 -0
  24. package/src/commands/inbox/sync-scopes.ts +59 -0
  25. package/src/commands/inbox/thread-output.ts +344 -0
  26. package/src/commands/inbox/watch.ts +203 -0
  27. package/src/commands/inbox.ts +58 -1220
  28. package/src/commands/post.ts +24 -116
  29. package/src/lib/args.ts +8 -0
  30. package/src/lib/cache/scopes.ts +216 -0
  31. package/src/lib/cache/store.ts +195 -0
  32. package/src/lib/cache/types.ts +31 -0
  33. package/src/lib/cache.ts +18 -382
  34. package/src/lib/client/auth.ts +122 -0
  35. package/src/lib/client/channel-keys.ts +57 -0
  36. package/src/lib/client/channels.ts +364 -0
  37. package/src/lib/client/core.ts +133 -0
  38. package/src/lib/client/feed.ts +76 -0
  39. package/src/lib/client/inbox.ts +361 -0
  40. package/src/lib/client/posts.ts +213 -0
  41. package/src/lib/client/raw-api.ts +33 -0
  42. package/src/lib/client/users.ts +88 -0
  43. package/src/lib/client.ts +197 -894
  44. package/src/lib/help.ts +66 -9
  45. package/src/lib/json_api.ts +74 -9
  46. package/src/lib/pagination.ts +5 -0
  47. package/src/lib/polling.ts +146 -0
  48. package/src/lib/post-output.ts +55 -0
  49. package/src/types/api.ts +1 -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];
@@ -40,6 +42,7 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
40
42
  channelId,
41
43
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
42
44
  cursor: stringFlag(args.flags, "cursor"),
45
+ before: stringFlag(args.flags, "before"),
43
46
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
44
47
  since: resolvedSince(args, cachePlan),
45
48
  });
@@ -51,9 +54,9 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
51
54
  response.nextCursor === undefined,
52
55
  );
53
56
 
54
- printFeedResponse(
57
+ printPostCollection(
55
58
  args,
56
- context,
59
+ context.outputMode,
57
60
  io,
58
61
  response,
59
62
  cacheResult(cachePlan, savedServerTimestamp),
@@ -77,6 +80,7 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
77
80
  channelId,
78
81
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
79
82
  cursor: stringFlag(args.flags, "cursor"),
83
+ before: stringFlag(args.flags, "before"),
80
84
  order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
81
85
  since: resolvedSince(args, cachePlan),
82
86
  });
@@ -88,9 +92,9 @@ export async function runFeedCommand(args: ParsedArgs, io: Io): Promise<void> {
88
92
  response.nextCursor === undefined,
89
93
  );
90
94
 
91
- printFeedResponse(
95
+ printPostCollection(
92
96
  args,
93
- context,
97
+ context.outputMode,
94
98
  io,
95
99
  response,
96
100
  cacheResult(cachePlan, savedServerTimestamp),
@@ -138,135 +142,6 @@ async function resolveChannelId(
138
142
  return channel ? context.client.resolveChannelId(channel) : undefined;
139
143
  }
140
144
 
141
- function printFeedResponse(
142
- args: ParsedArgs,
143
- context: CommandContext,
144
- io: Io,
145
- response: {
146
- items: Array<{ id: string; attributes: PostAttributes }>;
147
- nextCursor?: string;
148
- meta?: Record<string, unknown>;
149
- },
150
- cache?: CacheResult,
151
- ): void {
152
- if (context.outputMode === "json") {
153
- printJson(
154
- io,
155
- paginatedJson(args, {
156
- items: response.items,
157
- nextCursor: response.nextCursor,
158
- meta: response.meta,
159
- ...(cache ? { cache } : {}),
160
- }),
161
- );
162
- return;
163
- }
164
-
165
- const rows = response.items.map((item) => ({
166
- id: item.id,
167
- source: item.attributes.source,
168
- date: item.attributes.updated_at ?? item.attributes.inserted_at ?? "",
169
- body: item.attributes.body,
170
- }));
171
- printValue(io, context.outputMode, rows);
172
-
173
- const pagination = paginationInfo(args, response.nextCursor);
174
- const message = renderPagination(
175
- pagination?.nextCursor,
176
- pagination?.nextCommand,
177
- );
178
-
179
- if (message) {
180
- io.stdout(message);
181
- }
182
-
183
- printCacheNote(io, cache);
184
- }
185
-
186
- function requiredSince(args: ParsedArgs, cachePlan?: CachePlan): string {
187
- const since = stringFlag(args.flags, "since");
188
-
189
- if (since) {
190
- return since;
191
- }
192
-
193
- if (cachePlan?.previousServerTimestamp) {
194
- return cachePlan.previousServerTimestamp;
195
- }
196
-
197
- if (cacheFlags(args).sinceCache) {
198
- throw new CliError(
199
- "No cached server timestamp for this feed scope. Run a read command with `--save-cache` first.",
200
- 2,
201
- );
202
- }
203
-
204
- if (!since) {
205
- throw new CliError("Missing `--since`", 2);
206
- }
207
-
208
- return since;
209
- }
210
-
211
- function parseLatestFirstOrder(value: string | undefined): LatestFirstOrder | undefined {
212
- if (!value) {
213
- return undefined;
214
- }
215
-
216
- if (value === "latest" || value === "oldest") {
217
- return value;
218
- }
219
-
220
- throw new CliError("--order must be one of: latest, oldest", 2);
221
- }
222
-
223
- function printChangeCheckResponse(
224
- context: CommandContext,
225
- io: Io,
226
- response: ChangeCheckResponse,
227
- cache?: CacheResult,
228
- ): void {
229
- printValue(
230
- io,
231
- context.outputMode,
232
- context.outputMode === "json"
233
- ? { ...response, ...(cache ? { cache } : {}) }
234
- : renderFields([
235
- ["Has updates", response.has_updates ? "yes" : "no"],
236
- ["Server time", formatTimestamp(response.server_time)],
237
- [
238
- "Recommended poll",
239
- response.recommended_poll_after_ms === undefined
240
- ? undefined
241
- : `${response.recommended_poll_after_ms}ms`,
242
- ],
243
- ...cacheFields(cache),
244
- ]),
245
- );
246
- }
247
-
248
- async function maybePrepareCachePlan(
249
- args: ParsedArgs,
250
- context: CommandContext,
251
- scope: CacheScope | undefined,
252
- ): Promise<CachePlan | undefined> {
253
- return cacheFlags(args).sinceCache && scope
254
- ? prepareCachePlan(context, scope)
255
- : undefined;
256
- }
257
-
258
- async function maybeSaveCacheTimestamp(
259
- args: ParsedArgs,
260
- context: CommandContext,
261
- scope: CacheScope | undefined,
262
- meta: Record<string, unknown> | undefined,
263
- shouldSave: boolean,
264
- ): Promise<string | undefined> {
265
- return cacheFlags(args).saveCache && scope && shouldSave
266
- ? saveCacheTimestamp(context, scope, meta)
267
- : undefined;
268
- }
269
-
270
145
  async function maybeFeedMyScope(
271
146
  args: ParsedArgs,
272
147
  context: CommandContext,
@@ -280,6 +155,7 @@ async function maybeFeedMyScope(
280
155
  context,
281
156
  actorKey: await authenticatedActorKey(context),
282
157
  channelId,
158
+ before: stringFlag(args.flags, "before"),
283
159
  });
284
160
  }
285
161
 
@@ -298,41 +174,6 @@ async function maybeFeedSearchScope(
298
174
  actorKey: await authenticatedActorKey(context),
299
175
  query,
300
176
  channelId,
177
+ before: stringFlag(args.flags, "before"),
301
178
  });
302
179
  }
303
-
304
- function resolvedSince(
305
- args: ParsedArgs,
306
- cachePlan: CachePlan | undefined,
307
- ): string | undefined {
308
- return stringFlag(args.flags, "since") ?? cachePlan?.previousServerTimestamp;
309
- }
310
-
311
- function printCacheNote(io: Io, cache: CacheResult | undefined): void {
312
- if (!cache) {
313
- return;
314
- }
315
-
316
- const details = [
317
- cache.previousServerTimestamp
318
- ? `used ${cache.previousServerTimestamp}`
319
- : cache.hit
320
- ? "used cache"
321
- : "no cached timestamp",
322
- cache.savedServerTimestamp ? `saved ${cache.savedServerTimestamp}` : undefined,
323
- ].filter(Boolean);
324
-
325
- io.stdout(`Cache: ${details.join("; ")}.`);
326
- }
327
-
328
- function cacheFields(cache: CacheResult | undefined): Array<[string, string | undefined]> {
329
- if (!cache) {
330
- return [];
331
- }
332
-
333
- return [
334
- ["Cache scope", cache.scopeKey],
335
- ["Cached timestamp", cache.previousServerTimestamp],
336
- ["Saved timestamp", cache.savedServerTimestamp],
337
- ];
338
- }
@@ -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";