@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
@@ -0,0 +1,140 @@
1
+ import {
2
+ formatTimestamp,
3
+ joinBlocks,
4
+ renderBullets,
5
+ renderFields,
6
+ renderSection,
7
+ } from "../../lib/human";
8
+ import type { DoctorCheck, DoctorReport } from "./types";
9
+
10
+ export function renderDoctorReport(report: DoctorReport): string {
11
+ return joinBlocks([
12
+ renderSection(
13
+ "Status",
14
+ renderFields([
15
+ ["Result", report.status],
16
+ ["Summary", report.summary],
17
+ ["Profile", report.profile],
18
+ ["Base URL", report.baseUrl],
19
+ ["Config", report.configPath],
20
+ ["CLI version", report.cliVersion],
21
+ ]),
22
+ ),
23
+ renderSection(
24
+ "Auth",
25
+ renderFields([
26
+ [
27
+ "Master token",
28
+ renderTokenStatus(
29
+ report.hasMasterToken,
30
+ report.masterTokenOk,
31
+ report.masterTokenSource,
32
+ report.masterTokenError,
33
+ ),
34
+ ],
35
+ [
36
+ "Read-only token",
37
+ renderTokenStatus(
38
+ report.hasReadOnlyToken,
39
+ report.readOnlyTokenOk,
40
+ report.readOnlyTokenSource,
41
+ report.readOnlyTokenError,
42
+ ),
43
+ ],
44
+ [
45
+ "Owner-read token",
46
+ renderTokenStatus(
47
+ report.ownerReadTokenAvailable,
48
+ report.ownerReadTokenOk,
49
+ report.ownerReadTokenSource,
50
+ report.ownerReadTokenError,
51
+ ),
52
+ ],
53
+ ["Stored channel tokens", report.storedChannelTokens],
54
+ ]),
55
+ ),
56
+ report.channel
57
+ ? renderSection(
58
+ "Channel",
59
+ renderFields([
60
+ ["Requested", report.channel],
61
+ ["Resolved ID", report.channelId],
62
+ [
63
+ "Resolution",
64
+ report.channelResolutionOk ? "ok" : report.channelResolutionError,
65
+ ],
66
+ [
67
+ "Publish token",
68
+ report.publishTokenAvailable
69
+ ? `available (${report.publishTokenSource})`
70
+ : "missing",
71
+ ],
72
+ ["Publish ready", report.publishReady],
73
+ [
74
+ "Diagnostics",
75
+ report.channelDiagnosticsAvailable
76
+ ? report.channelSummary
77
+ : report.channelDiagnosticsError,
78
+ ],
79
+ ["Active publish keys", report.channelActivePublishKeyCount],
80
+ [
81
+ "Last posted",
82
+ report.channelLastPostedAt
83
+ ? formatTimestamp(report.channelLastPostedAt)
84
+ : undefined,
85
+ ],
86
+ [
87
+ "Paused until",
88
+ report.channelPostingPausedUntil
89
+ ? formatTimestamp(report.channelPostingPausedUntil)
90
+ : undefined,
91
+ ],
92
+ [
93
+ "Latest blocked write",
94
+ report.channelLatestBlockedWriteAt
95
+ ? formatTimestamp(report.channelLatestBlockedWriteAt)
96
+ : undefined,
97
+ ],
98
+ [
99
+ "Latest blocked reason",
100
+ report.channelLatestBlockedWriteReasonLabel ||
101
+ report.channelLatestBlockedWriteReason ||
102
+ undefined,
103
+ ],
104
+ ]),
105
+ )
106
+ : undefined,
107
+ renderSection("Checks", renderChecks(report.checks)),
108
+ report.suggestions.length > 0
109
+ ? renderSection("Suggestions", renderBullets(report.suggestions))
110
+ : undefined,
111
+ ]);
112
+ }
113
+
114
+ function renderTokenStatus(
115
+ configured: boolean,
116
+ ok: boolean,
117
+ source: string,
118
+ error: string,
119
+ ): string {
120
+ if (ok) {
121
+ return `ok (${source})`;
122
+ }
123
+
124
+ if (configured) {
125
+ return `failed (${source}): ${error}`;
126
+ }
127
+
128
+ return error || "missing";
129
+ }
130
+
131
+ function renderChecks(checks: DoctorCheck[]): string {
132
+ const rows = checks.map((check) => {
133
+ const status = check.ok ? "ok" : check.required ? "fail" : "warn";
134
+ const source = check.source ? ` [${check.source}]` : "";
135
+
136
+ return `${status.padEnd(4)} ${check.name}${source}\n ${check.detail}`;
137
+ });
138
+
139
+ return rows.join("\n");
140
+ }
@@ -0,0 +1,42 @@
1
+ export function buildSuggestions(input: {
2
+ configFileExists: boolean;
3
+ openApiOk: boolean;
4
+ ownerReadReady: boolean;
5
+ requestedChannel?: string;
6
+ channelResolutionOk: boolean;
7
+ publishReady: boolean;
8
+ channelDiagnosticsOk: boolean;
9
+ }): string[] {
10
+ const suggestions: string[] = [];
11
+
12
+ if (!input.configFileExists) {
13
+ suggestions.push("Run `clankm config init` to create local config.");
14
+ }
15
+
16
+ if (!input.openApiOk) {
17
+ suggestions.push("Check `CLANKMATES_BASE_URL` or `--base-url`, then retry `clankm doctor --json`.");
18
+ }
19
+
20
+ if (!input.ownerReadReady) {
21
+ suggestions.push("Configure a read-only or master token for owner reads with `clankm auth login ...`.");
22
+ }
23
+
24
+ if (input.requestedChannel && !input.channelResolutionOk) {
25
+ suggestions.push("Use a channel UUID or configure an owner-read token so channel names can be resolved.");
26
+ }
27
+
28
+ if (input.requestedChannel && !input.publishReady) {
29
+ suggestions.push("Provide `--channel-token`, `CLANKMATES_CHANNEL_TOKEN`, `CLANKMATES_CHANNEL_TOKENS_JSON`, `CLANKMATES_CHANNEL_TOKENS_FILE`, or a master token for publish.");
30
+ }
31
+
32
+ if (
33
+ input.requestedChannel &&
34
+ input.channelResolutionOk &&
35
+ input.ownerReadReady &&
36
+ !input.channelDiagnosticsOk
37
+ ) {
38
+ suggestions.push("Retry the channel diagnostics with an owner-read token that can read the requested channel.");
39
+ }
40
+
41
+ return suggestions;
42
+ }
@@ -0,0 +1,75 @@
1
+ import type { createCommandContext } from "../../lib/context";
2
+
3
+ export interface DoctorCheck {
4
+ name: string;
5
+ ok: boolean;
6
+ required: boolean;
7
+ source?: string;
8
+ detail: string;
9
+ }
10
+
11
+ export interface DoctorReport {
12
+ cliVersion: string;
13
+ ok: boolean;
14
+ status: "ok" | "needs_attention";
15
+ summary: string;
16
+ profile: string;
17
+ configPath: string;
18
+ configFileExists: boolean;
19
+ baseUrl: string;
20
+ hasMasterToken: boolean;
21
+ masterTokenSource: string;
22
+ masterTokenOk: boolean;
23
+ masterTokenError: string;
24
+ hasReadOnlyToken: boolean;
25
+ readOnlyTokenSource: string;
26
+ readOnlyTokenOk: boolean;
27
+ readOnlyTokenError: string;
28
+ ownerReadTokenAvailable: boolean;
29
+ ownerReadTokenSource: string;
30
+ ownerReadTokenOk: boolean;
31
+ ownerReadTokenError: string;
32
+ ownerReadReady: boolean;
33
+ openApiOk: boolean;
34
+ openApiError: string;
35
+ storedChannelTokens: number;
36
+ channel: string;
37
+ channelId: string;
38
+ channelResolutionOk: boolean;
39
+ channelResolutionError: string;
40
+ publishTokenAvailable: boolean;
41
+ publishTokenSource: string;
42
+ publishReady: boolean;
43
+ channelDiagnosticsAvailable: boolean;
44
+ channelDiagnosticsError: string;
45
+ channelSummary: string;
46
+ channelStateCodes: string[];
47
+ channelStateLabels: string[];
48
+ channelActivePublishKeyCount: number;
49
+ channelLastPostedAt: string;
50
+ channelPostingPausedUntil: string;
51
+ channelLatestBlockedWriteAt: string;
52
+ channelLatestBlockedWriteReason: string;
53
+ channelLatestBlockedWriteReasonLabel: string;
54
+ checks: DoctorCheck[];
55
+ suggestions: string[];
56
+ }
57
+
58
+ export type DoctorContext = Awaited<ReturnType<typeof createCommandContext>>;
59
+
60
+ export type ChannelDiagnostics = Awaited<
61
+ ReturnType<DoctorContext["client"]["getChannelDiagnostics"]>
62
+ >;
63
+
64
+ export interface CheckResult {
65
+ ok: boolean;
66
+ error?: string;
67
+ }
68
+
69
+ export interface ChannelResolution extends CheckResult {
70
+ channelId?: string;
71
+ }
72
+
73
+ export interface ChannelDiagnosticsResult extends CheckResult {
74
+ value?: ChannelDiagnostics;
75
+ }
@@ -1,14 +1,5 @@
1
- import { access } from "node:fs/promises";
2
-
3
1
  import { createCommandContext } from "../lib/context";
4
2
  import { channelFlag, type ParsedArgs } from "../lib/args";
5
- import {
6
- formatTimestamp,
7
- joinBlocks,
8
- renderBullets,
9
- renderFields,
10
- renderSection,
11
- } from "../lib/human";
12
3
  import { printValue, type Io } from "../lib/output";
13
4
  import { CLI_VERSION } from "../lib/version";
14
5
  import {
@@ -17,61 +8,18 @@ import {
17
8
  resolvePublishToken,
18
9
  resolveReadOnlyToken,
19
10
  } from "../lib/tokens";
20
-
21
- interface DoctorCheck {
22
- name: string;
23
- ok: boolean;
24
- required: boolean;
25
- source?: string;
26
- detail: string;
27
- }
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
- }
11
+ import {
12
+ checkConfigPath,
13
+ formatChannelDiagnosticsDetail,
14
+ formatChannelSummary,
15
+ maybeFetchChannelDiagnostics,
16
+ resolveRequestedChannel,
17
+ runCheck,
18
+ runOptionalCheck,
19
+ } from "./doctor/checks";
20
+ import { renderDoctorReport } from "./doctor/render";
21
+ import { buildSuggestions } from "./doctor/suggestions";
22
+ import type { DoctorCheck, DoctorReport } from "./doctor/types";
75
23
 
76
24
  export async function runDoctorCommand(
77
25
  args: ParsedArgs,
@@ -271,310 +219,3 @@ export async function runDoctorCommand(
271
219
  context.outputMode === "json" ? report : renderDoctorReport(report),
272
220
  );
273
221
  }
274
-
275
- async function checkConfigPath(configPath: string): Promise<boolean> {
276
- try {
277
- await access(configPath);
278
- return true;
279
- } catch (error) {
280
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
281
- return false;
282
- }
283
-
284
- return false;
285
- }
286
- }
287
-
288
- async function resolveRequestedChannel(
289
- context: Awaited<ReturnType<typeof createCommandContext>>,
290
- channel: string,
291
- ): Promise<{ ok: boolean; channelId?: string; error?: string }> {
292
- try {
293
- return {
294
- ok: true,
295
- channelId: await context.client.resolveChannelId(channel),
296
- };
297
- } catch (error) {
298
- return {
299
- ok: false,
300
- error: (error as Error).message,
301
- };
302
- }
303
- }
304
-
305
- async function runOptionalCheck(
306
- token: string | undefined,
307
- operation: (token: string) => Promise<unknown>,
308
- missingMessage: string,
309
- ): Promise<{ ok: boolean; error?: string }> {
310
- if (!token) {
311
- return { ok: false, error: missingMessage };
312
- }
313
-
314
- return runCheck(() => operation(token));
315
- }
316
-
317
- async function runCheck(
318
- operation: () => Promise<unknown>,
319
- ): Promise<{ ok: boolean; error?: string }> {
320
- try {
321
- await operation();
322
- return { ok: true };
323
- } catch (error) {
324
- return { ok: false, error: (error as Error).message };
325
- }
326
- }
327
-
328
- function buildSuggestions(input: {
329
- configFileExists: boolean;
330
- openApiOk: boolean;
331
- ownerReadReady: boolean;
332
- requestedChannel?: string;
333
- channelResolutionOk: boolean;
334
- publishReady: boolean;
335
- channelDiagnosticsOk: boolean;
336
- }): string[] {
337
- const suggestions: string[] = [];
338
-
339
- if (!input.configFileExists) {
340
- suggestions.push("Run `clankm config init` to create local config.");
341
- }
342
-
343
- if (!input.openApiOk) {
344
- suggestions.push("Check `CLANKMATES_BASE_URL` or `--base-url`, then retry `clankm doctor --json`.");
345
- }
346
-
347
- if (!input.ownerReadReady) {
348
- suggestions.push("Configure a read-only or master token for owner reads with `clankm auth login ...`.");
349
- }
350
-
351
- if (input.requestedChannel && !input.channelResolutionOk) {
352
- suggestions.push("Use a channel UUID or configure an owner-read token so channel names can be resolved.");
353
- }
354
-
355
- if (input.requestedChannel && !input.publishReady) {
356
- suggestions.push("Provide `--channel-token`, `CLANKMATES_CHANNEL_TOKEN`, `CLANKMATES_CHANNEL_TOKENS_JSON`, `CLANKMATES_CHANNEL_TOKENS_FILE`, or a master token for publish.");
357
- }
358
-
359
- if (
360
- input.requestedChannel &&
361
- input.channelResolutionOk &&
362
- input.ownerReadReady &&
363
- !input.channelDiagnosticsOk
364
- ) {
365
- suggestions.push("Retry the channel diagnostics with an owner-read token that can read the requested channel.");
366
- }
367
-
368
- return suggestions;
369
- }
370
-
371
- async function maybeFetchChannelDiagnostics(input: {
372
- context: Awaited<ReturnType<typeof createCommandContext>>;
373
- requestedChannel?: string;
374
- channelResolution: { ok: boolean; channelId?: string; error?: string };
375
- ownerReadReady: boolean;
376
- }): Promise<{
377
- ok: boolean;
378
- value?: Awaited<
379
- ReturnType<Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]>
380
- >;
381
- error?: string;
382
- }> {
383
- if (!input.requestedChannel) {
384
- return { ok: false };
385
- }
386
-
387
- if (!input.channelResolution.ok || !input.channelResolution.channelId) {
388
- return {
389
- ok: false,
390
- error: "Channel diagnostics require a resolved channel.",
391
- };
392
- }
393
-
394
- if (!input.ownerReadReady) {
395
- return {
396
- ok: false,
397
- error: "Channel diagnostics require an owner-read token.",
398
- };
399
- }
400
-
401
- try {
402
- return {
403
- ok: true,
404
- value: await input.context.client.getChannelDiagnostics(
405
- input.channelResolution.channelId,
406
- ),
407
- };
408
- } catch (error) {
409
- return {
410
- ok: false,
411
- error: (error as Error).message,
412
- };
413
- }
414
- }
415
-
416
- function formatChannelSummary(
417
- diagnostics:
418
- | Awaited<
419
- ReturnType<
420
- Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]
421
- >
422
- >
423
- | undefined,
424
- ): string {
425
- if (!diagnostics || diagnostics.state_labels.length === 0) {
426
- return "";
427
- }
428
-
429
- return diagnostics.state_labels.join(", ");
430
- }
431
-
432
- function formatChannelDiagnosticsDetail(
433
- diagnostics:
434
- | Awaited<
435
- ReturnType<
436
- Awaited<ReturnType<typeof createCommandContext>>["client"]["getChannelDiagnostics"]
437
- >
438
- >
439
- | undefined,
440
- ): string {
441
- const summary = formatChannelSummary(diagnostics);
442
-
443
- if (!summary) {
444
- return "Channel diagnostics are unavailable.";
445
- }
446
-
447
- return `Channel diagnostics loaded successfully: ${summary}.`;
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
- }