@beryl-so/cli 0.1.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.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/adapters/cli.js +270 -0
- package/dist/adapters/mcp.js +106 -0
- package/dist/commands/account.js +89 -0
- package/dist/commands/auth.js +135 -0
- package/dist/commands/config-vars.js +166 -0
- package/dist/commands/credentials.js +144 -0
- package/dist/commands/environments.js +147 -0
- package/dist/commands/explorations.js +50 -0
- package/dist/commands/init.js +189 -0
- package/dist/commands/mcp.js +15 -0
- package/dist/commands/projects.js +177 -0
- package/dist/commands/runs.js +126 -0
- package/dist/commands/tests.js +239 -0
- package/dist/commands/util.js +43 -0
- package/dist/commands/watch.js +113 -0
- package/dist/commands/workspaces.js +220 -0
- package/dist/config.js +63 -0
- package/dist/context.js +101 -0
- package/dist/errors.js +21 -0
- package/dist/http.js +98 -0
- package/dist/index.js +8 -0
- package/dist/output.js +80 -0
- package/dist/registry/index.js +74 -0
- package/dist/registry/types.js +1 -0
- package/dist/sse.js +98 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vibe Monitor, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<!-- GENERATED by `npm run docs` — edit the command registry (src/commands/), not this file. -->
|
|
2
|
+
|
|
3
|
+
# beryl CLI
|
|
4
|
+
|
|
5
|
+
Beryl on the command line: create projects, watch the agent explore and author tests,
|
|
6
|
+
trigger runs in CI, manage everything the web app can — plus an MCP server (`beryl mcp`)
|
|
7
|
+
that exposes every command to coding agents.
|
|
8
|
+
|
|
9
|
+
## Quickstart — one command
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @beryl-so/cli@latest init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Signs you in, pins this repo to a project (creating it if needed — the agent starts
|
|
16
|
+
exploring and authoring tests immediately), and wires the MCP server into Claude Code
|
|
17
|
+
or Cursor. Safe to re-run.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @beryl-so/cli # `beryl` on your PATH
|
|
23
|
+
npx @beryl-so/cli@latest … # or run one-off without installing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
From the monorepo (development): `cd cli && npm install && npm run build && npm link`.
|
|
27
|
+
|
|
28
|
+
## Authenticate
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
beryl login # emailed one-time code; mints + stores a personal access token
|
|
32
|
+
export BERYL_API_KEY=beryl_pat_… # CI: use a token from Account → API tokens
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Defaults can be pinned per repo in a `.beryl.json` (`{"workspace": "…", "project": "…"}`)
|
|
36
|
+
or globally via `beryl workspaces use` / `beryl projects use`.
|
|
37
|
+
Precedence: flags > `BERYL_*` env > `.beryl.json` > `~/.config/beryl/config.json`.
|
|
38
|
+
|
|
39
|
+
## CI in one line
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
beryl runs trigger --url-override "$PREVIEW_URL" --watch --timeout 30
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Exit codes: `0` success / all tests passed · `1` failure · `2` usage error · `3` auth error.
|
|
46
|
+
|
|
47
|
+
## MCP server
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
claude mcp add beryl -- npx -y @beryl-so/cli mcp # no install needed
|
|
51
|
+
claude mcp add beryl -- beryl mcp # or, with the CLI installed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Every non-interactive command below is exposed as an MCP tool with the same name
|
|
55
|
+
(spaces and dashes become underscores).
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### init
|
|
60
|
+
|
|
61
|
+
| Command | Summary |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `beryl init` | Set up Beryl in this repo — sign in, pin a project, wire up your coding agent |
|
|
64
|
+
|
|
65
|
+
### login
|
|
66
|
+
|
|
67
|
+
| Command | Summary |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `beryl login` | Authenticate the CLI with your Beryl account |
|
|
70
|
+
|
|
71
|
+
### logout
|
|
72
|
+
|
|
73
|
+
| Command | Summary |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `beryl logout` | Remove the stored token from the CLI config |
|
|
76
|
+
|
|
77
|
+
### whoami
|
|
78
|
+
|
|
79
|
+
| Command | Summary |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `beryl whoami` | Show the signed-in account and the CLI's resolved defaults |
|
|
82
|
+
|
|
83
|
+
### tokens
|
|
84
|
+
|
|
85
|
+
| Command | Summary |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `beryl tokens list` | List your personal access tokens |
|
|
88
|
+
| `beryl tokens create <name>` | Mint a new personal access token (shown once) |
|
|
89
|
+
| `beryl tokens revoke <token-id>` | Revoke a personal access token |
|
|
90
|
+
|
|
91
|
+
### workspaces
|
|
92
|
+
|
|
93
|
+
| Command | Summary |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `beryl workspaces list` | List workspaces you belong to |
|
|
96
|
+
| `beryl workspaces get` | Show one workspace |
|
|
97
|
+
| `beryl workspaces create <name>` | Create a workspace |
|
|
98
|
+
| `beryl workspaces update` | Rename a workspace or change its visibility |
|
|
99
|
+
| `beryl workspaces delete` | Delete a workspace and everything in it |
|
|
100
|
+
| `beryl workspaces use <workspace>` | Set the default workspace for future commands |
|
|
101
|
+
| `beryl workspaces history` | Show the workspace's action history (who did what, when) |
|
|
102
|
+
| `beryl workspaces leave` | Leave a workspace you are a member of |
|
|
103
|
+
|
|
104
|
+
### members
|
|
105
|
+
|
|
106
|
+
| Command | Summary |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `beryl members list` | List workspace members |
|
|
109
|
+
| `beryl members set-role <user-id> <role>` | Change a member's role |
|
|
110
|
+
| `beryl members remove <user-id>` | Remove a member from the workspace |
|
|
111
|
+
|
|
112
|
+
### invites
|
|
113
|
+
|
|
114
|
+
| Command | Summary |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `beryl invites send <email>` | Invite someone to the workspace by email |
|
|
117
|
+
| `beryl invites list` | List the workspace's outstanding invitations |
|
|
118
|
+
| `beryl invites revoke <invitation-id>` | Revoke a pending invitation |
|
|
119
|
+
| `beryl invites mine` | List invitations sent to you |
|
|
120
|
+
| `beryl invites accept <invitation>` | Accept an invitation (by id, or by the token from the invite email) |
|
|
121
|
+
| `beryl invites decline <invitation-id>` | Decline an invitation |
|
|
122
|
+
|
|
123
|
+
### projects
|
|
124
|
+
|
|
125
|
+
| Command | Summary |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `beryl projects list` | List projects in the workspace |
|
|
128
|
+
| `beryl projects get` | Show one project, including its current exploration state |
|
|
129
|
+
| `beryl projects create <url>` | Create a project — the agent starts exploring and authoring tests immediately |
|
|
130
|
+
| `beryl projects rename <name>` | Rename a project |
|
|
131
|
+
| `beryl projects delete` | Delete a project and all its tests and runs |
|
|
132
|
+
| `beryl projects re-explore` | Send the agent back in — run/heal existing tests and discover new flows |
|
|
133
|
+
| `beryl projects report` | Aggregate quality report across recent runs (pass rates, flaky tests, trend) |
|
|
134
|
+
| `beryl projects reusable-auth <url>` | Check whether a saved login can be reused for a URL before creating a project |
|
|
135
|
+
| `beryl projects use <project>` | Set the default project for future commands |
|
|
136
|
+
|
|
137
|
+
### envs
|
|
138
|
+
|
|
139
|
+
| Command | Summary |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `beryl envs list` | List a project's environments |
|
|
142
|
+
| `beryl envs get <env-id>` | Show one environment |
|
|
143
|
+
| `beryl envs create <name> <url>` | Add an environment (e.g. staging) to a project |
|
|
144
|
+
| `beryl envs update <env-id>` | Update an environment's name, URL, or auth settings |
|
|
145
|
+
| `beryl envs delete <env-id>` | Delete an environment |
|
|
146
|
+
|
|
147
|
+
### schedule
|
|
148
|
+
|
|
149
|
+
| Command | Summary |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `beryl schedule get` | Show the project's daily/weekly run schedule |
|
|
152
|
+
| `beryl schedule set` | Enable scheduled runs (daily, or weekly on a given day) |
|
|
153
|
+
| `beryl schedule disable` | Turn scheduled runs off |
|
|
154
|
+
|
|
155
|
+
### tests
|
|
156
|
+
|
|
157
|
+
| Command | Summary |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `beryl tests list` | List the project's tests with their latest result |
|
|
160
|
+
| `beryl tests get <test-id>` | Show one test |
|
|
161
|
+
| `beryl tests plan <test-id>` | Print a test's current step plan (JSON) |
|
|
162
|
+
| `beryl tests create` | Create a test case from a JSON action plan — for tests authored locally, e.g. by your coding agent |
|
|
163
|
+
| `beryl tests set-plan <test-id>` | Replace a test's step plan from a JSON file (creates a new version) |
|
|
164
|
+
| `beryl tests recompile <test-id>` | Validate + verify an edited plan against the live site before persisting |
|
|
165
|
+
| `beryl tests versions <test-id>` | List a test's version history |
|
|
166
|
+
| `beryl tests version <test-id> <version-no>` | Show one specific version of a test (including its plan) |
|
|
167
|
+
| `beryl tests diff <test-id> <from> <to>` | Diff two versions of a test's plan |
|
|
168
|
+
| `beryl tests restore <test-id> <version-no>` | Restore a test to an earlier version |
|
|
169
|
+
| `beryl tests reset <test-id>` | Discard user edits and return the test to its latest system-authored version |
|
|
170
|
+
| `beryl tests heal <test-id> <state>` | Turn self-healing on or off for a test |
|
|
171
|
+
| `beryl tests history <test-id>` | Pass/fail history, streak, and stability for a test |
|
|
172
|
+
| `beryl tests script <test-id>` | Print the rendered Playwright spec for a test |
|
|
173
|
+
| `beryl tests export <test-ids...>` | Export tests as Playwright .spec.ts files in a ZIP |
|
|
174
|
+
|
|
175
|
+
### runs
|
|
176
|
+
|
|
177
|
+
| Command | Summary |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `beryl runs trigger` | Trigger a test run (whole suite, a subset, or one environment) |
|
|
180
|
+
| `beryl runs list` | List recent runs |
|
|
181
|
+
| `beryl runs get <run-id>` | Show one run with its per-test results |
|
|
182
|
+
| `beryl runs watch <run-id>` | Attach to a run and stream progress until it finishes |
|
|
183
|
+
| `beryl runs cancel <run-id>` | Cancel an in-flight run |
|
|
184
|
+
| `beryl runs report <run-id>` | Show the generated report for a run |
|
|
185
|
+
| `beryl runs download <run-id>` | Download a run's full results as JSON |
|
|
186
|
+
| `beryl runs explain <result-id>` | AI explanation of why a test result failed |
|
|
187
|
+
|
|
188
|
+
### explorations
|
|
189
|
+
|
|
190
|
+
| Command | Summary |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `beryl explorations list` | List the agent's exploration passes for a project |
|
|
193
|
+
| `beryl explorations get <exploration-id>` | Show one exploration: authored tests, abandoned flows, coverage, frontier |
|
|
194
|
+
| `beryl explorations steps <exploration-id>` | List every step the agent took in an exploration |
|
|
195
|
+
| `beryl explorations watch <exploration-id>` | Stream an exploration live — watch the agent explore and author tests |
|
|
196
|
+
|
|
197
|
+
### config
|
|
198
|
+
|
|
199
|
+
| Command | Summary |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| `beryl config vars list` | List the project's config variables (visible to the agent during runs) |
|
|
202
|
+
| `beryl config vars set <key> <value>` | Create or update a config variable |
|
|
203
|
+
| `beryl config vars delete <key>` | Delete a config variable |
|
|
204
|
+
| `beryl config secrets list` | List the project's secrets (values are never returned) |
|
|
205
|
+
| `beryl config secrets set <key> <value>` | Create a secret (write-only; re-setting a key replaces it) |
|
|
206
|
+
| `beryl config secrets delete <key>` | Delete a secret |
|
|
207
|
+
| `beryl config files list` | List files uploaded for the agent to use (e.g. CSVs, upload fixtures) |
|
|
208
|
+
| `beryl config files upload <file>` | Upload a file |
|
|
209
|
+
| `beryl config files download <file-id>` | Get a short-lived download URL for a file |
|
|
210
|
+
| `beryl config files delete <file-id>` | Delete an uploaded file |
|
|
211
|
+
|
|
212
|
+
### credentials
|
|
213
|
+
|
|
214
|
+
| Command | Summary |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `beryl credentials list` | List the workspace's saved logins |
|
|
217
|
+
| `beryl credentials get <credential-id>` | Show one saved login (status and freshness — never the session itself) |
|
|
218
|
+
| `beryl credentials projects <credential-id>` | List the projects using a saved login |
|
|
219
|
+
| `beryl credentials delete <credential-id>` | Delete a saved login |
|
|
220
|
+
| `beryl credentials attach <credential-id>` | Attach a saved login to a project |
|
|
221
|
+
| `beryl credentials detach` | Detach the project's saved login |
|
|
222
|
+
| `beryl credentials recapture <credential-id>` | Start a re-capture for an expiring saved login (returns a live browser URL) |
|
|
223
|
+
| `beryl credentials capture` | Capture a login for the project interactively: log in once in a real browser |
|
|
224
|
+
|
|
225
|
+
### auth-capture
|
|
226
|
+
|
|
227
|
+
| Command | Summary |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `beryl auth-capture start` | Start a login-capture browser session for the project (non-interactive) |
|
|
230
|
+
| `beryl auth-capture capture <session-id>` | Capture the session after the user has logged in via the live-view URL |
|
|
231
|
+
| `beryl auth-capture refresh <session-id>` | Capture a refreshed session for a project whose login is expiring |
|
|
232
|
+
| `beryl auth-capture release <session-id>` | Release a login-capture browser session without capturing |
|
|
233
|
+
|
|
234
|
+
### account
|
|
235
|
+
|
|
236
|
+
| Command | Summary |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `beryl account get` | Show your account profile |
|
|
239
|
+
| `beryl account update` | Update your profile |
|
|
240
|
+
| `beryl account deletion-preview` | Preview what deleting your account would remove or leave |
|
|
241
|
+
|
|
242
|
+
### feedback
|
|
243
|
+
|
|
244
|
+
| Command | Summary |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| `beryl feedback send <message>` | Send product feedback to the Beryl team |
|
|
247
|
+
|
|
248
|
+
### billing
|
|
249
|
+
|
|
250
|
+
| Command | Summary |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `beryl billing usage` | Show plan usage: services and weekly AI units |
|
|
253
|
+
| `beryl billing subscription` | Show the workspace's subscription |
|
|
254
|
+
| `beryl billing invoices` | List recent invoices |
|
|
255
|
+
| `beryl billing portal` | Get a Stripe billing-portal link for the workspace |
|
|
256
|
+
|
|
257
|
+
### mcp
|
|
258
|
+
|
|
259
|
+
| Command | Summary |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `beryl mcp` | Run the Beryl MCP server (stdio) — every CLI command as an agent tool |
|
|
262
|
+
|
|
263
|
+
Run `beryl <command> --help` for flags and examples.
|
|
264
|
+
|
|
265
|
+
## Global flags
|
|
266
|
+
|
|
267
|
+
- `--json` — Print machine-readable JSON (NDJSON for streams)
|
|
268
|
+
- `--api-url` — API base URL (default https://api.beryl.so)
|
|
269
|
+
- `--token` — Personal access token (overrides config/BERYL_API_KEY)
|
|
270
|
+
- `--help` — Show help
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { createContext } from "../context.js";
|
|
4
|
+
import { CliError, EXIT_OK, EXIT_USAGE, UsageError } from "../errors.js";
|
|
5
|
+
import { ApiClient } from "../http.js";
|
|
6
|
+
import { autoFormat, bold, cyan, dim } from "../output.js";
|
|
7
|
+
import { commandGroups, findCommand } from "../registry/index.js";
|
|
8
|
+
export const GLOBAL_FLAGS = [
|
|
9
|
+
{ name: "json", description: "Print machine-readable JSON (NDJSON for streams)" },
|
|
10
|
+
{ name: "api-url", description: "API base URL (default https://api.beryl.so)", value: true },
|
|
11
|
+
{ name: "token", description: "Personal access token (overrides config/BERYL_API_KEY)", value: true },
|
|
12
|
+
{ name: "help", description: "Show help", alias: "h" },
|
|
13
|
+
];
|
|
14
|
+
export function cliVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function parseArgv(spec, tokens) {
|
|
24
|
+
const flags = {};
|
|
25
|
+
const positional = [];
|
|
26
|
+
let json = false;
|
|
27
|
+
let apiUrl;
|
|
28
|
+
let token;
|
|
29
|
+
let help = false;
|
|
30
|
+
let flagsDone = false;
|
|
31
|
+
const specFlags = spec.flags ?? [];
|
|
32
|
+
const byName = new Map(specFlags.map((f) => [f.name, f]));
|
|
33
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34
|
+
const tok = tokens[i];
|
|
35
|
+
if (flagsDone || !tok.startsWith("-") || tok === "-") {
|
|
36
|
+
positional.push(tok);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (tok === "--") {
|
|
40
|
+
flagsDone = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (tok === "-h" || tok === "--help") {
|
|
44
|
+
help = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const eq = tok.indexOf("=");
|
|
48
|
+
const name = (eq === -1 ? tok : tok.slice(0, eq)).replace(/^--?/, "");
|
|
49
|
+
const inline = eq === -1 ? undefined : tok.slice(eq + 1);
|
|
50
|
+
const takeValue = (flagName) => {
|
|
51
|
+
if (inline !== undefined)
|
|
52
|
+
return inline;
|
|
53
|
+
const next = tokens[++i];
|
|
54
|
+
if (next === undefined)
|
|
55
|
+
throw new UsageError(`--${flagName} needs a value`);
|
|
56
|
+
return next;
|
|
57
|
+
};
|
|
58
|
+
// A command-declared flag wins over the like-named global (e.g. `login --token`).
|
|
59
|
+
if (byName.has(name)) {
|
|
60
|
+
const f = byName.get(name);
|
|
61
|
+
if (f.type === "boolean") {
|
|
62
|
+
flags[name] = inline !== undefined ? inline !== "false" : true;
|
|
63
|
+
}
|
|
64
|
+
else if (f.type === "strings") {
|
|
65
|
+
const list = flags[name] ?? [];
|
|
66
|
+
list.push(takeValue(name));
|
|
67
|
+
flags[name] = list;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const value = takeValue(name);
|
|
71
|
+
if (f.enum && !f.enum.includes(value)) {
|
|
72
|
+
throw new UsageError(`--${name} must be one of: ${f.enum.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
flags[name] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (name === "json") {
|
|
78
|
+
json = true;
|
|
79
|
+
}
|
|
80
|
+
else if (name === "api-url") {
|
|
81
|
+
apiUrl = takeValue(name);
|
|
82
|
+
}
|
|
83
|
+
else if (name === "token") {
|
|
84
|
+
token = takeValue(name);
|
|
85
|
+
}
|
|
86
|
+
else if (name.startsWith("no-") && byName.get(name.slice(3))?.type === "boolean") {
|
|
87
|
+
flags[name.slice(3)] = false;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new UsageError(`Unknown flag --${name} for \`beryl ${spec.name}\``);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const f of specFlags) {
|
|
94
|
+
if (flags[f.name] === undefined && f.default !== undefined)
|
|
95
|
+
flags[f.name] = f.default;
|
|
96
|
+
if (f.required && flags[f.name] === undefined) {
|
|
97
|
+
throw new UsageError(`--${f.name} is required for \`beryl ${spec.name}\``);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const args = {};
|
|
101
|
+
const specArgs = spec.args ?? [];
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
for (const a of specArgs) {
|
|
104
|
+
if (a.variadic) {
|
|
105
|
+
const rest = positional.slice(cursor);
|
|
106
|
+
cursor = positional.length;
|
|
107
|
+
if (a.required && rest.length === 0)
|
|
108
|
+
throw new UsageError(`Missing <${a.name}>`);
|
|
109
|
+
args[a.name] = rest;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const value = positional[cursor++];
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
if (a.required)
|
|
115
|
+
throw new UsageError(`Missing <${a.name}>\n\n${usageLine(spec)}`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
args[a.name] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (cursor < positional.length) {
|
|
123
|
+
throw new UsageError(`Unexpected argument "${positional[cursor]}"\n\n${usageLine(spec)}`);
|
|
124
|
+
}
|
|
125
|
+
return { input: { args, flags }, json, apiUrl, token, help };
|
|
126
|
+
}
|
|
127
|
+
export function usageLine(spec) {
|
|
128
|
+
const args = (spec.args ?? [])
|
|
129
|
+
.map((a) => {
|
|
130
|
+
const inner = a.variadic ? `${a.name}...` : a.name;
|
|
131
|
+
return a.required ? `<${inner}>` : `[${inner}]`;
|
|
132
|
+
})
|
|
133
|
+
.join(" ");
|
|
134
|
+
return `Usage: beryl ${spec.name}${args ? " " + args : ""} [flags]`;
|
|
135
|
+
}
|
|
136
|
+
export function commandHelp(spec) {
|
|
137
|
+
const lines = [spec.summary, "", usageLine(spec)];
|
|
138
|
+
if (spec.description)
|
|
139
|
+
lines.push("", spec.description);
|
|
140
|
+
if (spec.args?.length) {
|
|
141
|
+
lines.push("", bold("Arguments:"));
|
|
142
|
+
for (const a of spec.args)
|
|
143
|
+
lines.push(` ${cyan(a.name.padEnd(16))} ${a.description}`);
|
|
144
|
+
}
|
|
145
|
+
if (spec.flags?.length) {
|
|
146
|
+
lines.push("", bold("Flags:"));
|
|
147
|
+
for (const f of spec.flags) {
|
|
148
|
+
const label = `--${f.name}${f.type === "boolean" ? "" : ` <${f.placeholder ?? f.type}>`}`;
|
|
149
|
+
const suffix = f.enum ? ` (${f.enum.join("|")})` : "";
|
|
150
|
+
lines.push(` ${cyan(label.padEnd(24))} ${f.description}${suffix}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
lines.push("", bold("Global flags:"));
|
|
154
|
+
for (const g of GLOBAL_FLAGS) {
|
|
155
|
+
lines.push(` ${cyan(("--" + g.name).padEnd(24))} ${g.description}`);
|
|
156
|
+
}
|
|
157
|
+
if (spec.examples?.length) {
|
|
158
|
+
lines.push("", bold("Examples:"));
|
|
159
|
+
for (const e of spec.examples)
|
|
160
|
+
lines.push(` ${dim(e)}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
export function rootHelp() {
|
|
165
|
+
const lines = [
|
|
166
|
+
`${bold("beryl")} ${dim("v" + cliVersion())} — AI-agent web testing from the command line`,
|
|
167
|
+
"",
|
|
168
|
+
"Usage: beryl <command> [args] [flags]",
|
|
169
|
+
"",
|
|
170
|
+
];
|
|
171
|
+
for (const [group, specs] of commandGroups()) {
|
|
172
|
+
if (specs.length === 1 && specs[0].name === group) {
|
|
173
|
+
lines.push(` ${cyan(group.padEnd(14))} ${specs[0].summary}`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
lines.push(` ${cyan(group.padEnd(14))} ${specs
|
|
177
|
+
.map((s) => s.name.split(" ").slice(1).join(" "))
|
|
178
|
+
.join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push("", `Run ${cyan("beryl <command> --help")} for details, ${cyan("beryl login")} to get started.`, `Docs: ${cyan("https://beryl.so/docs/cli")}`);
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
function groupHelp(group, specs) {
|
|
185
|
+
const lines = [`${bold("beryl " + group)} — subcommands:`, ""];
|
|
186
|
+
for (const spec of specs) {
|
|
187
|
+
lines.push(` ${cyan(spec.name.padEnd(28))} ${spec.summary}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push("", `Run ${cyan(`beryl ${group} <subcommand> --help`)} for details.`);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
export async function runCli(argv) {
|
|
193
|
+
const words = [];
|
|
194
|
+
let rest = argv;
|
|
195
|
+
for (const tok of argv) {
|
|
196
|
+
if (tok.startsWith("-"))
|
|
197
|
+
break;
|
|
198
|
+
words.push(tok);
|
|
199
|
+
}
|
|
200
|
+
if (argv.includes("--version") || argv[0] === "version") {
|
|
201
|
+
process.stdout.write(cliVersion() + "\n");
|
|
202
|
+
return EXIT_OK;
|
|
203
|
+
}
|
|
204
|
+
if (words[0] === "help") {
|
|
205
|
+
words.shift();
|
|
206
|
+
rest = [...words, "--help"];
|
|
207
|
+
}
|
|
208
|
+
if (words.length === 0) {
|
|
209
|
+
process.stdout.write(rootHelp() + "\n");
|
|
210
|
+
return argv.length === 0 || argv.includes("--help") || argv.includes("-h")
|
|
211
|
+
? EXIT_OK
|
|
212
|
+
: EXIT_USAGE;
|
|
213
|
+
}
|
|
214
|
+
const found = findCommand(words);
|
|
215
|
+
if (!found) {
|
|
216
|
+
const groups = commandGroups();
|
|
217
|
+
const group = groups.get(words[0]);
|
|
218
|
+
if (group && words.length === 1) {
|
|
219
|
+
process.stdout.write(groupHelp(words[0], group) + "\n");
|
|
220
|
+
return EXIT_OK;
|
|
221
|
+
}
|
|
222
|
+
process.stderr.write(`Unknown command: ${words.join(" ")}\n\nRun \`beryl --help\` for the command list.\n`);
|
|
223
|
+
return EXIT_USAGE;
|
|
224
|
+
}
|
|
225
|
+
const { spec } = found;
|
|
226
|
+
const tokens = rest.slice(found.consumed);
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = parseArgv(spec, tokens);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (err instanceof UsageError) {
|
|
233
|
+
process.stderr.write(err.message + "\n");
|
|
234
|
+
return err.exitCode;
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
if (parsed.help) {
|
|
239
|
+
process.stdout.write(commandHelp(spec) + "\n");
|
|
240
|
+
return EXIT_OK;
|
|
241
|
+
}
|
|
242
|
+
const config = loadConfig();
|
|
243
|
+
if (parsed.apiUrl)
|
|
244
|
+
config.apiUrl = parsed.apiUrl;
|
|
245
|
+
if (parsed.token)
|
|
246
|
+
config.token = parsed.token;
|
|
247
|
+
const client = new ApiClient(config.apiUrl, config.token);
|
|
248
|
+
const ctx = createContext({ client, config, json: parsed.json });
|
|
249
|
+
try {
|
|
250
|
+
const result = (await spec.run(ctx, parsed.input)) ?? {};
|
|
251
|
+
if (parsed.json) {
|
|
252
|
+
if (result.data !== undefined)
|
|
253
|
+
process.stdout.write(JSON.stringify(result.data) + "\n");
|
|
254
|
+
}
|
|
255
|
+
else if (result.human !== undefined) {
|
|
256
|
+
process.stdout.write(result.human + "\n");
|
|
257
|
+
}
|
|
258
|
+
else if (result.data !== undefined) {
|
|
259
|
+
process.stdout.write(autoFormat(result.data) + "\n");
|
|
260
|
+
}
|
|
261
|
+
return result.exitCode ?? EXIT_OK;
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
if (err instanceof CliError) {
|
|
265
|
+
process.stderr.write(err.message + "\n");
|
|
266
|
+
return err.exitCode;
|
|
267
|
+
}
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { createContext } from "../context.js";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
import { commands } from "../registry/index.js";
|
|
7
|
+
import { cliVersion } from "./cli.js";
|
|
8
|
+
export function toolName(spec) {
|
|
9
|
+
return spec.name.replace(/ /g, "_").replace(/-/g, "_");
|
|
10
|
+
}
|
|
11
|
+
export function mcpTools() {
|
|
12
|
+
return commands.filter((c) => !c.interactive && !c.hidden && c.name !== "mcp");
|
|
13
|
+
}
|
|
14
|
+
export function toolInputSchema(spec) {
|
|
15
|
+
const properties = {};
|
|
16
|
+
const required = [];
|
|
17
|
+
for (const a of spec.args ?? []) {
|
|
18
|
+
properties[a.name] = a.variadic
|
|
19
|
+
? { type: "array", items: { type: "string" }, description: a.description }
|
|
20
|
+
: { type: "string", description: a.description };
|
|
21
|
+
if (a.required)
|
|
22
|
+
required.push(a.name);
|
|
23
|
+
}
|
|
24
|
+
for (const f of spec.flags ?? []) {
|
|
25
|
+
properties[f.name] =
|
|
26
|
+
f.type === "strings"
|
|
27
|
+
? { type: "array", items: { type: "string" }, description: f.description }
|
|
28
|
+
: { type: f.type, description: f.description, ...(f.enum ? { enum: f.enum } : {}) };
|
|
29
|
+
if (f.required)
|
|
30
|
+
required.push(f.name);
|
|
31
|
+
}
|
|
32
|
+
return { type: "object", properties, ...(required.length ? { required } : {}) };
|
|
33
|
+
}
|
|
34
|
+
function toInput(spec, params) {
|
|
35
|
+
const args = {};
|
|
36
|
+
const flags = {};
|
|
37
|
+
const argNames = new Set((spec.args ?? []).map((a) => a.name));
|
|
38
|
+
for (const [key, value] of Object.entries(params)) {
|
|
39
|
+
if (value === undefined || value === null)
|
|
40
|
+
continue;
|
|
41
|
+
if (argNames.has(key)) {
|
|
42
|
+
args[key] = value;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
flags[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { args, flags };
|
|
49
|
+
}
|
|
50
|
+
export async function serveMcp(baseCtx) {
|
|
51
|
+
const server = new Server({ name: "beryl", version: cliVersion() }, { capabilities: { tools: {} } });
|
|
52
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
53
|
+
tools: mcpTools().map((spec) => ({
|
|
54
|
+
name: toolName(spec),
|
|
55
|
+
description: spec.description ? `${spec.summary}. ${spec.description}` : spec.summary,
|
|
56
|
+
inputSchema: toolInputSchema(spec),
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
60
|
+
const spec = mcpTools().find((c) => toolName(c) === request.params.name);
|
|
61
|
+
if (!spec) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const lines = [];
|
|
68
|
+
const push = (text) => {
|
|
69
|
+
lines.push(text);
|
|
70
|
+
// Long watches can stream hundreds of events; keep the tool result bounded.
|
|
71
|
+
if (lines.length > 400)
|
|
72
|
+
lines.splice(0, lines.length - 400);
|
|
73
|
+
};
|
|
74
|
+
const ctx = createContext({
|
|
75
|
+
client: baseCtx.client,
|
|
76
|
+
config: baseCtx.config,
|
|
77
|
+
json: true,
|
|
78
|
+
interactive: false,
|
|
79
|
+
out: push,
|
|
80
|
+
err: push,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const result = (await spec.run(ctx, toInput(spec, request.params.arguments ?? {}))) ?? {};
|
|
84
|
+
const parts = [...lines];
|
|
85
|
+
if (result.data !== undefined)
|
|
86
|
+
parts.push(JSON.stringify(result.data, null, 2));
|
|
87
|
+
else if (result.human)
|
|
88
|
+
parts.push(result.human);
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: parts.join("\n") || "ok" }],
|
|
91
|
+
isError: result.exitCode !== undefined && result.exitCode !== 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof CliError ? err.message : String(err);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: [...lines, message].join("\n") }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
await server.connect(new StdioServerTransport());
|
|
103
|
+
await new Promise((resolve) => {
|
|
104
|
+
server.onclose = resolve;
|
|
105
|
+
});
|
|
106
|
+
}
|