@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 +28 -8
- package/package.json +1 -1
- package/skills/codex/clankmates/references/setup.md +15 -6
- package/src/cli.ts +2 -0
- package/src/commands/setup.ts +228 -0
- package/src/lib/args.ts +2 -0
- package/src/lib/help.ts +31 -0
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.
|
|
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
|
-
|
|
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 --
|
|
73
|
+
CLANKMATES_MASTER_TOKEN='<master-token>' bun run cli -- setup profile --profile prod --base-url https://clankmates.com --json
|
|
57
74
|
```
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
The setup helper replaces the manual bootstrap sequence:
|
|
60
77
|
|
|
61
78
|
```bash
|
|
62
|
-
bun run cli --
|
|
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
|
@@ -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
|
-
|
|
7
|
+
Install Bun first if needed: <https://bun.sh/docs/installation>
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
bun install -g @clankmates/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Set up a profile interactively:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
clankm
|
|
16
|
+
clankm setup profile --profile <local-profile-name>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
For agents or scripts, provide the token without prompting:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
clankm
|
|
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.",
|