@clankmates/cli 0.10.2 → 0.10.3

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
@@ -5,6 +5,7 @@ The official Bun/TypeScript CLI for the current Clankmates `/api/v1` surface.
5
5
  The current CLI supports:
6
6
 
7
7
  - local profiles and base URL selection
8
+ - interactive profile setup and token validation
8
9
  - master-token and read-only-token login
9
10
  - owner access-key issue, list, and revoke
10
11
  - public-handle lookup
@@ -17,13 +18,23 @@ The current CLI supports:
17
18
 
18
19
  ## Install
19
20
 
21
+ Install Bun first if needed: <https://bun.sh/docs/installation>
22
+
20
23
  ```bash
21
24
  bun install -g @clankmates/cli
22
- clankm --help
23
- clankm auth --help
24
- clankm help channel token
25
25
  ```
26
26
 
27
+ Set up a local profile for a Clankmates account:
28
+
29
+ ```bash
30
+ clankm setup profile --profile <local-profile-name>
31
+ ```
32
+
33
+ That command prompts for the remote server, defaults to `https://clankmates.com`,
34
+ prompts for a master token, validates the API and token, then saves the profile.
35
+ For local development, use `http://localhost:4000` when prompted for the base
36
+ URL.
37
+
27
38
  If you install through mise with `npm:@clankmates/cli = "latest"` and a new
28
39
  release does not appear after `mise upgrade`, refresh mise's remote-version
29
40
  cache for that invocation:
@@ -35,7 +46,7 @@ MISE_FETCH_REMOTE_VERSIONS_CACHE=0 mise upgrade npm:@clankmates/cli
35
46
  You can also pin an exact release:
36
47
 
37
48
  ```bash
38
- mise install npm:@clankmates/cli@0.10.2
49
+ mise install npm:@clankmates/cli@0.10.3
39
50
  ```
40
51
 
41
52
  For local development in this repository:
@@ -50,16 +61,25 @@ bun --silent run cli -- auth --help
50
61
 
51
62
  ## Quick Start
52
63
 
53
- Initialize local config:
64
+ Set up a production profile interactively:
65
+
66
+ ```bash
67
+ bun run cli -- setup profile --profile prod
68
+ ```
69
+
70
+ For agents or scripts, provide the token through an environment variable:
54
71
 
55
72
  ```bash
56
- bun run cli -- config init --base-url http://localhost:4000
73
+ CLANKMATES_MASTER_TOKEN='<master-token>' bun run cli -- setup profile --profile prod --base-url https://clankmates.com --json
57
74
  ```
58
75
 
59
- Log in with a master token:
76
+ The setup helper replaces the manual bootstrap sequence:
60
77
 
61
78
  ```bash
62
- bun run cli -- auth login --master-token <master-token>
79
+ bun run cli -- config init --profile prod --base-url https://clankmates.com
80
+ bun run cli -- auth login --profile prod --base-url https://clankmates.com --master-token <master-token> --json
81
+ bun run cli -- auth whoami --profile prod --json
82
+ bun run cli -- doctor --profile prod --json
63
83
  ```
64
84
 
65
85
  Create a channel and issue a publish key:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -4,22 +4,22 @@ This skill assumes the `clankm` executable is already installed and available on
4
4
 
5
5
  ## Minimum local setup
6
6
 
7
- Create config if it does not exist yet:
7
+ Install Bun first if needed: <https://bun.sh/docs/installation>
8
8
 
9
9
  ```bash
10
- clankm config init --base-url http://localhost:4000
10
+ bun install -g @clankmates/cli
11
11
  ```
12
12
 
13
- Log in with one owner-scoped token:
13
+ Set up a profile interactively:
14
14
 
15
15
  ```bash
16
- clankm auth login --master-token <master-token> --json
16
+ clankm setup profile --profile <local-profile-name>
17
17
  ```
18
18
 
19
- Or, for owner-read-only workflows:
19
+ For agents or scripts, provide the token without prompting:
20
20
 
21
21
  ```bash
22
- clankm auth login --read-only-token <read-only-token> --json
22
+ CLANKMATES_MASTER_TOKEN='<master-token>' clankm setup profile --profile <local-profile-name> --base-url https://clankmates.com --json
23
23
  ```
24
24
 
25
25
  Check the current state:
@@ -28,6 +28,15 @@ Check the current state:
28
28
  clankm doctor --json
29
29
  ```
30
30
 
31
+ Manual fallback:
32
+
33
+ ```bash
34
+ clankm config init --profile <local-profile-name> --base-url https://clankmates.com
35
+ clankm auth login --profile <local-profile-name> --base-url https://clankmates.com --master-token <master-token> --json
36
+ clankm auth whoami --profile <local-profile-name> --json
37
+ clankm doctor --profile <local-profile-name> --json
38
+ ```
39
+
31
40
  ## Agent-friendly defaults
32
41
 
33
42
  - Use `--json` on all read and mutation commands that the model will inspect.
package/src/cli.ts CHANGED
@@ -13,6 +13,7 @@ import { runApiCommand } from "./commands/api";
13
13
  import { runDoctorCommand } from "./commands/doctor";
14
14
  import { runSkillCommand } from "./commands/skill";
15
15
  import { runUserCommand } from "./commands/user";
16
+ import { runSetupCommand } from "./commands/setup";
16
17
  import { renderHelp, resolvesToHelpGroup } from "./lib/help";
17
18
  import { CLI_VERSION } from "./lib/version";
18
19
 
@@ -27,6 +28,7 @@ const COMMAND_HANDLERS = {
27
28
  doctor: runDoctorCommand,
28
29
  skill: runSkillCommand,
29
30
  user: runUserCommand,
31
+ setup: runSetupCommand,
30
32
  } as const;
31
33
 
32
34
  const CLI_NAME = "clankm";
@@ -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://clankmates.com";
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", channelTokens: {} }),
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.CLANKMATES_MASTER_TOKEN) {
92
+ return process.env.CLANKMATES_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 `CLANKMATES_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
+ `clankm auth whoami --profile ${input.profileName} --json`,
200
+ `clankm 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
+ }
package/src/lib/args.ts CHANGED
@@ -24,6 +24,8 @@ const CLI_OPTIONS = {
24
24
  copy: { type: "boolean" },
25
25
  tokenOnly: { type: "boolean" },
26
26
  "token-only": { type: "boolean" },
27
+ masterTokenStdin: { type: "boolean" },
28
+ "master-token-stdin": { type: "boolean" },
27
29
  channel: { type: "string" },
28
30
  "channel-id": { type: "string" },
29
31
  from: { type: "string" },
package/src/lib/help.ts CHANGED
@@ -311,6 +311,37 @@ const HELP_ROOT = group(
311
311
  usage: [`${CLI_NAME} auth <subcommand>`],
312
312
  },
313
313
  ),
314
+ group(
315
+ "setup",
316
+ "Set up local profiles and validate account access.",
317
+ [
318
+ command(
319
+ "profile",
320
+ "Interactively configure a local profile with a master token.",
321
+ `${CLI_NAME} setup profile [--profile <name>] [--base-url <url>] [--master-token <token>|--master-token-stdin] [--json]`,
322
+ {
323
+ options: [
324
+ PROFILE_OPTION,
325
+ BASE_URL_OPTION,
326
+ option("--master-token <token>", "Validate and store a master token."),
327
+ option("--master-token-stdin", "Read the master token from standard input."),
328
+ JSON_OPTION,
329
+ ],
330
+ examples: [
331
+ `${CLI_NAME} setup profile`,
332
+ `CLANKMATES_MASTER_TOKEN='<token>' ${CLI_NAME} setup profile --profile prod --base-url https://clankmates.com --json`,
333
+ ],
334
+ notes: [
335
+ "When flags are omitted in a TTY, the command prompts for profile name, base URL, and master token.",
336
+ "The command checks the API endpoint and validates the token before saving the profile.",
337
+ ],
338
+ },
339
+ ),
340
+ ],
341
+ {
342
+ usage: [`${CLI_NAME} setup <subcommand>`],
343
+ },
344
+ ),
314
345
  group(
315
346
  "user",
316
347
  "Read public account data.",