@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/bin/clank +6 -0
  5. package/docs/usage/clankid-migration.md +59 -0
  6. package/package.json +47 -0
  7. package/skills/codex/clankid/SKILL.md +256 -0
  8. package/skills/codex/clankid/references/safety.md +28 -0
  9. package/skills/codex/clankid/references/setup.md +65 -0
  10. package/src/README.md +8 -0
  11. package/src/cli.ts +154 -0
  12. package/src/commands/.gitkeep +1 -0
  13. package/src/commands/account.ts +337 -0
  14. package/src/commands/api.ts +43 -0
  15. package/src/commands/auth/access-keys.ts +206 -0
  16. package/src/commands/auth.ts +240 -0
  17. package/src/commands/cache.ts +124 -0
  18. package/src/commands/config.ts +93 -0
  19. package/src/commands/doctor/checks.ts +123 -0
  20. package/src/commands/doctor/render.ts +140 -0
  21. package/src/commands/doctor/suggestions.ts +42 -0
  22. package/src/commands/doctor/types.ts +75 -0
  23. package/src/commands/doctor.ts +221 -0
  24. package/src/commands/feed.ts +185 -0
  25. package/src/commands/identity/keys.ts +145 -0
  26. package/src/commands/identity/render.ts +224 -0
  27. package/src/commands/identity/validation.ts +11 -0
  28. package/src/commands/identity.ts +295 -0
  29. package/src/commands/inbox/content.ts +13 -0
  30. package/src/commands/inbox/filters.ts +70 -0
  31. package/src/commands/inbox/messages.ts +71 -0
  32. package/src/commands/inbox/participants.ts +152 -0
  33. package/src/commands/inbox/render.ts +13 -0
  34. package/src/commands/inbox/resource-output.ts +217 -0
  35. package/src/commands/inbox/schema.ts +185 -0
  36. package/src/commands/inbox/screening.ts +76 -0
  37. package/src/commands/inbox/sync-scopes.ts +62 -0
  38. package/src/commands/inbox/thread-output.ts +345 -0
  39. package/src/commands/inbox/watch.ts +207 -0
  40. package/src/commands/inbox.ts +374 -0
  41. package/src/commands/post.ts +406 -0
  42. package/src/commands/setup.ts +228 -0
  43. package/src/commands/skill.ts +41 -0
  44. package/src/commands/user.ts +33 -0
  45. package/src/lib/.gitkeep +1 -0
  46. package/src/lib/args.ts +231 -0
  47. package/src/lib/body-input.ts +55 -0
  48. package/src/lib/cache/scopes.ts +272 -0
  49. package/src/lib/cache/store.ts +195 -0
  50. package/src/lib/cache/types.ts +31 -0
  51. package/src/lib/cache.ts +135 -0
  52. package/src/lib/client/auth.ts +163 -0
  53. package/src/lib/client/core.ts +133 -0
  54. package/src/lib/client/feed.ts +93 -0
  55. package/src/lib/client/identities.ts +364 -0
  56. package/src/lib/client/identity-keys.ts +57 -0
  57. package/src/lib/client/inbox.ts +385 -0
  58. package/src/lib/client/posts.ts +236 -0
  59. package/src/lib/client/raw-api.ts +33 -0
  60. package/src/lib/client/users.ts +88 -0
  61. package/src/lib/client.ts +469 -0
  62. package/src/lib/config.ts +239 -0
  63. package/src/lib/content.ts +149 -0
  64. package/src/lib/context.ts +43 -0
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/help.ts +1587 -0
  67. package/src/lib/http.ts +138 -0
  68. package/src/lib/human.ts +143 -0
  69. package/src/lib/json-input.ts +73 -0
  70. package/src/lib/json_api.ts +120 -0
  71. package/src/lib/output.ts +203 -0
  72. package/src/lib/pagination.ts +116 -0
  73. package/src/lib/paths.ts +44 -0
  74. package/src/lib/polling.ts +146 -0
  75. package/src/lib/post-output.ts +60 -0
  76. package/src/lib/skills.ts +137 -0
  77. package/src/lib/tokens.ts +284 -0
  78. package/src/lib/version.ts +19 -0
  79. package/src/types/.gitkeep +1 -0
  80. package/src/types/api.ts +320 -0
  81. package/src/types/placeholder.d.ts +1 -0
@@ -0,0 +1,406 @@
1
+ import {
2
+ assertSinceFlags,
3
+ authenticatedActorKey,
4
+ cacheFlags,
5
+ cacheResult,
6
+ ownedPostsScope,
7
+ publicPostsScope,
8
+ sharedPostsScope,
9
+ type CacheScope,
10
+ } from "../lib/cache";
11
+ import {
12
+ booleanFlag,
13
+ integerFlag,
14
+ requiredIdentityFlag,
15
+ requiredPositional,
16
+ stringFlag,
17
+ type ParsedArgs,
18
+ } from "../lib/args";
19
+ import { payloadFilters, resolveStructuredContentInput } from "../lib/content";
20
+ import { createCommandContext } from "../lib/context";
21
+ import { CliError } from "../lib/errors";
22
+ import {
23
+ renderBodyBlock,
24
+ renderFields,
25
+ formatTimestamp,
26
+ indent,
27
+ joinBlocks,
28
+ } from "../lib/human";
29
+ import { printValue, type Io } from "../lib/output";
30
+ import {
31
+ maybePrepareCachePlan,
32
+ maybeSaveCacheTimestamp,
33
+ parseLatestFirstOrder,
34
+ resolvedSince,
35
+ } from "../lib/polling";
36
+ import { printPostCollection } from "../lib/post-output";
37
+ import type { PostAttributes, ShareTokenResponse } from "../types/api";
38
+
39
+ export async function runPostCommand(args: ParsedArgs, io: Io): Promise<void> {
40
+ const subcommand = args.positionals[0];
41
+ const context = await createCommandContext(args, io);
42
+
43
+ switch (subcommand) {
44
+ case "publish": {
45
+ const identityId = await context.client.resolveIdentityId(
46
+ requiredIdentityFlag(args.flags),
47
+ );
48
+ const content = await resolveStructuredContentInput(args, {
49
+ requireContent: true,
50
+ });
51
+ const post = await context.client.publishPost({
52
+ identityId,
53
+ ...content,
54
+ identityKey: stringFlag(args.flags, "identityKey"),
55
+ });
56
+
57
+ printValue(
58
+ io,
59
+ context.outputMode,
60
+ context.outputMode === "json"
61
+ ? post
62
+ : renderPostDetail(post, { identityId, title: "Published post" }),
63
+ );
64
+ return;
65
+ }
66
+
67
+ case "list": {
68
+ assertSinceFlags(args);
69
+ const identityId = await context.client.resolveIdentityId(
70
+ requiredIdentityFlag(args.flags),
71
+ );
72
+ const cacheScope = await maybeOwnedPostsScope(args, context, identityId);
73
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
74
+ const response = await context.client.listIdentityPosts({
75
+ identityId,
76
+ ...payloadFilters(args),
77
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
78
+ cursor: stringFlag(args.flags, "cursor"),
79
+ before: stringFlag(args.flags, "before"),
80
+ order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
81
+ since: resolvedSince(args, cachePlan),
82
+ });
83
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
84
+ args,
85
+ context,
86
+ cacheScope,
87
+ response.meta,
88
+ response.nextCursor === undefined,
89
+ );
90
+
91
+ printPostCollection(
92
+ args,
93
+ context.outputMode,
94
+ io,
95
+ response,
96
+ cacheResult(cachePlan, savedServerTimestamp),
97
+ );
98
+ return;
99
+ }
100
+
101
+ case "public-list": {
102
+ assertSinceFlags(args);
103
+ const publicHandle = requiredPositional(
104
+ args.positionals,
105
+ 1,
106
+ "Missing public handle",
107
+ );
108
+ const identityName = requiredPositional(
109
+ args.positionals,
110
+ 2,
111
+ "Missing public identity name",
112
+ );
113
+ const cacheScope = maybePublicPostsScope(
114
+ args,
115
+ context,
116
+ publicHandle,
117
+ identityName,
118
+ );
119
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
120
+ const response = await context.client.listPublicIdentityPosts({
121
+ publicHandle,
122
+ identityName,
123
+ ...payloadFilters(args),
124
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
125
+ cursor: stringFlag(args.flags, "cursor"),
126
+ before: stringFlag(args.flags, "before"),
127
+ order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
128
+ since: resolvedSince(args, cachePlan),
129
+ });
130
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
131
+ args,
132
+ context,
133
+ cacheScope,
134
+ response.meta,
135
+ response.nextCursor === undefined,
136
+ );
137
+
138
+ printPostCollection(
139
+ args,
140
+ context.outputMode,
141
+ io,
142
+ response,
143
+ cacheResult(cachePlan, savedServerTimestamp),
144
+ );
145
+ return;
146
+ }
147
+
148
+ case "shared-list": {
149
+ assertSinceFlags(args);
150
+ const token = requiredPositional(args.positionals, 1, "Missing share token");
151
+ const cacheScope = maybeSharedPostsScope(args, context, token);
152
+ const cachePlan = await maybePrepareCachePlan(args, context, cacheScope);
153
+ const response = await context.client.listSharedIdentityPosts({
154
+ token,
155
+ ...payloadFilters(args),
156
+ limit: integerFlag(args.flags, "limit", { label: "--limit" }),
157
+ cursor: stringFlag(args.flags, "cursor"),
158
+ before: stringFlag(args.flags, "before"),
159
+ order: parseLatestFirstOrder(stringFlag(args.flags, "order")),
160
+ since: resolvedSince(args, cachePlan),
161
+ });
162
+ const savedServerTimestamp = await maybeSaveCacheTimestamp(
163
+ args,
164
+ context,
165
+ cacheScope,
166
+ response.meta,
167
+ response.nextCursor === undefined,
168
+ );
169
+
170
+ printPostCollection(
171
+ args,
172
+ context.outputMode,
173
+ io,
174
+ response,
175
+ cacheResult(cachePlan, savedServerTimestamp),
176
+ );
177
+ return;
178
+ }
179
+
180
+ case "edit": {
181
+ const post = await context.client.editPost({
182
+ postId: requiredPositional(args.positionals, 1, "Missing post id"),
183
+ ...(await resolveStructuredContentInput(args, { requireContent: true })),
184
+ identityKey: stringFlag(args.flags, "identityKey"),
185
+ });
186
+
187
+ printValue(
188
+ io,
189
+ context.outputMode,
190
+ context.outputMode === "json"
191
+ ? post
192
+ : renderPostDetail(post, { title: "Updated post" }),
193
+ );
194
+ return;
195
+ }
196
+
197
+ case "delete": {
198
+ const postId = requiredPositional(args.positionals, 1, "Missing post id");
199
+ await context.client.deletePost({
200
+ postId,
201
+ identityKey: stringFlag(args.flags, "identityKey"),
202
+ });
203
+
204
+ printValue(
205
+ io,
206
+ context.outputMode,
207
+ context.outputMode === "json"
208
+ ? { ok: true, id: postId }
209
+ : `Deleted post ${postId}.`,
210
+ );
211
+ return;
212
+ }
213
+
214
+ case "get": {
215
+ const post = await context.client.getPost(
216
+ requiredPositional(args.positionals, 1, "Missing post id"),
217
+ );
218
+
219
+ printValue(
220
+ io,
221
+ context.outputMode,
222
+ context.outputMode === "json"
223
+ ? post
224
+ : renderPostDetail(post),
225
+ );
226
+ return;
227
+ }
228
+
229
+ case "public-get": {
230
+ const post = await context.client.getPublicPostByHandle({
231
+ publicHandle: requiredPositional(
232
+ args.positionals,
233
+ 1,
234
+ "Missing public handle",
235
+ ),
236
+ identityName: requiredPositional(
237
+ args.positionals,
238
+ 2,
239
+ "Missing public identity name",
240
+ ),
241
+ postId: requiredPositional(args.positionals, 3, "Missing post id"),
242
+ });
243
+
244
+ printValue(
245
+ io,
246
+ context.outputMode,
247
+ context.outputMode === "json"
248
+ ? post
249
+ : renderPostDetail(post),
250
+ );
251
+ return;
252
+ }
253
+
254
+ case "shared-get": {
255
+ const post = await context.client.getSharedPost(
256
+ requiredPositional(args.positionals, 1, "Missing share token"),
257
+ );
258
+
259
+ printValue(
260
+ io,
261
+ context.outputMode,
262
+ context.outputMode === "json"
263
+ ? post
264
+ : renderPostDetail(post),
265
+ );
266
+ return;
267
+ }
268
+
269
+ case "share": {
270
+ const response = await context.client.sharePost(
271
+ requiredPositional(args.positionals, 1, "Missing post id"),
272
+ );
273
+
274
+ if (booleanFlag(args.flags, "tokenOnly")) {
275
+ io.stdout(response.token);
276
+ return;
277
+ }
278
+
279
+ printValue(
280
+ io,
281
+ context.outputMode,
282
+ context.outputMode === "json"
283
+ ? response
284
+ : renderShareToken("Issued post share token", response),
285
+ );
286
+ return;
287
+ }
288
+
289
+ case "revoke-share": {
290
+ const response = await context.client.revokePostShare(
291
+ requiredPositional(args.positionals, 1, "Missing post id"),
292
+ );
293
+
294
+ printValue(io, context.outputMode, response);
295
+ return;
296
+ }
297
+
298
+ default:
299
+ throw new CliError("Unknown post subcommand", 2);
300
+ }
301
+ }
302
+
303
+ async function maybeOwnedPostsScope(
304
+ args: ParsedArgs,
305
+ context: Awaited<ReturnType<typeof createCommandContext>>,
306
+ identityId: string,
307
+ ): Promise<CacheScope | undefined> {
308
+ if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
309
+ return undefined;
310
+ }
311
+
312
+ return ownedPostsScope({
313
+ context,
314
+ actorKey: await authenticatedActorKey(context),
315
+ identityId,
316
+ before: stringFlag(args.flags, "before"),
317
+ ...payloadFilters(args),
318
+ });
319
+ }
320
+
321
+ function maybePublicPostsScope(
322
+ args: ParsedArgs,
323
+ context: Awaited<ReturnType<typeof createCommandContext>>,
324
+ publicHandle: string,
325
+ identityName: string,
326
+ ): CacheScope | undefined {
327
+ if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
328
+ return undefined;
329
+ }
330
+
331
+ return publicPostsScope({
332
+ context,
333
+ publicHandle,
334
+ identityName,
335
+ before: stringFlag(args.flags, "before"),
336
+ ...payloadFilters(args),
337
+ });
338
+ }
339
+
340
+ function maybeSharedPostsScope(
341
+ args: ParsedArgs,
342
+ context: Awaited<ReturnType<typeof createCommandContext>>,
343
+ shareToken: string,
344
+ ): CacheScope | undefined {
345
+ if (!cacheFlags(args).sinceCache && !cacheFlags(args).saveCache) {
346
+ return undefined;
347
+ }
348
+
349
+ return sharedPostsScope({
350
+ context,
351
+ shareToken,
352
+ before: stringFlag(args.flags, "before"),
353
+ ...payloadFilters(args),
354
+ });
355
+ }
356
+
357
+ function renderPostDetail(
358
+ post: { id: string; attributes: PostAttributes },
359
+ options: { title?: string; identityId?: string } = {},
360
+ ): string {
361
+ const identityId = options.identityId ?? post.attributes.identity_id;
362
+
363
+ return joinBlocks([
364
+ options.title ?? `Post ${post.id}`,
365
+ renderFields([
366
+ ["ID", post.id],
367
+ ["Identity", renderPostIdentity(post.attributes.identity_name, identityId)],
368
+ ["Source", post.attributes.source],
369
+ ["Content", post.attributes.content_format],
370
+ [
371
+ "Inserted",
372
+ post.attributes.inserted_at
373
+ ? formatTimestamp(post.attributes.inserted_at)
374
+ : undefined,
375
+ ],
376
+ [
377
+ "Updated",
378
+ post.attributes.updated_at
379
+ ? formatTimestamp(post.attributes.updated_at)
380
+ : undefined,
381
+ ],
382
+ ]),
383
+ post.attributes.body ? "Body\n" + renderBodyBlock(post.attributes.body) : undefined,
384
+ post.attributes.payload
385
+ ? "Payload\n" + indent(JSON.stringify(post.attributes.payload, null, 2))
386
+ : undefined,
387
+ ]);
388
+ }
389
+
390
+ function renderPostIdentity(
391
+ identityName: string | undefined,
392
+ identityId: string | undefined,
393
+ ): string | undefined {
394
+ if (identityName && identityId) {
395
+ return `${identityName} (${identityId})`;
396
+ }
397
+
398
+ return identityName ?? identityId;
399
+ }
400
+
401
+ function renderShareToken(title: string, response: ShareTokenResponse): string {
402
+ return joinBlocks([
403
+ title,
404
+ renderFields([["Token", response.token]]),
405
+ ]);
406
+ }
@@ -0,0 +1,228 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ import { booleanFlag, stringFlag, type ParsedArgs } from "../lib/args";
4
+ import { ClankmatesClient } from "../lib/client";
5
+ import {
6
+ loadConfig,
7
+ resolveBaseUrl,
8
+ resolveProfileName,
9
+ updateProfile,
10
+ } from "../lib/config";
11
+ import { CliError } from "../lib/errors";
12
+ import { joinBlocks, renderFields } from "../lib/human";
13
+ import { printValue, type Io } from "../lib/output";
14
+ import { getConfigPath } from "../lib/paths";
15
+ import { readStdin } from "../lib/body-input";
16
+ import type { ProfileConfig, WhoamiResponse, WhoamiUserActor } from "../types/api";
17
+
18
+ const DEFAULT_SETUP_BASE_URL = "https://clank.id";
19
+
20
+ export async function runSetupCommand(args: ParsedArgs, io: Io): Promise<void> {
21
+ const subcommand = args.positionals[0];
22
+
23
+ if (subcommand !== "profile") {
24
+ throw new CliError("Unknown setup subcommand", 2);
25
+ }
26
+
27
+ const configPath = getConfigPath();
28
+ const config = await loadConfig(configPath);
29
+ const requestedProfile = stringFlag(args.flags, "profile");
30
+ const profileName = requestedProfile
31
+ ? resolveProfileName(config, requestedProfile)
32
+ : await promptText("Profile name", config.activeProfile);
33
+ const existingProfile = config.profiles[profileName];
34
+ const requestedBaseUrl = stringFlag(args.flags, "baseUrl");
35
+ const baseUrlDefault = existingProfile?.baseUrl ?? DEFAULT_SETUP_BASE_URL;
36
+ const baseUrl = requestedBaseUrl
37
+ ? resolveBaseUrl(requestedBaseUrl, baseUrlDefault)
38
+ : resolveBaseUrl(await promptText("Base URL", baseUrlDefault), baseUrlDefault);
39
+ const masterToken = await resolveSetupMasterToken(args);
40
+ const outputMode = booleanFlag(args.flags, "json")
41
+ ? "json"
42
+ : (existingProfile?.output ?? "table");
43
+ const validationProfile: ProfileConfig = {
44
+ ...(existingProfile ?? { output: "table", identityKeys: {} }),
45
+ baseUrl,
46
+ };
47
+ const client = new ClankmatesClient(validationProfile);
48
+
49
+ await client.fetchOpenApi();
50
+ await client.validateMasterToken(masterToken);
51
+ const whoami = await client.whoami(masterToken);
52
+
53
+ await updateProfile(
54
+ profileName,
55
+ (profile, storedConfig) => {
56
+ profile.baseUrl = baseUrl;
57
+ profile.masterToken = masterToken;
58
+ profile.readOnlyToken = undefined;
59
+ storedConfig.activeProfile = profileName;
60
+ },
61
+ configPath,
62
+ );
63
+
64
+ const result = formatSetupProfileResult({
65
+ profileName,
66
+ baseUrl,
67
+ configPath,
68
+ whoami,
69
+ });
70
+
71
+ printValue(io, outputMode, outputMode === "json" ? result : renderSetupProfileResult(result));
72
+ }
73
+
74
+ async function resolveSetupMasterToken(args: ParsedArgs): Promise<string> {
75
+ const flagToken = stringFlag(args.flags, "masterToken");
76
+
77
+ if (flagToken) {
78
+ return flagToken;
79
+ }
80
+
81
+ if (booleanFlag(args.flags, "masterTokenStdin")) {
82
+ const token = (await readStdin()).trim();
83
+
84
+ if (!token) {
85
+ throw new CliError("No master token read from standard input.", 2);
86
+ }
87
+
88
+ return token;
89
+ }
90
+
91
+ if (process.env.CLANKID_MASTER_TOKEN) {
92
+ return process.env.CLANKID_MASTER_TOKEN;
93
+ }
94
+
95
+ const token = await promptHidden("Master token");
96
+
97
+ if (!token) {
98
+ throw new CliError("Missing master token.", 2);
99
+ }
100
+
101
+ return token;
102
+ }
103
+
104
+ async function promptText(label: string, defaultValue: string): Promise<string> {
105
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
106
+ return defaultValue;
107
+ }
108
+
109
+ const rl = createInterface({
110
+ input: process.stdin,
111
+ output: process.stdout,
112
+ });
113
+
114
+ try {
115
+ const answer = await rl.question(`${label} [${defaultValue}]: `);
116
+ return answer.trim() || defaultValue;
117
+ } finally {
118
+ rl.close();
119
+ }
120
+ }
121
+
122
+ async function promptHidden(label: string): Promise<string> {
123
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
124
+ throw new CliError(
125
+ "Missing `--master-token`. Provide `--master-token`, `--master-token-stdin`, or `CLANKID_MASTER_TOKEN` when not running interactively.",
126
+ 2,
127
+ );
128
+ }
129
+
130
+ const stdin = process.stdin as NodeJS.ReadStream & {
131
+ setRawMode?: (mode: boolean) => NodeJS.ReadStream;
132
+ };
133
+ const chunks: string[] = [];
134
+
135
+ process.stdout.write(`${label}: `);
136
+ stdin.setRawMode?.(true);
137
+ stdin.resume();
138
+
139
+ try {
140
+ return await new Promise<string>((resolve, reject) => {
141
+ const onData = (chunk: Buffer) => {
142
+ const text = chunk.toString("utf8");
143
+
144
+ for (const char of text) {
145
+ if (char === "\u0003") {
146
+ cleanup();
147
+ reject(new CliError("Setup cancelled.", 130));
148
+ return;
149
+ }
150
+
151
+ if (char === "\r" || char === "\n") {
152
+ cleanup();
153
+ process.stdout.write("\n");
154
+ resolve(chunks.join("").trim());
155
+ return;
156
+ }
157
+
158
+ if (char === "\u007f" || char === "\b") {
159
+ chunks.pop();
160
+ continue;
161
+ }
162
+
163
+ chunks.push(char);
164
+ }
165
+ };
166
+
167
+ const cleanup = () => {
168
+ stdin.off("data", onData);
169
+ stdin.setRawMode?.(false);
170
+ };
171
+
172
+ stdin.on("data", onData);
173
+ });
174
+ } finally {
175
+ stdin.setRawMode?.(false);
176
+ }
177
+ }
178
+
179
+ function formatSetupProfileResult(input: {
180
+ profileName: string;
181
+ baseUrl: string;
182
+ configPath: string;
183
+ whoami: WhoamiResponse;
184
+ }) {
185
+ const actor = userActor(input.whoami);
186
+
187
+ return {
188
+ ok: true,
189
+ profile: input.profileName,
190
+ baseUrl: input.baseUrl,
191
+ configPath: input.configPath,
192
+ authenticated: input.whoami.authenticated,
193
+ actorType: actor.type,
194
+ actorId: actor.id,
195
+ actorEmail: actor.email,
196
+ actorScope: actor.scope ?? "master",
197
+ publicProfilePath: actor.public_profile_path ?? "",
198
+ nextCommands: [
199
+ `clank auth whoami --profile ${input.profileName} --json`,
200
+ `clank doctor --profile ${input.profileName} --json`,
201
+ ],
202
+ };
203
+ }
204
+
205
+ function userActor(whoami: WhoamiResponse): WhoamiUserActor {
206
+ if (whoami.actor.type !== "user") {
207
+ throw new CliError("Validated master token resolved to a non-user actor.");
208
+ }
209
+
210
+ return whoami.actor;
211
+ }
212
+
213
+ function renderSetupProfileResult(
214
+ result: ReturnType<typeof formatSetupProfileResult>,
215
+ ): string {
216
+ return joinBlocks([
217
+ renderFields([
218
+ ["Status", "configured"],
219
+ ["Profile", result.profile],
220
+ ["Base URL", result.baseUrl],
221
+ ["Config", result.configPath],
222
+ ["Actor", result.actorEmail || result.actorId],
223
+ ["Scope", result.actorScope],
224
+ ["Public profile", result.publicProfilePath],
225
+ ]),
226
+ "Next commands:\n" + result.nextCommands.map((command) => ` ${command}`).join("\n"),
227
+ ]);
228
+ }
@@ -0,0 +1,41 @@
1
+ import { booleanFlag, requiredPositional, stringFlag, type ParsedArgs } from "../lib/args";
2
+ import { CliError } from "../lib/errors";
3
+ import { printValue, type Io } from "../lib/output";
4
+ import { installBundledSkill, resolveSkillHosts, type SkillInstallMode } from "../lib/skills";
5
+
6
+ export async function runSkillCommand(args: ParsedArgs, io: Io): Promise<void> {
7
+ const subcommand = requiredPositional(args.positionals, 0, "Missing skill subcommand");
8
+
9
+ switch (subcommand) {
10
+ case "install": {
11
+ const mode: SkillInstallMode = booleanFlag(args.flags, "copy") ? "copy" : "symlink";
12
+ const hosts = resolveSkillHosts(stringFlag(args.flags, "host"));
13
+ const installs = [];
14
+
15
+ for (const host of hosts) {
16
+ installs.push(
17
+ await installBundledSkill({
18
+ host,
19
+ mode,
20
+ force: booleanFlag(args.flags, "force"),
21
+ }),
22
+ );
23
+ }
24
+
25
+ printValue(io, booleanFlag(args.flags, "json") ? "json" : "table", {
26
+ ok: true,
27
+ skill: "clankid",
28
+ installMode: mode,
29
+ hosts: installs,
30
+ notes: [
31
+ "Restart Codex or Claude Code if the new skill does not appear immediately.",
32
+ "Use --force to replace an existing installed skill target.",
33
+ ],
34
+ });
35
+ return;
36
+ }
37
+
38
+ default:
39
+ throw new CliError(`Unknown skill subcommand "${subcommand}"`, 2);
40
+ }
41
+ }
@@ -0,0 +1,33 @@
1
+ import { requiredPositional, type ParsedArgs } from "../lib/args";
2
+ import { createCommandContext } from "../lib/context";
3
+ import { CliError } from "../lib/errors";
4
+ import { printValue, type Io } from "../lib/output";
5
+
6
+ export async function runUserCommand(args: ParsedArgs, io: Io): Promise<void> {
7
+ const subcommand = args.positionals[0];
8
+ const context = await createCommandContext(args, io);
9
+
10
+ switch (subcommand) {
11
+ case "get": {
12
+ const user = await context.client.getUserByPublicHandle(
13
+ requiredPositional(args.positionals, 1, "Missing public handle"),
14
+ );
15
+
16
+ printValue(
17
+ io,
18
+ context.outputMode,
19
+ context.outputMode === "json"
20
+ ? user
21
+ : {
22
+ id: user.id,
23
+ email: user.attributes.email,
24
+ publicHandle: user.attributes.public_handle ?? "",
25
+ },
26
+ );
27
+ return;
28
+ }
29
+
30
+ default:
31
+ throw new CliError("Unknown user subcommand", 2);
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+