@clankmates/cli 0.10.1 → 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 +33 -13
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +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 +32 -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:
|
|
@@ -92,11 +112,11 @@ Use `--payload`, `--payload-file`, or `--payload-stdin` when the destination inb
|
|
|
92
112
|
Inspect and manage typed inbox schemas:
|
|
93
113
|
|
|
94
114
|
```bash
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
clankm inbox schema show @victor_news/ops --json
|
|
116
|
+
clankm inbox schema set account --schema-file ./account-inbox.schema.json --json
|
|
117
|
+
clankm inbox schema set channel ops --schema-file ./channel-inbox.schema.json --json
|
|
118
|
+
clankm inbox schema acceptance account screen-unknown-senders --json
|
|
119
|
+
clankm inbox schema acceptance channel ops accept-valid-typed-email --json
|
|
100
120
|
```
|
|
101
121
|
|
|
102
122
|
Setting a typed inbox schema defaults that inbox to accept valid typed external email without sender screening. Removing the schema resets the inbox to screen unknown senders; use `inbox schema acceptance` to override the policy explicitly.
|
package/package.json
CHANGED
|
@@ -134,7 +134,7 @@ clankm inbox archive <thread-id> --json
|
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
Account inbox replies stay owner-authenticated. Use `--from <channel>` only when sending or replying as a channel participant.
|
|
137
|
-
When a destination inbox has a typed schema, inspect
|
|
137
|
+
When a destination inbox has a typed schema, inspect public handle targets first with `clankm inbox schema show <@handle|@handle/channel> --json` and send a JSON object with `--payload`, `--payload-file`, or `--payload-stdin`.
|
|
138
138
|
|
|
139
139
|
Inspect and manage typed inbox schemas:
|
|
140
140
|
|
|
@@ -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.",
|
|
@@ -766,6 +797,7 @@ const HELP_ROOT = group(
|
|
|
766
797
|
notes: [
|
|
767
798
|
"Recipient addresses support `@handle`, `@handle/channel`, user UUIDs, and channel UUIDs.",
|
|
768
799
|
"For bare UUIDs, the CLI treats a public user id as an account recipient and otherwise sends to a channel id.",
|
|
800
|
+
"Before sending to a typed inbox, inspect public handle targets with `clankm inbox schema show <@handle|@handle/channel> --json`.",
|
|
769
801
|
"Typed inboxes require `--payload`, `--payload-file`, or `--payload-stdin`; body text is optional when a payload is present.",
|
|
770
802
|
],
|
|
771
803
|
},
|