@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,224 @@
1
+ import {
2
+ joinBlocks,
3
+ renderFields,
4
+ renderPagination,
5
+ renderTokenAction,
6
+ } from "../../lib/human";
7
+ import { printJson, printValue, type Io } from "../../lib/output";
8
+ import { paginatedJson, paginationInfo } from "../../lib/pagination";
9
+ import type { ParsedArgs } from "../../lib/args";
10
+ import type {
11
+ ChannelAttributes,
12
+ ChannelDiagnosticsResponse,
13
+ ChannelKeyAttributes,
14
+ ChannelKeyIssueResponse,
15
+ ChannelKeyRevokeResponse,
16
+ ChannelPinResponse,
17
+ ChannelPublicationResponse,
18
+ IdResponse,
19
+ ShareTokenResponse,
20
+ } from "../../types/api";
21
+
22
+ export function printChannelCollection(
23
+ args: ParsedArgs,
24
+ outputMode: "json" | "table",
25
+ io: Io,
26
+ response: {
27
+ items: Array<{ id: string; attributes: ChannelAttributes }>;
28
+ nextCursor?: string;
29
+ },
30
+ ): void {
31
+ if (outputMode === "json") {
32
+ printJson(
33
+ io,
34
+ paginatedJson(args, {
35
+ items: response.items,
36
+ nextCursor: response.nextCursor,
37
+ }),
38
+ );
39
+ return;
40
+ }
41
+
42
+ printValue(
43
+ io,
44
+ outputMode,
45
+ response.items.map((item) => formatChannelRow(item)),
46
+ );
47
+ const pagination = paginationInfo(args, response.nextCursor);
48
+ const message = renderPagination(
49
+ pagination?.nextCursor,
50
+ pagination?.nextCommand,
51
+ );
52
+
53
+ if (message) {
54
+ io.stdout(message);
55
+ }
56
+ }
57
+
58
+ export function printChannelKeyCollection(
59
+ args: ParsedArgs,
60
+ outputMode: "json" | "table",
61
+ io: Io,
62
+ response: {
63
+ items: Array<{ id: string; attributes: ChannelKeyAttributes }>;
64
+ nextCursor?: string;
65
+ },
66
+ ): void {
67
+ if (outputMode === "json") {
68
+ printJson(
69
+ io,
70
+ paginatedJson(args, {
71
+ items: response.items,
72
+ nextCursor: response.nextCursor,
73
+ }),
74
+ );
75
+ return;
76
+ }
77
+
78
+ printValue(
79
+ io,
80
+ outputMode,
81
+ response.items.map((item) => formatChannelKeyRow(item)),
82
+ );
83
+ const pagination = paginationInfo(args, response.nextCursor);
84
+ const message = renderPagination(
85
+ pagination?.nextCursor,
86
+ pagination?.nextCommand,
87
+ );
88
+
89
+ if (message) {
90
+ io.stdout(message);
91
+ }
92
+ }
93
+
94
+ export function formatChannelRecord(channel: {
95
+ id: string;
96
+ attributes: ChannelAttributes;
97
+ }) {
98
+ return {
99
+ id: channel.id,
100
+ name: channel.attributes.name,
101
+ visibility: channel.attributes.visibility,
102
+ publiclyListed: channel.attributes.publicly_listed ?? false,
103
+ description: channel.attributes.description ?? "",
104
+ postingPausedUntil: channel.attributes.posting_paused_until ?? "",
105
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
106
+ insertedAt: channel.attributes.inserted_at ?? "",
107
+ updatedAt: channel.attributes.updated_at ?? "",
108
+ };
109
+ }
110
+
111
+ export function formatChannelDiagnostics(
112
+ diagnostics: ChannelDiagnosticsResponse,
113
+ ) {
114
+ return {
115
+ channelId: diagnostics.channel_id,
116
+ channelName: diagnostics.channel_name,
117
+ channelDescription: diagnostics.channel_description ?? "",
118
+ stateLabels: diagnostics.state_labels.join(", "),
119
+ activePublishKeyCount: diagnostics.active_publish_key_count,
120
+ lastPostedAt: diagnostics.last_posted_at ?? "",
121
+ postingPausedUntil: diagnostics.posting_paused_until ?? "",
122
+ latestBlockedWriteAt: diagnostics.latest_blocked_write_at ?? "",
123
+ latestBlockedWriteReason:
124
+ diagnostics.latest_blocked_write_reason_label ??
125
+ diagnostics.latest_blocked_write_reason ??
126
+ "",
127
+ };
128
+ }
129
+
130
+ export function renderShareToken(
131
+ title: string,
132
+ response: ShareTokenResponse,
133
+ ): string {
134
+ return joinBlocks([
135
+ title,
136
+ renderFields([["Token", response.token]]),
137
+ ]);
138
+ }
139
+
140
+ export function renderIdAction(title: string, response: IdResponse): string {
141
+ return joinBlocks([
142
+ title,
143
+ renderFields([["ID", response.id]]),
144
+ ]);
145
+ }
146
+
147
+ export function renderChannelPublicationAction(
148
+ title: string,
149
+ response: ChannelPublicationResponse,
150
+ ): string {
151
+ return joinBlocks([
152
+ title,
153
+ renderFields([
154
+ ["ID", response.id],
155
+ ["Name", response.name],
156
+ ["Publicly listed", response.publicly_listed],
157
+ ]),
158
+ ]);
159
+ }
160
+
161
+ export function renderChannelPinAction(
162
+ title: string,
163
+ response: ChannelPinResponse,
164
+ ): string {
165
+ const channel = Array.isArray(response.data)
166
+ ? response.data[0]
167
+ : response.data;
168
+
169
+ return joinBlocks([
170
+ title,
171
+ renderFields([
172
+ ["Channel", channel?.attributes.name ?? ""],
173
+ ["Pinned post", channel?.attributes.pinned_post_id ?? ""],
174
+ ]),
175
+ ]);
176
+ }
177
+
178
+ export function renderChannelKeyIssue(
179
+ title: string,
180
+ response: ChannelKeyIssueResponse,
181
+ ): string {
182
+ return renderTokenAction({
183
+ title,
184
+ id: response.id,
185
+ name: response.name,
186
+ token: response.token,
187
+ issuedAt: response.issued_at,
188
+ expiresAt: response.expires_at,
189
+ });
190
+ }
191
+
192
+ export function renderChannelKeyRevoke(
193
+ title: string,
194
+ response: ChannelKeyRevokeResponse,
195
+ ): string {
196
+ return joinBlocks([
197
+ title,
198
+ renderFields([
199
+ ["ID", response.id],
200
+ ["Name", response.name],
201
+ ]),
202
+ ]);
203
+ }
204
+
205
+ function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }) {
206
+ return {
207
+ id: channel.id,
208
+ name: channel.attributes.name,
209
+ visibility: channel.attributes.visibility,
210
+ publiclyListed: channel.attributes.publicly_listed ?? false,
211
+ pinnedPostId: channel.attributes.pinned_post_id ?? "",
212
+ description: channel.attributes.description ?? "",
213
+ };
214
+ }
215
+
216
+ function formatChannelKeyRow(item: { id: string; attributes: ChannelKeyAttributes }) {
217
+ return {
218
+ id: item.id,
219
+ name: item.attributes.name ?? "",
220
+ expiresAt: item.attributes.expires_at ?? "",
221
+ revokedAt: item.attributes.revoked_at ?? "",
222
+ issuedAt: item.attributes.inserted_at ?? "",
223
+ };
224
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ booleanFlag,
3
+ integerFlag,
4
+ requiredPositional,
5
+ requiredStringFlag,
6
+ stringFlag,
7
+ type ParsedArgs,
8
+ } from "../../lib/args";
9
+ import { storeChannelToken, updateProfile } from "../../lib/config";
10
+ import { createCommandContext } from "../../lib/context";
11
+ import { CliError } from "../../lib/errors";
12
+ import { printValue, type Io } from "../../lib/output";
13
+ import type { ChannelKeyIssueResponse } from "../../types/api";
14
+ import {
15
+ printChannelKeyCollection,
16
+ renderChannelKeyIssue,
17
+ renderChannelKeyRevoke,
18
+ } from "./render";
19
+
20
+ export async function runChannelTokenCommand(
21
+ args: ParsedArgs,
22
+ io: Io,
23
+ ): Promise<void> {
24
+ const context = await createCommandContext(args, io);
25
+ const tokenCommand = requiredPositional(
26
+ args.positionals,
27
+ 1,
28
+ "Missing channel token subcommand",
29
+ );
30
+
31
+ switch (tokenCommand) {
32
+ case "list": {
33
+ const channelId = await context.client.resolveChannelId(
34
+ requiredPositional(args.positionals, 2, "Missing channel"),
35
+ );
36
+ const response = await context.client.listChannelKeys({
37
+ channelId,
38
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
39
+ cursor: stringFlag(args.flags, "cursor"),
40
+ });
41
+
42
+ printChannelKeyCollection(args, context.outputMode, io, response);
43
+ return;
44
+ }
45
+
46
+ case "issue": {
47
+ const channelId = await context.client.resolveChannelId(
48
+ requiredPositional(args.positionals, 2, "Missing channel"),
49
+ );
50
+ const response = await context.client.issueChannelKey({
51
+ channelId,
52
+ name: requiredStringFlag(args.flags, "name"),
53
+ });
54
+
55
+ await maybeStoreChannelToken(
56
+ args,
57
+ response,
58
+ channelId,
59
+ context.profileName,
60
+ context.configPath,
61
+ );
62
+
63
+ if (booleanFlag(args.flags, "tokenOnly")) {
64
+ io.stdout(response.token);
65
+ return;
66
+ }
67
+
68
+ printValue(
69
+ io,
70
+ context.outputMode,
71
+ context.outputMode === "json"
72
+ ? response
73
+ : renderChannelKeyIssue("Issued channel token", response),
74
+ );
75
+ return;
76
+ }
77
+
78
+ case "revoke": {
79
+ const response = await context.client.revokeChannelKey(
80
+ requiredPositional(args.positionals, 2, "Missing channel key id"),
81
+ );
82
+ await pruneInvalidStoredChannelTokens(
83
+ context.profileName,
84
+ context.profile.channelTokens,
85
+ context.client,
86
+ context.configPath,
87
+ );
88
+
89
+ printValue(
90
+ io,
91
+ context.outputMode,
92
+ context.outputMode === "json"
93
+ ? response
94
+ : renderChannelKeyRevoke("Revoked channel token", response),
95
+ );
96
+ return;
97
+ }
98
+
99
+ default:
100
+ throw new CliError("Unknown channel token subcommand", 2);
101
+ }
102
+ }
103
+
104
+ async function maybeStoreChannelToken(
105
+ args: ParsedArgs,
106
+ response: ChannelKeyIssueResponse,
107
+ channelId: string,
108
+ profileName: string,
109
+ configPath: string,
110
+ ): Promise<void> {
111
+ if (!booleanFlag(args.flags, "save")) {
112
+ return;
113
+ }
114
+
115
+ await storeChannelToken(profileName, channelId, response.token, configPath);
116
+ }
117
+
118
+ async function pruneInvalidStoredChannelTokens(
119
+ profileName: string,
120
+ storedTokens: Record<string, { token: string }>,
121
+ client: Awaited<ReturnType<typeof createCommandContext>>["client"],
122
+ configPath: string,
123
+ ): Promise<void> {
124
+ const invalidChannelIds: string[] = [];
125
+
126
+ for (const [channelId, storedToken] of Object.entries(storedTokens)) {
127
+ if (!(await client.canAuthenticate(storedToken.token))) {
128
+ invalidChannelIds.push(channelId);
129
+ }
130
+ }
131
+
132
+ if (invalidChannelIds.length === 0) {
133
+ return;
134
+ }
135
+
136
+ await updateProfile(
137
+ profileName,
138
+ (profile) => {
139
+ for (const channelId of invalidChannelIds) {
140
+ delete profile.channelTokens[channelId];
141
+ }
142
+ },
143
+ configPath,
144
+ );
145
+ }
@@ -0,0 +1,11 @@
1
+ import { CliError } from "../../lib/errors";
2
+
3
+ const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
4
+ const CHANNEL_NAME_ERROR =
5
+ "Channel names must start with a lowercase letter or digit and then use only lowercase letters, digits, hyphens, or underscores.";
6
+
7
+ export function assertValidChannelName(name: string): void {
8
+ if (!CHANNEL_NAME_PATTERN.test(name)) {
9
+ throw new CliError(CHANNEL_NAME_ERROR, 2);
10
+ }
11
+ }