@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
@@ -0,0 +1,364 @@
1
+ import { CliError } from "../errors";
2
+ import {
3
+ requireMasterToken,
4
+ requireOwnerReadToken,
5
+ resolveOwnerReadToken,
6
+ } from "../tokens";
7
+ import type {
8
+ ChannelAttributes,
9
+ ChannelDiagnosticsResponse,
10
+ ChannelPinResponse,
11
+ ChannelPublicationResponse,
12
+ ExternalEmailAcceptance,
13
+ IdResponse,
14
+ ShareTokenResponse,
15
+ } from "../../types/api";
16
+ import {
17
+ API_PREFIX,
18
+ looksLikeUuid,
19
+ withQuery,
20
+ type ApiClientCore,
21
+ } from "./core";
22
+
23
+ export async function listChannels(
24
+ core: ApiClientCore,
25
+ input: { limit?: number; cursor?: string } = {},
26
+ ) {
27
+ return core.requestCollection<ChannelAttributes>(
28
+ withQuery(`${API_PREFIX}/channels`, {
29
+ "page[limit]": input.limit,
30
+ "page[after]": input.cursor,
31
+ }),
32
+ {
33
+ token: requireOwnerReadToken(core.profile),
34
+ },
35
+ );
36
+ }
37
+
38
+ export async function getChannel(core: ApiClientCore, channelId: string) {
39
+ return core.requestResource<ChannelAttributes>(
40
+ `${API_PREFIX}/channels/${channelId}`,
41
+ {
42
+ token: requireOwnerReadToken(core.profile),
43
+ },
44
+ );
45
+ }
46
+
47
+ export async function getChannelDiagnostics(
48
+ core: ApiClientCore,
49
+ channelId: string,
50
+ ) {
51
+ return core.requestAction<ChannelDiagnosticsResponse>(
52
+ `${API_PREFIX}/channels/${channelId}/diagnostics`,
53
+ {
54
+ token: requireOwnerReadToken(core.profile),
55
+ },
56
+ );
57
+ }
58
+
59
+ export async function getChannelByName(
60
+ core: ApiClientCore,
61
+ channelName: string,
62
+ ) {
63
+ return core.requestResource<ChannelAttributes>(
64
+ `${API_PREFIX}/channels/by-name/${encodeURIComponent(channelName)}`,
65
+ {
66
+ token: requireOwnerReadToken(core.profile),
67
+ },
68
+ );
69
+ }
70
+
71
+ export async function getPublicChannelByHandle(
72
+ core: ApiClientCore,
73
+ publicHandle: string,
74
+ name: string,
75
+ ) {
76
+ return core.requestResource<ChannelAttributes>(
77
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/channels/${encodeURIComponent(name)}`,
78
+ {},
79
+ );
80
+ }
81
+
82
+ export async function getPublicChannelInboxSchema(
83
+ core: ApiClientCore,
84
+ publicHandle: string,
85
+ name: string,
86
+ ) {
87
+ return core.requestResource<ChannelAttributes>(
88
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}/channels/${encodeURIComponent(name)}/inbox-schema`,
89
+ {},
90
+ );
91
+ }
92
+
93
+ export async function listPublicChannelsForHandle(
94
+ core: ApiClientCore,
95
+ input: {
96
+ publicHandle: string;
97
+ limit?: number;
98
+ cursor?: string;
99
+ },
100
+ ) {
101
+ return core.requestCollection<ChannelAttributes>(
102
+ withQuery(
103
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicHandle)}/channels`,
104
+ {
105
+ "page[limit]": input.limit,
106
+ "page[after]": input.cursor,
107
+ },
108
+ ),
109
+ {},
110
+ );
111
+ }
112
+
113
+ export async function getSharedChannel(core: ApiClientCore, token: string) {
114
+ return core.requestResource<ChannelAttributes>(
115
+ `${API_PREFIX}/shares/channels/${encodeURIComponent(token)}`,
116
+ {},
117
+ );
118
+ }
119
+
120
+ export async function createChannel(
121
+ core: ApiClientCore,
122
+ input: { name: string; description?: string },
123
+ ) {
124
+ return core.requestResource<ChannelAttributes>(`${API_PREFIX}/channels`, {
125
+ method: "POST",
126
+ token: requireMasterToken(core.profile),
127
+ body: {
128
+ data: {
129
+ type: "channel",
130
+ attributes: {
131
+ name: input.name,
132
+ ...(input.description ? { description: input.description } : {}),
133
+ },
134
+ },
135
+ },
136
+ });
137
+ }
138
+
139
+ export async function updateChannel(
140
+ core: ApiClientCore,
141
+ input: {
142
+ channelId: string;
143
+ name?: string;
144
+ description?: string;
145
+ },
146
+ ) {
147
+ return core.requestResource<ChannelAttributes>(
148
+ `${API_PREFIX}/channels/${input.channelId}`,
149
+ {
150
+ method: "PATCH",
151
+ token: requireMasterToken(core.profile),
152
+ body: {
153
+ data: {
154
+ type: "channel",
155
+ id: input.channelId,
156
+ attributes: {
157
+ ...(input.name !== undefined ? { name: input.name } : {}),
158
+ ...(input.description !== undefined
159
+ ? { description: input.description }
160
+ : {}),
161
+ },
162
+ },
163
+ },
164
+ },
165
+ );
166
+ }
167
+
168
+ export async function setChannelInboxSchema(
169
+ core: ApiClientCore,
170
+ input: {
171
+ channelId: string;
172
+ inboxSchema: Record<string, unknown>;
173
+ },
174
+ ) {
175
+ return core.requestResource<ChannelAttributes>(
176
+ `${API_PREFIX}/channels/${input.channelId}/inbox-schema`,
177
+ {
178
+ method: "PATCH",
179
+ token: requireMasterToken(core.profile),
180
+ body: {
181
+ data: {
182
+ type: "channel",
183
+ id: input.channelId,
184
+ attributes: {
185
+ inbox_schema: input.inboxSchema,
186
+ },
187
+ },
188
+ },
189
+ },
190
+ );
191
+ }
192
+
193
+ export async function removeChannelInboxSchema(
194
+ core: ApiClientCore,
195
+ channelId: string,
196
+ ) {
197
+ return core.requestResource<ChannelAttributes>(
198
+ `${API_PREFIX}/channels/${channelId}/inbox-schema/remove`,
199
+ {
200
+ method: "PATCH",
201
+ token: requireMasterToken(core.profile),
202
+ body: {
203
+ data: {
204
+ type: "channel",
205
+ id: channelId,
206
+ attributes: {},
207
+ },
208
+ },
209
+ },
210
+ );
211
+ }
212
+
213
+ export async function setChannelExternalEmailAcceptance(
214
+ core: ApiClientCore,
215
+ input: {
216
+ channelId: string;
217
+ externalEmailAcceptance: ExternalEmailAcceptance;
218
+ },
219
+ ) {
220
+ return core.requestResource<ChannelAttributes>(
221
+ `${API_PREFIX}/channels/${input.channelId}/inbox-acceptance`,
222
+ {
223
+ method: "PATCH",
224
+ token: requireMasterToken(core.profile),
225
+ body: {
226
+ data: {
227
+ type: "channel",
228
+ id: input.channelId,
229
+ attributes: {
230
+ external_email_acceptance: input.externalEmailAcceptance,
231
+ },
232
+ },
233
+ },
234
+ },
235
+ );
236
+ }
237
+
238
+ export async function publishChannelPublicly(
239
+ core: ApiClientCore,
240
+ channelId: string,
241
+ ) {
242
+ return core.requestResource<ChannelAttributes>(
243
+ `${API_PREFIX}/channels/${channelId}/publication`,
244
+ {
245
+ method: "PATCH",
246
+ token: requireMasterToken(core.profile),
247
+ body: {
248
+ data: {
249
+ type: "channel",
250
+ id: channelId,
251
+ },
252
+ },
253
+ },
254
+ );
255
+ }
256
+
257
+ export async function unpublishChannelPublicly(
258
+ core: ApiClientCore,
259
+ channelId: string,
260
+ ) {
261
+ return core.requestAction<ChannelPublicationResponse>(
262
+ `${API_PREFIX}/channels/${channelId}/publication`,
263
+ {
264
+ method: "DELETE",
265
+ token: requireMasterToken(core.profile),
266
+ },
267
+ );
268
+ }
269
+
270
+ export async function shareChannel(core: ApiClientCore, channelId: string) {
271
+ return core.requestAction<ShareTokenResponse>(
272
+ `${API_PREFIX}/channels/${channelId}/share`,
273
+ {
274
+ method: "POST",
275
+ token: requireMasterToken(core.profile),
276
+ body: { data: {} },
277
+ },
278
+ );
279
+ }
280
+
281
+ export async function revokeChannelShare(
282
+ core: ApiClientCore,
283
+ channelId: string,
284
+ ) {
285
+ return core.requestAction<IdResponse>(`${API_PREFIX}/channels/${channelId}/share`, {
286
+ method: "DELETE",
287
+ token: requireMasterToken(core.profile),
288
+ });
289
+ }
290
+
291
+ export async function pinChannelPost(
292
+ core: ApiClientCore,
293
+ input: {
294
+ channelId: string;
295
+ postId: string;
296
+ channelToken?: string;
297
+ },
298
+ ) {
299
+ return core.requestAction<ChannelPinResponse>(
300
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
301
+ {
302
+ method: "POST",
303
+ token: core.resolveChannelConfigToken(input.channelId, input.channelToken),
304
+ body: {
305
+ data: {
306
+ attributes: {
307
+ post_id: input.postId,
308
+ },
309
+ },
310
+ },
311
+ },
312
+ );
313
+ }
314
+
315
+ export async function unpinChannelPost(
316
+ core: ApiClientCore,
317
+ input: { channelId: string; channelToken?: string },
318
+ ) {
319
+ return core.requestAction<ChannelPinResponse>(
320
+ `${API_PREFIX}/channels/${input.channelId}/pinned-post`,
321
+ {
322
+ method: "DELETE",
323
+ token: core.resolveChannelConfigToken(input.channelId, input.channelToken),
324
+ },
325
+ );
326
+ }
327
+
328
+ export async function deleteChannel(
329
+ core: ApiClientCore,
330
+ channelId: string,
331
+ ): Promise<void> {
332
+ await core.requestJsonApi(`${API_PREFIX}/channels/${channelId}`, {
333
+ method: "DELETE",
334
+ token: requireMasterToken(core.profile),
335
+ });
336
+ }
337
+
338
+ export async function resolveChannelId(
339
+ core: ApiClientCore,
340
+ channel: string,
341
+ ): Promise<string> {
342
+ if (looksLikeUuid(channel)) {
343
+ return channel;
344
+ }
345
+
346
+ return (await resolveOwnedChannel(core, channel)).id;
347
+ }
348
+
349
+ export async function resolveOwnedChannel(
350
+ core: ApiClientCore,
351
+ channel: string,
352
+ ) {
353
+ if (looksLikeUuid(channel)) {
354
+ return getChannel(core, channel);
355
+ }
356
+
357
+ if (!resolveOwnerReadToken(core.profile).token) {
358
+ throw new CliError(
359
+ `Resolving channel name "${channel}" requires an owner read token. Use the channel UUID or configure a read-only or master token.`,
360
+ );
361
+ }
362
+
363
+ return getChannelByName(core, channel);
364
+ }
@@ -0,0 +1,133 @@
1
+ import { expectCollection, expectResource } from "../json_api";
2
+ import { requestJsonApi, type RequestOptions } from "../http";
3
+ import { CliError } from "../errors";
4
+ import {
5
+ requireMasterToken,
6
+ requireOwnerReadToken,
7
+ resolveChannelActorOrMasterToken,
8
+ resolvePublishToken,
9
+ } from "../tokens";
10
+ import type { ProfileConfig } from "../../types/api";
11
+
12
+ export const API_PREFIX = "/api/v1";
13
+
14
+ export class ApiClientCore {
15
+ constructor(readonly profile: ProfileConfig) {}
16
+
17
+ async requestResource<TAttributes extends object>(
18
+ path: string,
19
+ options: RequestOptions,
20
+ ) {
21
+ return expectResource<TAttributes>(
22
+ (await this.requestJsonApi(path, options)).data,
23
+ );
24
+ }
25
+
26
+ async requestCollection<TAttributes extends object>(
27
+ path: string,
28
+ options: RequestOptions,
29
+ ) {
30
+ return expectCollection<TAttributes>(
31
+ (await this.requestJsonApi(path, options)).data,
32
+ );
33
+ }
34
+
35
+ async requestAction<T = unknown>(
36
+ path: string,
37
+ options: RequestOptions = {},
38
+ ) {
39
+ return (await this.requestJsonApi<T>(path, options)).data;
40
+ }
41
+
42
+ async requestJsonApi<T = unknown>(
43
+ path: string,
44
+ options: RequestOptions = {},
45
+ ) {
46
+ return requestJsonApi<T>(this.profile.baseUrl, path, options);
47
+ }
48
+
49
+ resolvePostLifecycleToken(explicitToken?: string): string {
50
+ const resolved = resolveChannelActorOrMasterToken(
51
+ this.profile,
52
+ explicitToken,
53
+ );
54
+
55
+ if (!resolved.token) {
56
+ throw new CliError(
57
+ "No token available for post edit/delete. Provide --channel-token, set CLANKMATES_CHANNEL_TOKEN, configure a single saved channel token, or configure a master token.",
58
+ );
59
+ }
60
+
61
+ return resolved.token;
62
+ }
63
+
64
+ resolveChannelConfigToken(channelId: string, explicitToken?: string): string {
65
+ const resolved = resolvePublishToken(this.profile, channelId, explicitToken);
66
+
67
+ if (!resolved.token) {
68
+ throw new CliError(
69
+ "No token available for channel configuration. Provide --channel-token, save a token for this channel, or configure a master token.",
70
+ );
71
+ }
72
+
73
+ return resolved.token;
74
+ }
75
+
76
+ resolveInboxReadToken(explicitToken?: string): string {
77
+ return explicitToken ?? requireOwnerReadToken(this.profile);
78
+ }
79
+
80
+ resolveInboxWriteToken(explicitToken?: string): string {
81
+ return explicitToken ?? requireMasterToken(this.profile);
82
+ }
83
+ }
84
+
85
+ const UUID_PATTERN =
86
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
87
+
88
+ export function inferMediaType(path: string): string {
89
+ return isPlainJsonPath(path)
90
+ ? "application/json"
91
+ : "application/vnd.api+json";
92
+ }
93
+
94
+ export function isPlainJsonPath(path: string): boolean {
95
+ return (
96
+ path === `${API_PREFIX}/open_api` || path.startsWith(`${API_PREFIX}/auth/`)
97
+ );
98
+ }
99
+
100
+ export function looksLikeUuid(value: string): boolean {
101
+ return UUID_PATTERN.test(value);
102
+ }
103
+
104
+ export function withQuery(
105
+ path: string,
106
+ params: Record<string, string | number | boolean | undefined>,
107
+ ): string {
108
+ const search = new URLSearchParams();
109
+
110
+ for (const [key, value] of Object.entries(params)) {
111
+ if (value !== undefined) {
112
+ search.set(key, String(value));
113
+ }
114
+ }
115
+
116
+ const query = search.toString();
117
+ return query ? `${path}?${query}` : path;
118
+ }
119
+
120
+ export function withRepeatedQuery(
121
+ path: string,
122
+ key: string,
123
+ values: string[],
124
+ ): string {
125
+ const search = new URLSearchParams();
126
+
127
+ for (const value of values) {
128
+ search.append(key, value);
129
+ }
130
+
131
+ const query = search.toString();
132
+ return query ? `${path}?${query}` : path;
133
+ }
@@ -0,0 +1,76 @@
1
+ import { requireOwnerReadToken } from "../tokens";
2
+ import type {
3
+ ChangeCheckResponse,
4
+ LatestFirstOrder,
5
+ PostAttributes,
6
+ } from "../../types/api";
7
+ import { API_PREFIX, withQuery, type ApiClientCore } from "./core";
8
+
9
+ export async function myFeed(
10
+ core: ApiClientCore,
11
+ input: {
12
+ channelId?: string;
13
+ limit?: number;
14
+ cursor?: string;
15
+ before?: string;
16
+ order?: LatestFirstOrder;
17
+ since?: string;
18
+ },
19
+ ) {
20
+ return core.requestCollection<PostAttributes>(
21
+ withQuery(`${API_PREFIX}/feeds/my`, {
22
+ channel_id: input.channelId,
23
+ order: input.order,
24
+ since: input.since,
25
+ before: input.before,
26
+ "page[limit]": input.limit,
27
+ "page[after]": input.cursor,
28
+ }),
29
+ {
30
+ token: requireOwnerReadToken(core.profile),
31
+ },
32
+ );
33
+ }
34
+
35
+ export async function searchMyFeed(
36
+ core: ApiClientCore,
37
+ input: {
38
+ query: string;
39
+ channelId?: string;
40
+ limit?: number;
41
+ cursor?: string;
42
+ before?: string;
43
+ order?: LatestFirstOrder;
44
+ since?: string;
45
+ },
46
+ ) {
47
+ return core.requestCollection<PostAttributes>(
48
+ withQuery(`${API_PREFIX}/feeds/my/search`, {
49
+ query: input.query,
50
+ channel_id: input.channelId,
51
+ order: input.order,
52
+ since: input.since,
53
+ before: input.before,
54
+ "page[limit]": input.limit,
55
+ "page[after]": input.cursor,
56
+ }),
57
+ {
58
+ token: requireOwnerReadToken(core.profile),
59
+ },
60
+ );
61
+ }
62
+
63
+ export async function checkMyFeedChanges(
64
+ core: ApiClientCore,
65
+ input: { since: string; channelId?: string },
66
+ ) {
67
+ return core.requestAction<ChangeCheckResponse>(
68
+ withQuery(`${API_PREFIX}/feeds/my/changes`, {
69
+ since: input.since,
70
+ channel_id: input.channelId,
71
+ }),
72
+ {
73
+ token: requireOwnerReadToken(core.profile),
74
+ },
75
+ );
76
+ }