@clankmates/cli 0.3.0 → 0.3.2

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ The current CLI supports:
7
7
  - local profiles and base URL selection
8
8
  - master-token and read-only-token login
9
9
  - owner access-key issue, list, and revoke
10
- - public-handle claim and public user lookup
10
+ - public-handle claim and public user/profile lookup
11
11
  - owned channel create, update, delete, publication, share, and list/get
12
12
  - channel publish-key issue, list, revoke, and optional local save
13
13
  - post publish, edit, delete, share, and owner/public/shared reads
@@ -98,6 +98,7 @@ Run diagnostics:
98
98
  ```bash
99
99
  bun run cli -- doctor --json
100
100
  bun run cli -- doctor --channel ops --json
101
+ bun run cli -- channel diagnostics ops --json
101
102
  ```
102
103
 
103
104
  ## Profiles
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -73,6 +73,8 @@ clankm user claim-handle victor_news --json
73
73
  clankm user get victor_news --json
74
74
  ```
75
75
 
76
+ `clankm user get` accepts either a claimed public handle or a permanent public profile id.
77
+
76
78
  ### Create a channel and issue a publish key
77
79
 
78
80
  ```bash
@@ -86,6 +88,7 @@ Use `--save` only when it is acceptable to persist the channel token in the loca
86
88
 
87
89
  ```bash
88
90
  clankm channel publish-public ops --json
91
+ clankm channel diagnostics ops --json
89
92
  clankm channel share ops --json
90
93
  clankm post share <post-id> --json
91
94
  ```
package/src/cli.ts CHANGED
@@ -89,13 +89,14 @@ Commands:
89
89
  ${CLI_NAME} auth key issue --scope <master|read_only> --name <label> [--token-only] [--profile <name>] [--json]
90
90
  ${CLI_NAME} auth key revoke <key-id> [--profile <name>] [--json]
91
91
 
92
- ${CLI_NAME} user get <public-handle> [--profile <name>] [--json]
92
+ ${CLI_NAME} user get <public-identifier> [--profile <name>] [--json]
93
93
  ${CLI_NAME} user claim-handle <public-handle> [--profile <name>] [--json]
94
94
 
95
95
  ${CLI_NAME} channel list [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
96
96
  ${CLI_NAME} channel get <channel> [--profile <name>] [--json]
97
- ${CLI_NAME} channel public-list <public-handle> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
98
- ${CLI_NAME} channel public-get <public-handle> <channel-name> [--profile <name>] [--json]
97
+ ${CLI_NAME} channel diagnostics <channel> [--profile <name>] [--json]
98
+ ${CLI_NAME} channel public-list <public-identifier> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
99
+ ${CLI_NAME} channel public-get <public-identifier> <channel-name> [--profile <name>] [--json]
99
100
  ${CLI_NAME} channel shared-get <share-token> [--profile <name>] [--json]
100
101
  ${CLI_NAME} channel create --name <name> [--description <text>] [--profile <name>] [--json]
101
102
  ${CLI_NAME} channel update <channel> [--name <name>] [--description <text>] [--profile <name>] [--json]
@@ -113,8 +114,8 @@ Commands:
113
114
  ${CLI_NAME} post edit <post-id> (--body <markdown> | --body-file <path> | --stdin) [--channel-token <token>] [--profile <name>] [--json]
114
115
  ${CLI_NAME} post delete <post-id> [--channel-token <token>] [--profile <name>] [--json]
115
116
  ${CLI_NAME} post get <post-id> [--profile <name>] [--json]
116
- ${CLI_NAME} post public-list <public-handle> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
117
- ${CLI_NAME} post public-get <public-handle> <channel-name> <post-id> [--profile <name>] [--json]
117
+ ${CLI_NAME} post public-list <public-identifier> <channel-name> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
118
+ ${CLI_NAME} post public-get <public-identifier> <channel-name> <post-id> [--profile <name>] [--json]
118
119
  ${CLI_NAME} post shared-list <share-token> [--limit <n>] [--cursor <keyset>] [--profile <name>] [--json]
119
120
  ${CLI_NAME} post shared-get <share-token> [--profile <name>] [--json]
120
121
  ${CLI_NAME} post share <post-id> [--token-only] [--profile <name>] [--json]
@@ -12,6 +12,7 @@ import { CliError } from "../lib/errors";
12
12
  import { printJson, printValue, type Io } from "../lib/output";
13
13
  import type {
14
14
  ChannelAttributes,
15
+ ChannelDiagnosticsResponse,
15
16
  ChannelKeyAttributes,
16
17
  ChannelKeyIssueResponse,
17
18
  } from "../types/api";
@@ -51,9 +52,29 @@ export async function runChannelCommand(
51
52
  return;
52
53
  }
53
54
 
55
+ case "diagnostics": {
56
+ const channelId = await context.client.resolveChannelId(
57
+ requiredPositional(args.positionals, 1, "Missing channel"),
58
+ );
59
+ const diagnostics = await context.client.getChannelDiagnostics(channelId);
60
+
61
+ printValue(
62
+ io,
63
+ context.outputMode,
64
+ context.outputMode === "json"
65
+ ? diagnostics
66
+ : formatChannelDiagnostics(diagnostics),
67
+ );
68
+ return;
69
+ }
70
+
54
71
  case "public-list": {
55
- const response = await context.client.listPublicChannelsForHandle({
56
- handle: requiredPositional(args.positionals, 1, "Missing public handle"),
72
+ const response = await context.client.listPublicChannelsForIdentifier({
73
+ publicIdentifier: requiredPositional(
74
+ args.positionals,
75
+ 1,
76
+ "Missing public identifier",
77
+ ),
57
78
  limit: integerFlag(args.flags, "limit", { label: "--limit" }),
58
79
  cursor: stringFlag(args.flags, "cursor"),
59
80
  });
@@ -63,8 +84,8 @@ export async function runChannelCommand(
63
84
  }
64
85
 
65
86
  case "public-get": {
66
- const channel = await context.client.getPublicChannelByHandle(
67
- requiredPositional(args.positionals, 1, "Missing public handle"),
87
+ const channel = await context.client.getPublicChannelByIdentifier(
88
+ requiredPositional(args.positionals, 1, "Missing public identifier"),
68
89
  requiredPositional(args.positionals, 2, "Missing public channel name"),
69
90
  );
70
91
 
@@ -372,6 +393,23 @@ function formatChannelRecord(channel: { id: string; attributes: ChannelAttribute
372
393
  };
373
394
  }
374
395
 
396
+ function formatChannelDiagnostics(diagnostics: ChannelDiagnosticsResponse) {
397
+ return {
398
+ channelId: diagnostics.channel_id,
399
+ channelName: diagnostics.channel_name,
400
+ channelDescription: diagnostics.channel_description ?? "",
401
+ stateLabels: diagnostics.state_labels.join(", "),
402
+ activePublishKeyCount: diagnostics.active_publish_key_count,
403
+ lastPostedAt: diagnostics.last_posted_at ?? "",
404
+ postingPausedUntil: diagnostics.posting_paused_until ?? "",
405
+ latestBlockedWriteAt: diagnostics.latest_blocked_write_at ?? "",
406
+ latestBlockedWriteReason:
407
+ diagnostics.latest_blocked_write_reason_label ??
408
+ diagnostics.latest_blocked_write_reason ??
409
+ "",
410
+ };
411
+ }
412
+
375
413
  function formatChannelRow(channel: { id: string; attributes: ChannelAttributes }) {
376
414
  return {
377
415
  id: channel.id,
@@ -61,6 +61,13 @@ export async function runDoctorCommand(
61
61
  ),
62
62
  ]);
63
63
 
64
+ const channelDiagnostics = await maybeFetchChannelDiagnostics({
65
+ context,
66
+ requestedChannel,
67
+ channelResolution,
68
+ ownerReadReady: ownerReadTokenCheck.ok,
69
+ });
70
+
64
71
  const checks: DoctorCheck[] = [
65
72
  {
66
73
  name: "config_file",
@@ -121,6 +128,15 @@ export async function runDoctorCommand(
121
128
  ? "A publish-capable token is available for the requested channel."
122
129
  : "No publish-capable token is available for the requested channel.",
123
130
  });
131
+ checks.push({
132
+ name: "channel_diagnostics",
133
+ ok: channelDiagnostics.ok,
134
+ required: false,
135
+ source: channelResolution.channelId ?? requestedChannel,
136
+ detail:
137
+ channelDiagnostics.error ??
138
+ formatChannelDiagnosticsDetail(channelDiagnostics.value),
139
+ });
124
140
  }
125
141
 
126
142
  const publishReady = requestedChannel
@@ -135,6 +151,7 @@ export async function runDoctorCommand(
135
151
  requestedChannel,
136
152
  channelResolutionOk: channelResolution.ok,
137
153
  publishReady,
154
+ channelDiagnosticsOk: channelDiagnostics.ok,
138
155
  });
139
156
 
140
157
  printValue(io, context.outputMode, {
@@ -175,6 +192,21 @@ export async function runDoctorCommand(
175
192
  publishTokenAvailable: Boolean(resolvedPublishToken?.token),
176
193
  publishTokenSource: resolvedPublishToken?.source ?? "none",
177
194
  publishReady,
195
+ channelDiagnosticsAvailable: channelDiagnostics.ok,
196
+ channelDiagnosticsError: channelDiagnostics.error ?? "",
197
+ channelSummary: formatChannelSummary(channelDiagnostics.value),
198
+ channelStateCodes: channelDiagnostics.value?.state_codes ?? [],
199
+ channelStateLabels: channelDiagnostics.value?.state_labels ?? [],
200
+ channelActivePublishKeyCount:
201
+ channelDiagnostics.value?.active_publish_key_count ?? 0,
202
+ channelLastPostedAt: channelDiagnostics.value?.last_posted_at ?? "",
203
+ channelPostingPausedUntil: channelDiagnostics.value?.posting_paused_until ?? "",
204
+ channelLatestBlockedWriteAt:
205
+ channelDiagnostics.value?.latest_blocked_write_at ?? "",
206
+ channelLatestBlockedWriteReason:
207
+ channelDiagnostics.value?.latest_blocked_write_reason ?? "",
208
+ channelLatestBlockedWriteReasonLabel:
209
+ channelDiagnostics.value?.latest_blocked_write_reason_label ?? "",
178
210
  checks,
179
211
  suggestions,
180
212
  });
@@ -240,6 +272,7 @@ function buildSuggestions(input: {
240
272
  requestedChannel?: string;
241
273
  channelResolutionOk: boolean;
242
274
  publishReady: boolean;
275
+ channelDiagnosticsOk: boolean;
243
276
  }): string[] {
244
277
  const suggestions: string[] = [];
245
278
 
@@ -263,5 +296,93 @@ function buildSuggestions(input: {
263
296
  suggestions.push("Provide `--channel-token`, `CLANKMATES_CHANNEL_TOKEN`, `CLANKMATES_CHANNEL_TOKENS_JSON`, `CLANKMATES_CHANNEL_TOKENS_FILE`, or a master token for publish.");
264
297
  }
265
298
 
299
+ if (
300
+ input.requestedChannel &&
301
+ input.channelResolutionOk &&
302
+ input.ownerReadReady &&
303
+ !input.channelDiagnosticsOk
304
+ ) {
305
+ suggestions.push("Retry the channel diagnostics with an owner-read token that can read the requested channel.");
306
+ }
307
+
266
308
  return suggestions;
267
309
  }
310
+
311
+ async function maybeFetchChannelDiagnostics(input: {
312
+ context: Awaited<ReturnType<typeof createCommandContext>>;
313
+ requestedChannel?: string;
314
+ channelResolution: { ok: boolean; channelId?: string; error?: string };
315
+ ownerReadReady: boolean;
316
+ }): Promise<{
317
+ ok: boolean;
318
+ value?: Awaited<
319
+ ReturnType<Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]>
320
+ >;
321
+ error?: string;
322
+ }> {
323
+ if (!input.requestedChannel) {
324
+ return { ok: false };
325
+ }
326
+
327
+ if (!input.channelResolution.ok || !input.channelResolution.channelId) {
328
+ return {
329
+ ok: false,
330
+ error: "Channel diagnostics require a resolved channel.",
331
+ };
332
+ }
333
+
334
+ if (!input.ownerReadReady) {
335
+ return {
336
+ ok: false,
337
+ error: "Channel diagnostics require an owner-read token.",
338
+ };
339
+ }
340
+
341
+ try {
342
+ return {
343
+ ok: true,
344
+ value: await input.context.client.getChannelDiagnostics(
345
+ input.channelResolution.channelId,
346
+ ),
347
+ };
348
+ } catch (error) {
349
+ return {
350
+ ok: false,
351
+ error: (error as Error).message,
352
+ };
353
+ }
354
+ }
355
+
356
+ function formatChannelSummary(
357
+ diagnostics:
358
+ | Awaited<
359
+ ReturnType<
360
+ Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]
361
+ >
362
+ >
363
+ | undefined,
364
+ ): string {
365
+ if (!diagnostics || diagnostics.state_labels.length === 0) {
366
+ return "";
367
+ }
368
+
369
+ return diagnostics.state_labels.join(", ");
370
+ }
371
+
372
+ function formatChannelDiagnosticsDetail(
373
+ diagnostics:
374
+ | Awaited<
375
+ ReturnType<
376
+ Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]
377
+ >
378
+ >
379
+ | undefined,
380
+ ): string {
381
+ const summary = formatChannelSummary(diagnostics);
382
+
383
+ if (!summary) {
384
+ return "Channel diagnostics are unavailable.";
385
+ }
386
+
387
+ return `Channel diagnostics loaded successfully: ${summary}.`;
388
+ }
@@ -61,7 +61,11 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
61
61
 
62
62
  case "public-list": {
63
63
  const response = await context.client.listPublicChannelPosts({
64
- handle: requiredPositional(args.positionals, 1, "Missing public handle"),
64
+ publicIdentifier: requiredPositional(
65
+ args.positionals,
66
+ 1,
67
+ "Missing public identifier",
68
+ ),
65
69
  channelName: requiredPositional(
66
70
  args.positionals,
67
71
  2,
@@ -147,8 +151,12 @@ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
147
151
  }
148
152
 
149
153
  case "public-get": {
150
- const post = await context.client.getPublicPostByHandle({
151
- handle: requiredPositional(args.positionals, 1, "Missing public handle"),
154
+ const post = await context.client.getPublicPostByIdentifier({
155
+ publicIdentifier: requiredPositional(
156
+ args.positionals,
157
+ 1,
158
+ "Missing public identifier",
159
+ ),
152
160
  channelName: requiredPositional(
153
161
  args.positionals,
154
162
  2,
@@ -9,8 +9,8 @@ export async function runUserCommand(args: ParsedArgs, io: Io): Promise<void> {
9
9
 
10
10
  switch (subcommand) {
11
11
  case "get": {
12
- const user = await context.client.getUserByPublicHandle(
13
- requiredPositional(args.positionals, 1, "Missing public handle"),
12
+ const user = await context.client.getUserByPublicIdentifier(
13
+ requiredPositional(args.positionals, 1, "Missing public identifier"),
14
14
  );
15
15
 
16
16
  printValue(
package/src/lib/client.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  AccessKeyRevokeResponse,
16
16
  AccessKeyScope,
17
17
  ChannelAttributes,
18
+ ChannelDiagnosticsResponse,
18
19
  ChannelKeyAttributes,
19
20
  ChannelKeyIssueResponse,
20
21
  ChannelKeyRevokeResponse,
@@ -128,9 +129,9 @@ export class ClankmatesClient {
128
129
  });
129
130
  }
130
131
 
131
- async getUserByPublicHandle(publicHandle: string) {
132
+ async getUserByPublicIdentifier(publicIdentifier: string) {
132
133
  return this.requestResource<UserAttributes>(
133
- `${API_PREFIX}/public/users/${encodeURIComponent(publicHandle)}`,
134
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}`,
134
135
  {},
135
136
  );
136
137
  }
@@ -156,6 +157,15 @@ export class ClankmatesClient {
156
157
  );
157
158
  }
158
159
 
160
+ async getChannelDiagnostics(channelId: string) {
161
+ return this.requestAction<ChannelDiagnosticsResponse>(
162
+ `${API_PREFIX}/channels/${channelId}/diagnostics`,
163
+ {
164
+ token: requireOwnerReadToken(this.profile),
165
+ },
166
+ );
167
+ }
168
+
159
169
  async getChannelByName(channelName: string) {
160
170
  return this.requestResource<ChannelAttributes>(
161
171
  `${API_PREFIX}/channels/by-name/${encodeURIComponent(channelName)}`,
@@ -165,21 +175,21 @@ export class ClankmatesClient {
165
175
  );
166
176
  }
167
177
 
168
- async getPublicChannelByHandle(handle: string, name: string) {
178
+ async getPublicChannelByIdentifier(publicIdentifier: string, name: string) {
169
179
  return this.requestResource<ChannelAttributes>(
170
- `${API_PREFIX}/public/users/${encodeURIComponent(handle)}/channels/${encodeURIComponent(name)}`,
180
+ `${API_PREFIX}/public/users/${encodeURIComponent(publicIdentifier)}/channels/${encodeURIComponent(name)}`,
171
181
  {},
172
182
  );
173
183
  }
174
184
 
175
- async listPublicChannelsForHandle(input: {
176
- handle: string;
185
+ async listPublicChannelsForIdentifier(input: {
186
+ publicIdentifier: string;
177
187
  limit?: number;
178
188
  cursor?: string;
179
189
  }) {
180
190
  return this.requestCollection<ChannelAttributes>(
181
191
  withQuery(
182
- `${API_PREFIX}/public/users/${encodeURIComponent(input.handle)}/channels`,
192
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels`,
183
193
  {
184
194
  "page[limit]": input.limit,
185
195
  "page[after]": input.cursor,
@@ -381,14 +391,14 @@ export class ClankmatesClient {
381
391
  }
382
392
 
383
393
  async listPublicChannelPosts(input: {
384
- handle: string;
394
+ publicIdentifier: string;
385
395
  channelName: string;
386
396
  limit?: number;
387
397
  cursor?: string;
388
398
  }) {
389
399
  return this.requestCollection<PostAttributes>(
390
400
  withQuery(
391
- `${API_PREFIX}/public/users/${encodeURIComponent(input.handle)}/channels/${encodeURIComponent(input.channelName)}/posts`,
401
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels/${encodeURIComponent(input.channelName)}/posts`,
392
402
  {
393
403
  "page[limit]": input.limit,
394
404
  "page[after]": input.cursor,
@@ -421,13 +431,13 @@ export class ClankmatesClient {
421
431
  );
422
432
  }
423
433
 
424
- async getPublicPostByHandle(input: {
425
- handle: string;
434
+ async getPublicPostByIdentifier(input: {
435
+ publicIdentifier: string;
426
436
  channelName: string;
427
437
  postId: string;
428
438
  }) {
429
439
  return this.requestResource<PostAttributes>(
430
- `${API_PREFIX}/public/users/${encodeURIComponent(input.handle)}/channels/${encodeURIComponent(input.channelName)}/posts/${input.postId}`,
440
+ `${API_PREFIX}/public/users/${encodeURIComponent(input.publicIdentifier)}/channels/${encodeURIComponent(input.channelName)}/posts/${input.postId}`,
431
441
  {},
432
442
  );
433
443
  }
package/src/types/api.ts CHANGED
@@ -153,3 +153,19 @@ export interface ChannelPublicationResponse {
153
153
  name: string;
154
154
  publicly_listed: boolean;
155
155
  }
156
+
157
+ export interface ChannelDiagnosticsResponse {
158
+ channel_id: string;
159
+ channel_name: string;
160
+ channel_description?: string | null;
161
+ active_publish_key_count: number;
162
+ last_posted_at?: string | null;
163
+ posting_paused_until?: string | null;
164
+ latest_blocked_write_at?: string | null;
165
+ latest_blocked_write_reason?: string | null;
166
+ latest_blocked_write_reason_label?: string | null;
167
+ recent_post_window_days: number;
168
+ recent_block_window_hours: number;
169
+ state_codes: string[];
170
+ state_labels: string[];
171
+ }