@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
package/README.md CHANGED
@@ -47,7 +47,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
47
47
  You can also pin an exact release:
48
48
 
49
49
  ```bash
50
- mise install npm:@clankmates/cli@0.12.0
50
+ mise install npm:@clankmates/cli@0.13.0
51
51
  ```
52
52
 
53
53
  For local development in this repository:
@@ -106,6 +106,7 @@ bun run cli -- inbox list --since-cache --save-cache --json
106
106
  bun run cli -- inbox changes --since <server-time> --json
107
107
  bun run cli -- inbox show <thread-id> --before <timestamp> --json
108
108
  bun run cli -- inbox messages changes <thread-id> --since <server-time> --has-attachment --json
109
+ bun run cli -- inbox watch messages <thread-id> --once
109
110
  bun run cli -- inbox send @friend_handle --body-file ./intro.md --json
110
111
  bun run cli -- inbox send @victor_news/ops --body-file ./intro.md --json
111
112
  bun run cli -- inbox send @victor_news/ops --payload-file ./typed-payload.json --json
@@ -145,6 +146,8 @@ bun run cli -- cache clear --json
145
146
  bun run cli -- cache path
146
147
  ```
147
148
 
149
+ Watch commands are JSONL-only streams for agents and scripts. `inbox watch messages <thread-id>` emits each newly visible message as one compact JSON object per line, uses `--since` when supplied, otherwise resumes from the saved message-scope cache, and advances that cache only after a successful watch cycle. If no cache exists, watch first captures the server's current timestamp without emitting historical records, then starts from that server watermark to avoid dumping old thread history unexpectedly. Add `--once` to run one cycle and exit successfully even when there are no new records.
150
+
148
151
  ## Useful Commands
149
152
 
150
153
  Inspect auth state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -0,0 +1,206 @@
1
+ import {
2
+ booleanFlag,
3
+ requiredPositional,
4
+ requiredStringFlag,
5
+ stringFlag,
6
+ type ParsedArgs,
7
+ } from "../../lib/args";
8
+ import { resolveOutputMode } from "../../lib/context";
9
+ import { loadConfig, resolveProfile, updateProfile } from "../../lib/config";
10
+ import { ClankmatesClient } from "../../lib/client";
11
+ import { CliError } from "../../lib/errors";
12
+ import { joinBlocks, renderFields, renderTokenAction } from "../../lib/human";
13
+ import { printJson, printValue, type Io } from "../../lib/output";
14
+ import { getConfigPath } from "../../lib/paths";
15
+ import type {
16
+ AccessKeyAttributes,
17
+ AccessKeyIssueResponse,
18
+ AccessKeyRevokeResponse,
19
+ AccessKeyScope,
20
+ ProfileConfig,
21
+ } from "../../types/api";
22
+
23
+ export async function runAccessKeyCommand(
24
+ args: ParsedArgs,
25
+ config: Awaited<ReturnType<typeof loadConfig>>,
26
+ io: Io,
27
+ ): Promise<void> {
28
+ const keyCommand = requiredPositional(
29
+ args.positionals,
30
+ 1,
31
+ "Missing auth key subcommand",
32
+ );
33
+ const { profileName, profile } = resolveProfile(
34
+ config,
35
+ stringFlag(args.flags, "profile"),
36
+ stringFlag(args.flags, "baseUrl"),
37
+ );
38
+ const outputMode = resolveOutputMode(profile, args.flags);
39
+ const client = new ClankmatesClient(profile);
40
+ const configPath = getConfigPath();
41
+
42
+ switch (keyCommand) {
43
+ case "list": {
44
+ const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
45
+ const response = await client.listAccessKeys(scope);
46
+
47
+ if (outputMode === "json") {
48
+ printJson(io, { items: response.items });
49
+ return;
50
+ }
51
+
52
+ printValue(
53
+ io,
54
+ outputMode,
55
+ response.items.map((item) => formatAccessKeyRow(item)),
56
+ );
57
+ return;
58
+ }
59
+
60
+ case "issue": {
61
+ const scope = requireAccessKeyScope(
62
+ requiredStringFlag(args.flags, "scope"),
63
+ );
64
+ const response = await client.issueAccessKey({
65
+ scope,
66
+ name: requiredStringFlag(args.flags, "name"),
67
+ });
68
+
69
+ if (booleanFlag(args.flags, "tokenOnly")) {
70
+ io.stdout(response.token);
71
+ return;
72
+ }
73
+
74
+ printValue(
75
+ io,
76
+ outputMode,
77
+ outputMode === "json"
78
+ ? response
79
+ : renderAccessKeyIssue("Issued owner access key", response),
80
+ );
81
+ return;
82
+ }
83
+
84
+ case "revoke": {
85
+ const response = await client.revokeAccessKey(
86
+ requiredPositional(args.positionals, 2, "Missing access key id"),
87
+ );
88
+ await pruneInvalidStoredOwnerTokens(
89
+ profileName,
90
+ profile,
91
+ client,
92
+ configPath,
93
+ );
94
+ printValue(
95
+ io,
96
+ outputMode,
97
+ outputMode === "json"
98
+ ? response
99
+ : renderAccessKeyRevoke("Revoked owner access key", response),
100
+ );
101
+ return;
102
+ }
103
+
104
+ default:
105
+ throw new CliError("Unknown auth key subcommand", 2);
106
+ }
107
+ }
108
+
109
+ function parseAccessKeyScope(
110
+ scope: string | undefined,
111
+ required: boolean,
112
+ ): AccessKeyScope | undefined {
113
+ if (scope === undefined) {
114
+ if (required) {
115
+ throw new CliError("Missing `--scope`", 2);
116
+ }
117
+
118
+ return undefined;
119
+ }
120
+
121
+ if (scope !== "master" && scope !== "read_only") {
122
+ throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
123
+ }
124
+
125
+ return scope;
126
+ }
127
+
128
+ function requireAccessKeyScope(scope: string): AccessKeyScope {
129
+ const parsed = parseAccessKeyScope(scope, true);
130
+
131
+ if (!parsed) {
132
+ throw new CliError("Missing `--scope`", 2);
133
+ }
134
+
135
+ return parsed;
136
+ }
137
+
138
+ function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
139
+ return {
140
+ id: item.id,
141
+ scope: item.attributes.scope,
142
+ name: item.attributes.name ?? "",
143
+ expiresAt: item.attributes.expires_at,
144
+ issuedAt: item.attributes.inserted_at ?? "",
145
+ };
146
+ }
147
+
148
+ function renderAccessKeyIssue(
149
+ title: string,
150
+ response: AccessKeyIssueResponse,
151
+ ): string {
152
+ return renderTokenAction({
153
+ title,
154
+ id: response.id,
155
+ name: response.name,
156
+ token: response.token,
157
+ issuedAt: response.issued_at,
158
+ expiresAt: response.expires_at,
159
+ });
160
+ }
161
+
162
+ function renderAccessKeyRevoke(
163
+ title: string,
164
+ response: AccessKeyRevokeResponse,
165
+ ): string {
166
+ return joinBlocks([
167
+ title,
168
+ renderFields([
169
+ ["ID", response.id],
170
+ ["Scope", response.scope],
171
+ ["Name", response.name],
172
+ ]),
173
+ ]);
174
+ }
175
+
176
+ async function pruneInvalidStoredOwnerTokens(
177
+ profileName: string,
178
+ profile: ProfileConfig,
179
+ client: ClankmatesClient,
180
+ configPath: string,
181
+ ): Promise<void> {
182
+ const invalidMasterToken = profile.masterToken
183
+ ? !(await client.canAuthenticate(profile.masterToken))
184
+ : false;
185
+ const invalidReadOnlyToken = profile.readOnlyToken
186
+ ? !(await client.canAuthenticate(profile.readOnlyToken))
187
+ : false;
188
+
189
+ if (!invalidMasterToken && !invalidReadOnlyToken) {
190
+ return;
191
+ }
192
+
193
+ await updateProfile(
194
+ profileName,
195
+ (storedProfile) => {
196
+ if (invalidMasterToken) {
197
+ storedProfile.masterToken = undefined;
198
+ }
199
+
200
+ if (invalidReadOnlyToken) {
201
+ storedProfile.readOnlyToken = undefined;
202
+ }
203
+ },
204
+ configPath,
205
+ );
206
+ }
@@ -2,7 +2,6 @@ import { resolveOutputMode } from "../lib/context";
2
2
  import {
3
3
  booleanFlag,
4
4
  requiredPositional,
5
- requiredStringFlag,
6
5
  stringFlag,
7
6
  type ParsedArgs,
8
7
  } from "../lib/args";
@@ -15,22 +14,15 @@ import {
15
14
  } from "../lib/config";
16
15
  import { ClankmatesClient } from "../lib/client";
17
16
  import { CliError } from "../lib/errors";
18
- import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
19
- import { printJson, printValue, type Io } from "../lib/output";
17
+ import { printValue, type Io } from "../lib/output";
20
18
  import { getConfigPath } from "../lib/paths";
21
19
  import {
22
20
  resolveMasterToken,
23
21
  resolveOwnerReadToken,
24
22
  resolveReadOnlyToken,
25
23
  } from "../lib/tokens";
26
- import type {
27
- AccessKeyAttributes,
28
- AccessKeyIssueResponse,
29
- AccessKeyRevokeResponse,
30
- AccessKeyScope,
31
- ProfileConfig,
32
- WhoamiResponse,
33
- } from "../types/api";
24
+ import type { ProfileConfig, WhoamiResponse } from "../types/api";
25
+ import { runAccessKeyCommand } from "./auth/access-keys";
34
26
 
35
27
  export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
36
28
  const subcommand = args.positionals[0];
@@ -196,92 +188,6 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
196
188
  }
197
189
  }
198
190
 
199
- async function runAccessKeyCommand(
200
- args: ParsedArgs,
201
- config: Awaited<ReturnType<typeof loadConfig>>,
202
- io: Io,
203
- ): Promise<void> {
204
- const keyCommand = requiredPositional(
205
- args.positionals,
206
- 1,
207
- "Missing auth key subcommand",
208
- );
209
- const { profileName, profile } = resolveProfile(
210
- config,
211
- stringFlag(args.flags, "profile"),
212
- stringFlag(args.flags, "baseUrl"),
213
- );
214
- const outputMode = resolveOutputMode(profile, args.flags);
215
- const client = new ClankmatesClient(profile);
216
- const configPath = getConfigPath();
217
-
218
- switch (keyCommand) {
219
- case "list": {
220
- const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
221
- const response = await client.listAccessKeys(scope);
222
-
223
- if (outputMode === "json") {
224
- printJson(io, { items: response.items });
225
- return;
226
- }
227
-
228
- printValue(
229
- io,
230
- outputMode,
231
- response.items.map((item) => formatAccessKeyRow(item)),
232
- );
233
- return;
234
- }
235
-
236
- case "issue": {
237
- const scope = requireAccessKeyScope(
238
- requiredStringFlag(args.flags, "scope"),
239
- );
240
- const response = await client.issueAccessKey({
241
- scope,
242
- name: requiredStringFlag(args.flags, "name"),
243
- });
244
-
245
- if (booleanFlag(args.flags, "tokenOnly")) {
246
- io.stdout(response.token);
247
- return;
248
- }
249
-
250
- printValue(
251
- io,
252
- outputMode,
253
- outputMode === "json"
254
- ? response
255
- : renderAccessKeyIssue("Issued owner access key", response),
256
- );
257
- return;
258
- }
259
-
260
- case "revoke": {
261
- const response = await client.revokeAccessKey(
262
- requiredPositional(args.positionals, 2, "Missing access key id"),
263
- );
264
- await pruneInvalidStoredOwnerTokens(
265
- profileName,
266
- profile,
267
- client,
268
- configPath,
269
- );
270
- printValue(
271
- io,
272
- outputMode,
273
- outputMode === "json"
274
- ? response
275
- : renderAccessKeyRevoke("Revoked owner access key", response),
276
- );
277
- return;
278
- }
279
-
280
- default:
281
- throw new CliError("Unknown auth key subcommand", 2);
282
- }
283
- }
284
-
285
191
  function defaultProfileConfig(): ProfileConfig {
286
192
  return {
287
193
  baseUrl: resolveBaseUrl(),
@@ -332,102 +238,3 @@ function formatWhoamiOutput(
332
238
  publicUrl: whoami.actor.public_url ?? "",
333
239
  };
334
240
  }
335
-
336
- function parseAccessKeyScope(
337
- scope: string | undefined,
338
- required: boolean,
339
- ): AccessKeyScope | undefined {
340
- if (scope === undefined) {
341
- if (required) {
342
- throw new CliError("Missing `--scope`", 2);
343
- }
344
-
345
- return undefined;
346
- }
347
-
348
- if (scope !== "master" && scope !== "read_only") {
349
- throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
350
- }
351
-
352
- return scope;
353
- }
354
-
355
- function requireAccessKeyScope(scope: string): AccessKeyScope {
356
- const parsed = parseAccessKeyScope(scope, true);
357
-
358
- if (!parsed) {
359
- throw new CliError("Missing `--scope`", 2);
360
- }
361
-
362
- return parsed;
363
- }
364
-
365
- function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
366
- return {
367
- id: item.id,
368
- scope: item.attributes.scope,
369
- name: item.attributes.name ?? "",
370
- expiresAt: item.attributes.expires_at,
371
- issuedAt: item.attributes.inserted_at ?? "",
372
- };
373
- }
374
-
375
- function renderAccessKeyIssue(
376
- title: string,
377
- response: AccessKeyIssueResponse,
378
- ): string {
379
- return renderTokenAction({
380
- title,
381
- id: response.id,
382
- name: response.name,
383
- token: response.token,
384
- issuedAt: response.issued_at,
385
- expiresAt: response.expires_at,
386
- });
387
- }
388
-
389
- function renderAccessKeyRevoke(
390
- title: string,
391
- response: AccessKeyRevokeResponse,
392
- ): string {
393
- return joinBlocks([
394
- title,
395
- renderFields([
396
- ["ID", response.id],
397
- ["Scope", response.scope],
398
- ["Name", response.name],
399
- ]),
400
- ]);
401
- }
402
-
403
- async function pruneInvalidStoredOwnerTokens(
404
- profileName: string,
405
- profile: ProfileConfig,
406
- client: ClankmatesClient,
407
- configPath: string,
408
- ): Promise<void> {
409
- const invalidMasterToken = profile.masterToken
410
- ? !(await client.canAuthenticate(profile.masterToken))
411
- : false;
412
- const invalidReadOnlyToken = profile.readOnlyToken
413
- ? !(await client.canAuthenticate(profile.readOnlyToken))
414
- : false;
415
-
416
- if (!invalidMasterToken && !invalidReadOnlyToken) {
417
- return;
418
- }
419
-
420
- await updateProfile(
421
- profileName,
422
- (storedProfile) => {
423
- if (invalidMasterToken) {
424
- storedProfile.masterToken = undefined;
425
- }
426
-
427
- if (invalidReadOnlyToken) {
428
- storedProfile.readOnlyToken = undefined;
429
- }
430
- },
431
- configPath,
432
- );
433
- }
@@ -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
+ }