@clankid/cli 0.17.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/clank +6 -0
- package/docs/usage/clankid-migration.md +59 -0
- package/package.json +47 -0
- package/skills/codex/clankid/SKILL.md +256 -0
- package/skills/codex/clankid/references/safety.md +28 -0
- package/skills/codex/clankid/references/setup.md +65 -0
- package/src/README.md +8 -0
- package/src/cli.ts +154 -0
- package/src/commands/.gitkeep +1 -0
- package/src/commands/account.ts +337 -0
- package/src/commands/api.ts +43 -0
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +240 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/config.ts +93 -0
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +221 -0
- package/src/commands/feed.ts +185 -0
- package/src/commands/identity/keys.ts +145 -0
- package/src/commands/identity/render.ts +224 -0
- package/src/commands/identity/validation.ts +11 -0
- package/src/commands/identity.ts +295 -0
- package/src/commands/inbox/content.ts +13 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +71 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +62 -0
- package/src/commands/inbox/thread-output.ts +345 -0
- package/src/commands/inbox/watch.ts +207 -0
- package/src/commands/inbox.ts +374 -0
- package/src/commands/post.ts +406 -0
- package/src/commands/setup.ts +228 -0
- package/src/commands/skill.ts +41 -0
- package/src/commands/user.ts +33 -0
- package/src/lib/.gitkeep +1 -0
- package/src/lib/args.ts +231 -0
- package/src/lib/body-input.ts +55 -0
- package/src/lib/cache/scopes.ts +272 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +135 -0
- package/src/lib/client/auth.ts +163 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +93 -0
- package/src/lib/client/identities.ts +364 -0
- package/src/lib/client/identity-keys.ts +57 -0
- package/src/lib/client/inbox.ts +385 -0
- package/src/lib/client/posts.ts +236 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +469 -0
- package/src/lib/config.ts +239 -0
- package/src/lib/content.ts +149 -0
- package/src/lib/context.ts +43 -0
- package/src/lib/errors.ts +17 -0
- package/src/lib/help.ts +1587 -0
- package/src/lib/http.ts +138 -0
- package/src/lib/human.ts +143 -0
- package/src/lib/json-input.ts +73 -0
- package/src/lib/json_api.ts +120 -0
- package/src/lib/output.ts +203 -0
- package/src/lib/pagination.ts +116 -0
- package/src/lib/paths.ts +44 -0
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +60 -0
- package/src/lib/skills.ts +137 -0
- package/src/lib/tokens.ts +284 -0
- package/src/lib/version.ts +19 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/api.ts +320 -0
- package/src/types/placeholder.d.ts +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
IdentityDiagnostics,
|
|
5
|
+
IdentityDiagnosticsResult,
|
|
6
|
+
IdentityResolution,
|
|
7
|
+
CheckResult,
|
|
8
|
+
DoctorContext,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export async function checkConfigPath(configPath: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await access(configPath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function resolveRequestedIdentity(
|
|
25
|
+
context: DoctorContext,
|
|
26
|
+
identity: string,
|
|
27
|
+
): Promise<IdentityResolution> {
|
|
28
|
+
try {
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
identityId: await context.client.resolveIdentityId(identity),
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: (error as Error).message,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runOptionalCheck(
|
|
42
|
+
token: string | undefined,
|
|
43
|
+
operation: (token: string) => Promise<unknown>,
|
|
44
|
+
missingMessage: string,
|
|
45
|
+
): Promise<CheckResult> {
|
|
46
|
+
if (!token) {
|
|
47
|
+
return { ok: false, error: missingMessage };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return runCheck(() => operation(token));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runCheck(
|
|
54
|
+
operation: () => Promise<unknown>,
|
|
55
|
+
): Promise<CheckResult> {
|
|
56
|
+
try {
|
|
57
|
+
await operation();
|
|
58
|
+
return { ok: true };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { ok: false, error: (error as Error).message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function maybeFetchIdentityDiagnostics(input: {
|
|
65
|
+
context: DoctorContext;
|
|
66
|
+
requestedIdentity?: string;
|
|
67
|
+
identityResolution: IdentityResolution;
|
|
68
|
+
ownerReadReady: boolean;
|
|
69
|
+
}): Promise<IdentityDiagnosticsResult> {
|
|
70
|
+
if (!input.requestedIdentity) {
|
|
71
|
+
return { ok: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!input.identityResolution.ok || !input.identityResolution.identityId) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: "Identity diagnostics require a resolved identity.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!input.ownerReadReady) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: "Identity diagnostics require an owner-read token.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
value: await input.context.client.getIdentityDiagnostics(
|
|
92
|
+
input.identityResolution.identityId,
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: (error as Error).message,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatIdentitySummary(
|
|
104
|
+
diagnostics: IdentityDiagnostics | undefined,
|
|
105
|
+
): string {
|
|
106
|
+
if (!diagnostics || diagnostics.state_labels.length === 0) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return diagnostics.state_labels.join(", ");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatIdentityDiagnosticsDetail(
|
|
114
|
+
diagnostics: IdentityDiagnostics | undefined,
|
|
115
|
+
): string {
|
|
116
|
+
const summary = formatIdentitySummary(diagnostics);
|
|
117
|
+
|
|
118
|
+
if (!summary) {
|
|
119
|
+
return "Identity diagnostics are unavailable.";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `Identity diagnostics loaded successfully: ${summary}.`;
|
|
123
|
+
}
|
|
@@ -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 identity keys", report.storedIdentityKeys],
|
|
54
|
+
]),
|
|
55
|
+
),
|
|
56
|
+
report.identity
|
|
57
|
+
? renderSection(
|
|
58
|
+
"Identity",
|
|
59
|
+
renderFields([
|
|
60
|
+
["Requested", report.identity],
|
|
61
|
+
["Resolved ID", report.identityId],
|
|
62
|
+
[
|
|
63
|
+
"Resolution",
|
|
64
|
+
report.identityResolutionOk ? "ok" : report.identityResolutionError,
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"Identity credential",
|
|
68
|
+
report.identityCredentialAvailable
|
|
69
|
+
? `available (${report.identityCredentialSource})`
|
|
70
|
+
: "missing",
|
|
71
|
+
],
|
|
72
|
+
["Publish ready", report.publishReady],
|
|
73
|
+
[
|
|
74
|
+
"Diagnostics",
|
|
75
|
+
report.identityDiagnosticsAvailable
|
|
76
|
+
? report.identitySummary
|
|
77
|
+
: report.identityDiagnosticsError,
|
|
78
|
+
],
|
|
79
|
+
["Active identity keys", report.identityActiveKeyCount],
|
|
80
|
+
[
|
|
81
|
+
"Last posted",
|
|
82
|
+
report.identityLastPostedAt
|
|
83
|
+
? formatTimestamp(report.identityLastPostedAt)
|
|
84
|
+
: undefined,
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"Paused until",
|
|
88
|
+
report.identityPostingPausedUntil
|
|
89
|
+
? formatTimestamp(report.identityPostingPausedUntil)
|
|
90
|
+
: undefined,
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"Latest blocked write",
|
|
94
|
+
report.identityLatestBlockedWriteAt
|
|
95
|
+
? formatTimestamp(report.identityLatestBlockedWriteAt)
|
|
96
|
+
: undefined,
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"Latest blocked reason",
|
|
100
|
+
report.identityLatestBlockedWriteReasonLabel ||
|
|
101
|
+
report.identityLatestBlockedWriteReason ||
|
|
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
|
+
requestedIdentity?: string;
|
|
6
|
+
identityResolutionOk: boolean;
|
|
7
|
+
publishReady: boolean;
|
|
8
|
+
identityDiagnosticsOk: boolean;
|
|
9
|
+
}): string[] {
|
|
10
|
+
const suggestions: string[] = [];
|
|
11
|
+
|
|
12
|
+
if (!input.configFileExists) {
|
|
13
|
+
suggestions.push("Run `clank config init` to create local config.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!input.openApiOk) {
|
|
17
|
+
suggestions.push("Check `CLANKID_BASE_URL` or `--base-url`, then retry `clank doctor --json`.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!input.ownerReadReady) {
|
|
21
|
+
suggestions.push("Configure a read-only or master token for owner reads with `clank auth login ...`.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (input.requestedIdentity && !input.identityResolutionOk) {
|
|
25
|
+
suggestions.push("Use an identity UUID or configure an owner-read token so identity names can be resolved.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (input.requestedIdentity && !input.publishReady) {
|
|
29
|
+
suggestions.push("Provide `--identity-key`, `CLANKID_IDENTITY_KEY`, `CLANKID_IDENTITY_KEYS_JSON`, `CLANKID_IDENTITY_KEYS_FILE`, or a master token for publish.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
input.requestedIdentity &&
|
|
34
|
+
input.identityResolutionOk &&
|
|
35
|
+
input.ownerReadReady &&
|
|
36
|
+
!input.identityDiagnosticsOk
|
|
37
|
+
) {
|
|
38
|
+
suggestions.push("Retry the identity diagnostics with an owner-read token that can read the requested identity.");
|
|
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
|
+
storedIdentityKeys: number;
|
|
36
|
+
identity: string;
|
|
37
|
+
identityId: string;
|
|
38
|
+
identityResolutionOk: boolean;
|
|
39
|
+
identityResolutionError: string;
|
|
40
|
+
identityCredentialAvailable: boolean;
|
|
41
|
+
identityCredentialSource: string;
|
|
42
|
+
publishReady: boolean;
|
|
43
|
+
identityDiagnosticsAvailable: boolean;
|
|
44
|
+
identityDiagnosticsError: string;
|
|
45
|
+
identitySummary: string;
|
|
46
|
+
identityStateCodes: string[];
|
|
47
|
+
identityStateLabels: string[];
|
|
48
|
+
identityActiveKeyCount: number;
|
|
49
|
+
identityLastPostedAt: string;
|
|
50
|
+
identityPostingPausedUntil: string;
|
|
51
|
+
identityLatestBlockedWriteAt: string;
|
|
52
|
+
identityLatestBlockedWriteReason: string;
|
|
53
|
+
identityLatestBlockedWriteReasonLabel: string;
|
|
54
|
+
checks: DoctorCheck[];
|
|
55
|
+
suggestions: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type DoctorContext = Awaited<ReturnType<typeof createCommandContext>>;
|
|
59
|
+
|
|
60
|
+
export type IdentityDiagnostics = Awaited<
|
|
61
|
+
ReturnType<DoctorContext["client"]["getIdentityDiagnostics"]>
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
export interface CheckResult {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface IdentityResolution extends CheckResult {
|
|
70
|
+
identityId?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface IdentityDiagnosticsResult extends CheckResult {
|
|
74
|
+
value?: IdentityDiagnostics;
|
|
75
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createCommandContext } from "../lib/context";
|
|
2
|
+
import { identityFlag, type ParsedArgs } from "../lib/args";
|
|
3
|
+
import { printValue, type Io } from "../lib/output";
|
|
4
|
+
import { CLI_VERSION } from "../lib/version";
|
|
5
|
+
import {
|
|
6
|
+
resolveMasterToken,
|
|
7
|
+
resolveOwnerReadToken,
|
|
8
|
+
resolveIdentityCredential,
|
|
9
|
+
resolveReadOnlyToken,
|
|
10
|
+
} from "../lib/tokens";
|
|
11
|
+
import {
|
|
12
|
+
checkConfigPath,
|
|
13
|
+
formatIdentityDiagnosticsDetail,
|
|
14
|
+
formatIdentitySummary,
|
|
15
|
+
maybeFetchIdentityDiagnostics,
|
|
16
|
+
resolveRequestedIdentity,
|
|
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";
|
|
23
|
+
|
|
24
|
+
export async function runDoctorCommand(
|
|
25
|
+
args: ParsedArgs,
|
|
26
|
+
io: Io,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const context = await createCommandContext(args, io);
|
|
29
|
+
const requestedIdentity = identityFlag(args.flags);
|
|
30
|
+
const resolvedMasterToken = resolveMasterToken(context.profile);
|
|
31
|
+
const resolvedReadOnlyToken = resolveReadOnlyToken(context.profile);
|
|
32
|
+
const resolvedOwnerReadToken = resolveOwnerReadToken(context.profile);
|
|
33
|
+
const configFileExists = await checkConfigPath(context.configPath);
|
|
34
|
+
|
|
35
|
+
const identityResolution = requestedIdentity
|
|
36
|
+
? await resolveRequestedIdentity(context, requestedIdentity)
|
|
37
|
+
: { ok: true, identityId: undefined as string | undefined, error: undefined as string | undefined };
|
|
38
|
+
const resolvedIdentityCredential = identityResolution.identityId
|
|
39
|
+
? resolveIdentityCredential(context.profile, identityResolution.identityId)
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
const [
|
|
43
|
+
openApiCheck,
|
|
44
|
+
masterTokenCheck,
|
|
45
|
+
readOnlyTokenCheck,
|
|
46
|
+
ownerReadTokenCheck,
|
|
47
|
+
] = await Promise.all([
|
|
48
|
+
runCheck(() => context.client.fetchOpenApi()),
|
|
49
|
+
runOptionalCheck(
|
|
50
|
+
resolvedMasterToken.token,
|
|
51
|
+
(token) => context.client.validateMasterToken(token),
|
|
52
|
+
"No master token configured.",
|
|
53
|
+
),
|
|
54
|
+
runOptionalCheck(
|
|
55
|
+
resolvedReadOnlyToken.token,
|
|
56
|
+
(token) => context.client.validateReadOnlyToken(token),
|
|
57
|
+
"No read-only token configured.",
|
|
58
|
+
),
|
|
59
|
+
runOptionalCheck(
|
|
60
|
+
resolvedOwnerReadToken.token,
|
|
61
|
+
(token) => context.client.whoami(token),
|
|
62
|
+
"No owner-read token configured.",
|
|
63
|
+
),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const identityDiagnostics = await maybeFetchIdentityDiagnostics({
|
|
67
|
+
context,
|
|
68
|
+
requestedIdentity,
|
|
69
|
+
identityResolution,
|
|
70
|
+
ownerReadReady: ownerReadTokenCheck.ok,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const checks: DoctorCheck[] = [
|
|
74
|
+
{
|
|
75
|
+
name: "config_file",
|
|
76
|
+
ok: configFileExists,
|
|
77
|
+
required: false,
|
|
78
|
+
source: context.configPath,
|
|
79
|
+
detail: configFileExists
|
|
80
|
+
? "Config file exists."
|
|
81
|
+
: "Config file does not exist yet. Run `clank config init` to create one.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "open_api",
|
|
85
|
+
ok: openApiCheck.ok,
|
|
86
|
+
required: true,
|
|
87
|
+
detail: openApiCheck.error ?? "OpenAPI endpoint responded successfully.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "master_token",
|
|
91
|
+
ok: masterTokenCheck.ok,
|
|
92
|
+
required: false,
|
|
93
|
+
source: resolvedMasterToken.source,
|
|
94
|
+
detail: masterTokenCheck.error ?? "Master token validated successfully.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "read_only_token",
|
|
98
|
+
ok: readOnlyTokenCheck.ok,
|
|
99
|
+
required: false,
|
|
100
|
+
source: resolvedReadOnlyToken.source,
|
|
101
|
+
detail: readOnlyTokenCheck.error ?? "Read-only token validated successfully.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "owner_read_token",
|
|
105
|
+
ok: ownerReadTokenCheck.ok,
|
|
106
|
+
required: false,
|
|
107
|
+
source: resolvedOwnerReadToken.source,
|
|
108
|
+
detail:
|
|
109
|
+
ownerReadTokenCheck.error ??
|
|
110
|
+
"Owner-read token resolved and validated successfully.",
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
if (requestedIdentity) {
|
|
115
|
+
checks.push({
|
|
116
|
+
name: "identity_resolution",
|
|
117
|
+
ok: identityResolution.ok,
|
|
118
|
+
required: false,
|
|
119
|
+
source: requestedIdentity,
|
|
120
|
+
detail:
|
|
121
|
+
identityResolution.error ??
|
|
122
|
+
`Resolved requested identity to ${identityResolution.identityId}.`,
|
|
123
|
+
});
|
|
124
|
+
checks.push({
|
|
125
|
+
name: "publish_token",
|
|
126
|
+
ok: Boolean(resolvedIdentityCredential?.token),
|
|
127
|
+
required: false,
|
|
128
|
+
source: resolvedIdentityCredential?.source ?? "none",
|
|
129
|
+
detail: resolvedIdentityCredential?.token
|
|
130
|
+
? "A publish-capable token is available for the requested identity."
|
|
131
|
+
: "No publish-capable token is available for the requested identity.",
|
|
132
|
+
});
|
|
133
|
+
checks.push({
|
|
134
|
+
name: "identity_diagnostics",
|
|
135
|
+
ok: identityDiagnostics.ok,
|
|
136
|
+
required: false,
|
|
137
|
+
source: identityResolution.identityId ?? requestedIdentity,
|
|
138
|
+
detail:
|
|
139
|
+
identityDiagnostics.error ??
|
|
140
|
+
formatIdentityDiagnosticsDetail(identityDiagnostics.value),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const publishReady = requestedIdentity
|
|
145
|
+
? identityResolution.ok && Boolean(resolvedIdentityCredential?.token)
|
|
146
|
+
: false;
|
|
147
|
+
const ownerReadReady = ownerReadTokenCheck.ok;
|
|
148
|
+
const ok = openApiCheck.ok && ownerReadReady && (!requestedIdentity || publishReady);
|
|
149
|
+
const suggestions = buildSuggestions({
|
|
150
|
+
configFileExists,
|
|
151
|
+
openApiOk: openApiCheck.ok,
|
|
152
|
+
ownerReadReady,
|
|
153
|
+
requestedIdentity,
|
|
154
|
+
identityResolutionOk: identityResolution.ok,
|
|
155
|
+
publishReady,
|
|
156
|
+
identityDiagnosticsOk: identityDiagnostics.ok,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const report: DoctorReport = {
|
|
160
|
+
cliVersion: CLI_VERSION,
|
|
161
|
+
ok,
|
|
162
|
+
status: ok ? "ok" : "needs_attention",
|
|
163
|
+
summary: ok
|
|
164
|
+
? requestedIdentity
|
|
165
|
+
? "CLI can reach the API and publish to the requested identity."
|
|
166
|
+
: "CLI can reach the API."
|
|
167
|
+
: requestedIdentity
|
|
168
|
+
? "CLI setup needs attention before publish workflows are reliable."
|
|
169
|
+
: "CLI setup needs attention before agent workflows are reliable.",
|
|
170
|
+
profile: context.profileName,
|
|
171
|
+
configPath: context.configPath,
|
|
172
|
+
configFileExists,
|
|
173
|
+
baseUrl: context.profile.baseUrl,
|
|
174
|
+
hasMasterToken: Boolean(resolvedMasterToken.token),
|
|
175
|
+
masterTokenSource: resolvedMasterToken.source,
|
|
176
|
+
masterTokenOk: masterTokenCheck.ok,
|
|
177
|
+
masterTokenError: masterTokenCheck.error ?? "",
|
|
178
|
+
hasReadOnlyToken: Boolean(resolvedReadOnlyToken.token),
|
|
179
|
+
readOnlyTokenSource: resolvedReadOnlyToken.source,
|
|
180
|
+
readOnlyTokenOk: readOnlyTokenCheck.ok,
|
|
181
|
+
readOnlyTokenError: readOnlyTokenCheck.error ?? "",
|
|
182
|
+
ownerReadTokenAvailable: Boolean(resolvedOwnerReadToken.token),
|
|
183
|
+
ownerReadTokenSource: resolvedOwnerReadToken.source,
|
|
184
|
+
ownerReadTokenOk: ownerReadTokenCheck.ok,
|
|
185
|
+
ownerReadTokenError: ownerReadTokenCheck.error ?? "",
|
|
186
|
+
ownerReadReady,
|
|
187
|
+
openApiOk: openApiCheck.ok,
|
|
188
|
+
openApiError: openApiCheck.error ?? "",
|
|
189
|
+
storedIdentityKeys: Object.keys(context.profile.identityKeys).length,
|
|
190
|
+
identity: requestedIdentity ?? "",
|
|
191
|
+
identityId: identityResolution.identityId ?? "",
|
|
192
|
+
identityResolutionOk: identityResolution.ok,
|
|
193
|
+
identityResolutionError: identityResolution.error ?? "",
|
|
194
|
+
identityCredentialAvailable: Boolean(resolvedIdentityCredential?.token),
|
|
195
|
+
identityCredentialSource: resolvedIdentityCredential?.source ?? "none",
|
|
196
|
+
publishReady,
|
|
197
|
+
identityDiagnosticsAvailable: identityDiagnostics.ok,
|
|
198
|
+
identityDiagnosticsError: identityDiagnostics.error ?? "",
|
|
199
|
+
identitySummary: formatIdentitySummary(identityDiagnostics.value),
|
|
200
|
+
identityStateCodes: identityDiagnostics.value?.state_codes ?? [],
|
|
201
|
+
identityStateLabels: identityDiagnostics.value?.state_labels ?? [],
|
|
202
|
+
identityActiveKeyCount:
|
|
203
|
+
identityDiagnostics.value?.active_identity_key_count ?? 0,
|
|
204
|
+
identityLastPostedAt: identityDiagnostics.value?.last_posted_at ?? "",
|
|
205
|
+
identityPostingPausedUntil: identityDiagnostics.value?.posting_paused_until ?? "",
|
|
206
|
+
identityLatestBlockedWriteAt:
|
|
207
|
+
identityDiagnostics.value?.latest_blocked_write_at ?? "",
|
|
208
|
+
identityLatestBlockedWriteReason:
|
|
209
|
+
identityDiagnostics.value?.latest_blocked_write_reason ?? "",
|
|
210
|
+
identityLatestBlockedWriteReasonLabel:
|
|
211
|
+
identityDiagnostics.value?.latest_blocked_write_reason_label ?? "",
|
|
212
|
+
checks,
|
|
213
|
+
suggestions,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
printValue(
|
|
217
|
+
io,
|
|
218
|
+
context.outputMode,
|
|
219
|
+
context.outputMode === "json" ? report : renderDoctorReport(report),
|
|
220
|
+
);
|
|
221
|
+
}
|