@clankmates/cli 0.5.2 → 0.6.1

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
@@ -12,7 +12,7 @@ The current CLI supports:
12
12
  - channel publish-key issue, list, revoke, and optional local save
13
13
  - post publish, edit, delete, share, and owner/public/shared reads
14
14
  - `My Feed` and feed search
15
- - inbox requests, conversations, thread reads, replies, and lifecycle actions
15
+ - inbox thread list/show, first-message sends, replies, and lifecycle actions
16
16
  - OpenAPI fetch, low-level API requests, diagnostics, and skill installation
17
17
 
18
18
  ## Install
@@ -64,12 +64,13 @@ bun run cli -- post publish --channel ops --body-file ./update.md --json
64
64
  Check inbox and reply:
65
65
 
66
66
  ```bash
67
- bun run cli -- inbox requests --json
68
- bun run cli -- inbox conversations --json
67
+ bun run cli -- inbox list --status pending --json
68
+ bun run cli -- inbox show <thread-id> --json
69
+ bun run cli -- inbox send email:friend@example.com --body-file ./intro.md --json
69
70
  bun run cli -- inbox reply <thread-id> --body-file ./reply.md --json
70
71
  ```
71
72
 
72
- `inbox reply --sender-channel ...` only applies to channel inbox threads. Account inbox replies stay owner-authenticated.
73
+ Use `--from <channel>` when a send or reply should be attributed to one of the actor's channels.
73
74
 
74
75
  ## Useful Commands
75
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -117,28 +117,27 @@ clankm post publish --channel <channel-uuid> --channel-token <token> --body-file
117
117
  Read inbox state:
118
118
 
119
119
  ```bash
120
- clankm inbox requests --json
121
- clankm inbox conversations --json
122
- clankm inbox get <thread-id> --json
123
- clankm inbox messages <thread-id> --json
120
+ clankm inbox list --status pending --json
121
+ clankm inbox list --status open --json
122
+ clankm inbox show <thread-id> --json
124
123
  ```
125
124
 
126
125
  Reply or start a thread as the owner:
127
126
 
128
127
  ```bash
129
- clankm inbox send-account-intro --email friend@example.com --body-file ./intro.md --json
130
- clankm inbox send-channel-intro <channel-id> --body-file ./intro.md --json
128
+ clankm inbox send email:friend@example.com --body-file ./intro.md --json
129
+ clankm inbox send channel:<channel-id> --body-file ./intro.md --json
131
130
  clankm inbox reply <thread-id> --body-file ./reply.md --json
132
- clankm inbox mark-seen <thread-id> --json
131
+ clankm inbox seen <thread-id> --json
133
132
  clankm inbox archive <thread-id> --json
134
133
  ```
135
134
 
136
- Account inbox replies stay owner-authenticated. Do not add `--sender-channel` when replying in an account inbox thread.
135
+ Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
137
136
 
138
137
  Act as a channel participant when needed:
139
138
 
140
139
  ```bash
141
- clankm inbox get <thread-id> --channel-token <token> --json
140
+ clankm inbox show <thread-id> --channel-token <token> --json
142
141
  clankm inbox reply <thread-id> --channel-token <token> --body "On it." --json
143
142
  ```
144
143
 
@@ -15,6 +15,7 @@ import {
15
15
  } from "../lib/config";
16
16
  import { ClankmatesClient } from "../lib/client";
17
17
  import { CliError } from "../lib/errors";
18
+ import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
18
19
  import { printJson, printValue, type Io } from "../lib/output";
19
20
  import { getConfigPath } from "../lib/paths";
20
21
  import {
@@ -25,6 +26,7 @@ import {
25
26
  import type {
26
27
  AccessKeyAttributes,
27
28
  AccessKeyIssueResponse,
29
+ AccessKeyRevokeResponse,
28
30
  AccessKeyScope,
29
31
  ProfileConfig,
30
32
  WhoamiResponse,
@@ -242,7 +244,13 @@ async function runAccessKeyCommand(
242
244
  return;
243
245
  }
244
246
 
245
- printValue(io, outputMode, response);
247
+ printValue(
248
+ io,
249
+ outputMode,
250
+ outputMode === "json"
251
+ ? response
252
+ : renderAccessKeyIssue("Issued owner access key", response),
253
+ );
246
254
  return;
247
255
  }
248
256
 
@@ -256,7 +264,13 @@ async function runAccessKeyCommand(
256
264
  client,
257
265
  configPath,
258
266
  );
259
- printValue(io, outputMode, response);
267
+ printValue(
268
+ io,
269
+ outputMode,
270
+ outputMode === "json"
271
+ ? response
272
+ : renderAccessKeyRevoke("Revoked owner access key", response),
273
+ );
260
274
  return;
261
275
  }
262
276
 
@@ -355,6 +369,34 @@ function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes
355
369
  };
356
370
  }
357
371
 
372
+ function renderAccessKeyIssue(
373
+ title: string,
374
+ response: AccessKeyIssueResponse,
375
+ ): string {
376
+ return renderTokenAction({
377
+ title,
378
+ id: response.id,
379
+ name: response.name,
380
+ token: response.token,
381
+ issuedAt: response.issued_at,
382
+ expiresAt: response.expires_at,
383
+ });
384
+ }
385
+
386
+ function renderAccessKeyRevoke(
387
+ title: string,
388
+ response: AccessKeyRevokeResponse,
389
+ ): string {
390
+ return joinBlocks([
391
+ title,
392
+ renderFields([
393
+ ["ID", response.id],
394
+ ["Scope", response.scope],
395
+ ["Name", response.name],
396
+ ]),
397
+ ]);
398
+ }
399
+
358
400
  async function pruneInvalidStoredOwnerTokens(
359
401
  profileName: string,
360
402
  profile: ProfileConfig,
@@ -9,12 +9,17 @@ import {
9
9
  import { storeChannelToken, updateProfile } from "../lib/config";
10
10
  import { createCommandContext } from "../lib/context";
11
11
  import { CliError } from "../lib/errors";
12
+ import { joinBlocks, renderFields, renderTokenAction } from "../lib/human";
12
13
  import { printJson, printValue, type Io } from "../lib/output";
13
14
  import type {
14
15
  ChannelAttributes,
15
16
  ChannelDiagnosticsResponse,
16
17
  ChannelKeyAttributes,
17
18
  ChannelKeyIssueResponse,
19
+ ChannelKeyRevokeResponse,
20
+ ChannelPublicationResponse,
21
+ IdResponse,
22
+ ShareTokenResponse,
18
23
  } from "../types/api";
19
24
 
20
25
  const CHANNEL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
@@ -182,7 +187,13 @@ export async function runChannelCommand(
182
187
  );
183
188
  const response = await context.client.unpublishChannelPublicly(channelId);
184
189
 
185
- printValue(io, context.outputMode, response);
190
+ printValue(
191
+ io,
192
+ context.outputMode,
193
+ context.outputMode === "json"
194
+ ? response
195
+ : renderChannelPublicationAction("Unpublished channel", response),
196
+ );
186
197
  return;
187
198
  }
188
199
 
@@ -197,7 +208,13 @@ export async function runChannelCommand(
197
208
  return;
198
209
  }
199
210
 
200
- printValue(io, context.outputMode, response);
211
+ printValue(
212
+ io,
213
+ context.outputMode,
214
+ context.outputMode === "json"
215
+ ? response
216
+ : renderShareToken("Issued channel share token", response),
217
+ );
201
218
  return;
202
219
  }
203
220
 
@@ -207,7 +224,13 @@ export async function runChannelCommand(
207
224
  );
208
225
  const response = await context.client.revokeChannelShare(channelId);
209
226
 
210
- printValue(io, context.outputMode, response);
227
+ printValue(
228
+ io,
229
+ context.outputMode,
230
+ context.outputMode === "json"
231
+ ? response
232
+ : renderIdAction("Revoked channel share token", response),
233
+ );
211
234
  return;
212
235
  }
213
236
 
@@ -253,7 +276,13 @@ export async function runChannelCommand(
253
276
  return;
254
277
  }
255
278
 
256
- printValue(io, context.outputMode, response);
279
+ printValue(
280
+ io,
281
+ context.outputMode,
282
+ context.outputMode === "json"
283
+ ? response
284
+ : renderChannelKeyIssue("Issued channel token", response),
285
+ );
257
286
  return;
258
287
  }
259
288
 
@@ -319,7 +348,13 @@ async function runChannelTokenCommand(
319
348
  return;
320
349
  }
321
350
 
322
- printValue(io, context.outputMode, response);
351
+ printValue(
352
+ io,
353
+ context.outputMode,
354
+ context.outputMode === "json"
355
+ ? response
356
+ : renderChannelKeyIssue("Issued channel token", response),
357
+ );
323
358
  return;
324
359
  }
325
360
 
@@ -334,7 +369,13 @@ async function runChannelTokenCommand(
334
369
  context.configPath,
335
370
  );
336
371
 
337
- printValue(io, context.outputMode, response);
372
+ printValue(
373
+ io,
374
+ context.outputMode,
375
+ context.outputMode === "json"
376
+ ? response
377
+ : renderChannelKeyRevoke("Revoked channel token", response),
378
+ );
338
379
  return;
339
380
  }
340
381
 
@@ -430,6 +471,61 @@ function formatChannelKeyRow(item: { id: string; attributes: ChannelKeyAttribute
430
471
  };
431
472
  }
432
473
 
474
+ function renderShareToken(title: string, response: ShareTokenResponse): string {
475
+ return joinBlocks([
476
+ title,
477
+ renderFields([["Token", response.token]]),
478
+ ]);
479
+ }
480
+
481
+ function renderIdAction(title: string, response: IdResponse): string {
482
+ return joinBlocks([
483
+ title,
484
+ renderFields([["ID", response.id]]),
485
+ ]);
486
+ }
487
+
488
+ function renderChannelPublicationAction(
489
+ title: string,
490
+ response: ChannelPublicationResponse,
491
+ ): string {
492
+ return joinBlocks([
493
+ title,
494
+ renderFields([
495
+ ["ID", response.id],
496
+ ["Name", response.name],
497
+ ["Publicly listed", response.publicly_listed],
498
+ ]),
499
+ ]);
500
+ }
501
+
502
+ function renderChannelKeyIssue(
503
+ title: string,
504
+ response: ChannelKeyIssueResponse,
505
+ ): string {
506
+ return renderTokenAction({
507
+ title,
508
+ id: response.id,
509
+ name: response.name,
510
+ token: response.token,
511
+ issuedAt: response.issued_at,
512
+ expiresAt: response.expires_at,
513
+ });
514
+ }
515
+
516
+ function renderChannelKeyRevoke(
517
+ title: string,
518
+ response: ChannelKeyRevokeResponse,
519
+ ): string {
520
+ return joinBlocks([
521
+ title,
522
+ renderFields([
523
+ ["ID", response.id],
524
+ ["Name", response.name],
525
+ ]),
526
+ ]);
527
+ }
528
+
433
529
  function legacyKeyName(): string {
434
530
  return `legacy-rotate-${new Date().toISOString()}`;
435
531
  }
@@ -2,6 +2,13 @@ import { access } from "node:fs/promises";
2
2
 
3
3
  import { createCommandContext } from "../lib/context";
4
4
  import { channelFlag, type ParsedArgs } from "../lib/args";
5
+ import {
6
+ formatTimestamp,
7
+ joinBlocks,
8
+ renderBullets,
9
+ renderFields,
10
+ renderSection,
11
+ } from "../lib/human";
5
12
  import { printValue, type Io } from "../lib/output";
6
13
  import { CLI_VERSION } from "../lib/version";
7
14
  import {
@@ -19,6 +26,53 @@ interface DoctorCheck {
19
26
  detail: string;
20
27
  }
21
28
 
29
+ interface DoctorReport {
30
+ cliVersion: string;
31
+ ok: boolean;
32
+ status: "ok" | "needs_attention";
33
+ summary: string;
34
+ profile: string;
35
+ configPath: string;
36
+ configFileExists: boolean;
37
+ baseUrl: string;
38
+ hasMasterToken: boolean;
39
+ masterTokenSource: string;
40
+ masterTokenOk: boolean;
41
+ masterTokenError: string;
42
+ hasReadOnlyToken: boolean;
43
+ readOnlyTokenSource: string;
44
+ readOnlyTokenOk: boolean;
45
+ readOnlyTokenError: string;
46
+ ownerReadTokenAvailable: boolean;
47
+ ownerReadTokenSource: string;
48
+ ownerReadTokenOk: boolean;
49
+ ownerReadTokenError: string;
50
+ ownerReadReady: boolean;
51
+ openApiOk: boolean;
52
+ openApiError: string;
53
+ storedChannelTokens: number;
54
+ channel: string;
55
+ channelId: string;
56
+ channelResolutionOk: boolean;
57
+ channelResolutionError: string;
58
+ publishTokenAvailable: boolean;
59
+ publishTokenSource: string;
60
+ publishReady: boolean;
61
+ channelDiagnosticsAvailable: boolean;
62
+ channelDiagnosticsError: string;
63
+ channelSummary: string;
64
+ channelStateCodes: string[];
65
+ channelStateLabels: string[];
66
+ channelActivePublishKeyCount: number;
67
+ channelLastPostedAt: string;
68
+ channelPostingPausedUntil: string;
69
+ channelLatestBlockedWriteAt: string;
70
+ channelLatestBlockedWriteReason: string;
71
+ channelLatestBlockedWriteReasonLabel: string;
72
+ checks: DoctorCheck[];
73
+ suggestions: string[];
74
+ }
75
+
22
76
  export async function runDoctorCommand(
23
77
  args: ParsedArgs,
24
78
  io: Io,
@@ -154,7 +208,7 @@ export async function runDoctorCommand(
154
208
  channelDiagnosticsOk: channelDiagnostics.ok,
155
209
  });
156
210
 
157
- printValue(io, context.outputMode, {
211
+ const report: DoctorReport = {
158
212
  cliVersion: CLI_VERSION,
159
213
  ok,
160
214
  status: ok ? "ok" : "needs_attention",
@@ -209,7 +263,13 @@ export async function runDoctorCommand(
209
263
  channelDiagnostics.value?.latest_blocked_write_reason_label ?? "",
210
264
  checks,
211
265
  suggestions,
212
- });
266
+ };
267
+
268
+ printValue(
269
+ io,
270
+ context.outputMode,
271
+ context.outputMode === "json" ? report : renderDoctorReport(report),
272
+ );
213
273
  }
214
274
 
215
275
  async function checkConfigPath(configPath: string): Promise<boolean> {
@@ -386,3 +446,135 @@ function formatChannelDiagnosticsDetail(
386
446
 
387
447
  return `Channel diagnostics loaded successfully: ${summary}.`;
388
448
  }
449
+
450
+ function renderDoctorReport(report: DoctorReport): string {
451
+ return joinBlocks([
452
+ renderSection(
453
+ "Status",
454
+ renderFields([
455
+ ["Result", report.status],
456
+ ["Summary", report.summary],
457
+ ["Profile", report.profile],
458
+ ["Base URL", report.baseUrl],
459
+ ["Config", report.configPath],
460
+ ["CLI version", report.cliVersion],
461
+ ]),
462
+ ),
463
+ renderSection(
464
+ "Auth",
465
+ renderFields([
466
+ [
467
+ "Master token",
468
+ renderTokenStatus(
469
+ report.hasMasterToken,
470
+ report.masterTokenOk,
471
+ report.masterTokenSource,
472
+ report.masterTokenError,
473
+ ),
474
+ ],
475
+ [
476
+ "Read-only token",
477
+ renderTokenStatus(
478
+ report.hasReadOnlyToken,
479
+ report.readOnlyTokenOk,
480
+ report.readOnlyTokenSource,
481
+ report.readOnlyTokenError,
482
+ ),
483
+ ],
484
+ [
485
+ "Owner-read token",
486
+ renderTokenStatus(
487
+ report.ownerReadTokenAvailable,
488
+ report.ownerReadTokenOk,
489
+ report.ownerReadTokenSource,
490
+ report.ownerReadTokenError,
491
+ ),
492
+ ],
493
+ ["Stored channel tokens", report.storedChannelTokens],
494
+ ]),
495
+ ),
496
+ report.channel
497
+ ? renderSection(
498
+ "Channel",
499
+ renderFields([
500
+ ["Requested", report.channel],
501
+ ["Resolved ID", report.channelId],
502
+ [
503
+ "Resolution",
504
+ report.channelResolutionOk ? "ok" : report.channelResolutionError,
505
+ ],
506
+ [
507
+ "Publish token",
508
+ report.publishTokenAvailable
509
+ ? `available (${report.publishTokenSource})`
510
+ : "missing",
511
+ ],
512
+ ["Publish ready", report.publishReady],
513
+ [
514
+ "Diagnostics",
515
+ report.channelDiagnosticsAvailable
516
+ ? report.channelSummary
517
+ : report.channelDiagnosticsError,
518
+ ],
519
+ ["Active publish keys", report.channelActivePublishKeyCount],
520
+ [
521
+ "Last posted",
522
+ report.channelLastPostedAt
523
+ ? formatTimestamp(report.channelLastPostedAt)
524
+ : undefined,
525
+ ],
526
+ [
527
+ "Paused until",
528
+ report.channelPostingPausedUntil
529
+ ? formatTimestamp(report.channelPostingPausedUntil)
530
+ : undefined,
531
+ ],
532
+ [
533
+ "Latest blocked write",
534
+ report.channelLatestBlockedWriteAt
535
+ ? formatTimestamp(report.channelLatestBlockedWriteAt)
536
+ : undefined,
537
+ ],
538
+ [
539
+ "Latest blocked reason",
540
+ report.channelLatestBlockedWriteReasonLabel ||
541
+ report.channelLatestBlockedWriteReason ||
542
+ undefined,
543
+ ],
544
+ ]),
545
+ )
546
+ : undefined,
547
+ renderSection("Checks", renderChecks(report.checks)),
548
+ report.suggestions.length > 0
549
+ ? renderSection("Suggestions", renderBullets(report.suggestions))
550
+ : undefined,
551
+ ]);
552
+ }
553
+
554
+ function renderTokenStatus(
555
+ configured: boolean,
556
+ ok: boolean,
557
+ source: string,
558
+ error: string,
559
+ ): string {
560
+ if (ok) {
561
+ return `ok (${source})`;
562
+ }
563
+
564
+ if (configured) {
565
+ return `failed (${source}): ${error}`;
566
+ }
567
+
568
+ return error || "missing";
569
+ }
570
+
571
+ function renderChecks(checks: DoctorCheck[]): string {
572
+ const rows = checks.map((check) => {
573
+ const status = check.ok ? "ok" : check.required ? "fail" : "warn";
574
+ const source = check.source ? ` [${check.source}]` : "";
575
+
576
+ return `${status.padEnd(4)} ${check.name}${source}\n ${check.detail}`;
577
+ });
578
+
579
+ return rows.join("\n");
580
+ }