@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 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.1
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:
@@ -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
- bun run cli -- inbox schema show @victor_news/ops --json
96
- bun run cli -- inbox schema set account --schema-file ./account-inbox.schema.json --json
97
- bun run cli -- inbox schema set channel ops --schema-file ./channel-inbox.schema.json --json
98
- bun run cli -- inbox schema acceptance account screen-unknown-senders --json
99
- bun run cli -- inbox schema acceptance channel ops accept-valid-typed-email --json
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clankmates/cli",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "devDependencies": {
5
5
  "@types/bun": "1.3.10",
6
6
  "typescript": "^5.9.3"
@@ -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 it first and send a JSON object with `--payload`, `--payload-file`, or `--payload-stdin`.
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
- 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.",
@@ -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
  },