@beryl-so/cli 0.22.0 → 0.24.1
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 +7 -5
- package/dist/adapters/mcp.js +111 -15
- package/dist/beryl-test-skill.js +241 -264
- package/dist/commands/accounts.js +8 -5
- package/dist/commands/auth.js +80 -24
- package/dist/commands/config-vars.js +5 -0
- package/dist/commands/init.js +27 -10
- package/dist/commands/mailboxes.js +5 -1
- package/dist/commands/runs.js +19 -6
- package/dist/commands/tests.js +36 -6
- package/dist/device-login.js +132 -0
- package/dist/skill-tables.js +74 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@ that exposes every command to coding agents.
|
|
|
12
12
|
npx @beryl-so/cli@latest init
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Signs you in
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Safe to re-run.
|
|
15
|
+
Signs you in (browser approval by default, emailed one-time code as fallback — new
|
|
16
|
+
emails are signed up on the spot), wires the beryl + playwright MCP servers into Claude
|
|
17
|
+
Code or Cursor, installs the beryl-test authoring skill, and installs Playwright for
|
|
18
|
+
local runs. Safe to re-run.
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -33,7 +33,8 @@ latest npm release — a stale MCP server silently exposes fewer tools. Set
|
|
|
33
33
|
## Authenticate
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
beryl login #
|
|
36
|
+
beryl login # opens the browser to approve — press Enter for an emailed code instead
|
|
37
|
+
beryl login --otp # skip the browser; emailed one-time code (signs up new emails too)
|
|
37
38
|
export BERYL_API_KEY=beryl_pat_… # CI: use a token from Account → API tokens
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -120,6 +121,7 @@ Manage the personal access tokens that authenticate the CLI and CI.
|
|
|
120
121
|
| `beryl tokens list` | List your personal access tokens | `tokens_list` |
|
|
121
122
|
| `beryl tokens create <name>` | Mint a new personal access token (shown once) | `tokens_create` |
|
|
122
123
|
| `beryl tokens revoke <token-id>` | Revoke a personal access token | `tokens_revoke` |
|
|
124
|
+
| `beryl tokens dismiss <token-id>` | Remove an already-revoked token from your list | `tokens_dismiss` |
|
|
123
125
|
|
|
124
126
|
### workspaces
|
|
125
127
|
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -6,6 +6,7 @@ import { loadConfig } from "../config.js";
|
|
|
6
6
|
import { createContext } from "../context.js";
|
|
7
7
|
import { CliError } from "../errors.js";
|
|
8
8
|
import { ApiClient } from "../http.js";
|
|
9
|
+
import { parseArgv } from "./cli.js";
|
|
9
10
|
import { commands } from "../registry/index.js";
|
|
10
11
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
11
12
|
export function toolName(spec) {
|
|
@@ -31,15 +32,103 @@ export function toolInputSchema(spec) {
|
|
|
31
32
|
required.push(a.name);
|
|
32
33
|
}
|
|
33
34
|
for (const f of spec.flags ?? []) {
|
|
35
|
+
const withDefault = f.default !== undefined ? { default: f.default } : {};
|
|
34
36
|
properties[f.name] =
|
|
35
37
|
f.type === "strings"
|
|
36
|
-
? { type: "array", items: { type: "string" }, description: f.description }
|
|
37
|
-
: {
|
|
38
|
+
? { type: "array", items: { type: "string" }, description: f.description, ...withDefault }
|
|
39
|
+
: {
|
|
40
|
+
type: f.type,
|
|
41
|
+
description: f.description,
|
|
42
|
+
...(f.enum ? { enum: f.enum } : {}),
|
|
43
|
+
...withDefault,
|
|
44
|
+
};
|
|
38
45
|
if (f.required)
|
|
39
46
|
required.push(f.name);
|
|
40
47
|
}
|
|
41
48
|
return { type: "object", properties, ...(required.length ? { required } : {}) };
|
|
42
49
|
}
|
|
50
|
+
function shellTokens(text) {
|
|
51
|
+
const tokens = [];
|
|
52
|
+
let current = "";
|
|
53
|
+
let quote = null;
|
|
54
|
+
let pending = false;
|
|
55
|
+
for (const ch of text) {
|
|
56
|
+
if (quote) {
|
|
57
|
+
if (ch === quote)
|
|
58
|
+
quote = null;
|
|
59
|
+
else
|
|
60
|
+
current += ch;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === '"' || ch === "'") {
|
|
64
|
+
quote = ch;
|
|
65
|
+
pending = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (/\s/.test(ch)) {
|
|
69
|
+
if (pending || current)
|
|
70
|
+
tokens.push(current);
|
|
71
|
+
current = "";
|
|
72
|
+
pending = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (">|;#&".includes(ch))
|
|
76
|
+
return null;
|
|
77
|
+
current += ch;
|
|
78
|
+
pending = true;
|
|
79
|
+
}
|
|
80
|
+
if (quote)
|
|
81
|
+
return null;
|
|
82
|
+
if (pending || current)
|
|
83
|
+
tokens.push(current);
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
/** A CLI example translated to the JSON args the MCP tool takes, or null when it doesn't
|
|
87
|
+
* translate cleanly (shell syntax, another command's example, nothing beyond defaults) —
|
|
88
|
+
* agents must see tool args as JSON, never `--flag` syntax. */
|
|
89
|
+
export function exampleArgs(spec, example) {
|
|
90
|
+
const prefix = `beryl ${spec.name}`;
|
|
91
|
+
if (example !== prefix && !example.startsWith(`${prefix} `))
|
|
92
|
+
return null;
|
|
93
|
+
const tokens = shellTokens(example.slice(prefix.length));
|
|
94
|
+
if (!tokens)
|
|
95
|
+
return null;
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseArgv(spec, tokens);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (parsed.help)
|
|
104
|
+
return null;
|
|
105
|
+
const byName = new Map((spec.flags ?? []).map((f) => [f.name, f]));
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [name, value] of Object.entries(parsed.input.args)) {
|
|
108
|
+
if (value === undefined || (Array.isArray(value) && value.length === 0))
|
|
109
|
+
continue;
|
|
110
|
+
out[name] = value;
|
|
111
|
+
}
|
|
112
|
+
for (const [name, value] of Object.entries(parsed.input.flags)) {
|
|
113
|
+
const f = byName.get(name);
|
|
114
|
+
// parseArgv fills declared defaults in; only what the example explicitly set teaches.
|
|
115
|
+
if (value === undefined || value === f?.default)
|
|
116
|
+
continue;
|
|
117
|
+
out[name] =
|
|
118
|
+
f?.type === "number" && typeof value === "string" && Number.isFinite(Number(value))
|
|
119
|
+
? Number(value)
|
|
120
|
+
: value;
|
|
121
|
+
}
|
|
122
|
+
return Object.keys(out).length ? out : null;
|
|
123
|
+
}
|
|
124
|
+
export function toolDescription(spec) {
|
|
125
|
+
const base = spec.description ? `${spec.summary}. ${spec.description}` : spec.summary;
|
|
126
|
+
const lines = (spec.examples ?? [])
|
|
127
|
+
.map((e) => exampleArgs(spec, e))
|
|
128
|
+
.filter((a) => a !== null)
|
|
129
|
+
.map((a) => `Example: ${JSON.stringify(a)}`);
|
|
130
|
+
return lines.length ? `${base}\n${lines.join("\n")}` : base;
|
|
131
|
+
}
|
|
43
132
|
export function toolResult(result, lines) {
|
|
44
133
|
const parts = [...lines];
|
|
45
134
|
if (result.data !== undefined)
|
|
@@ -102,29 +191,36 @@ export function currentAuth(fallback) {
|
|
|
102
191
|
export function __resetAuthCacheForTests() {
|
|
103
192
|
authCache = undefined;
|
|
104
193
|
}
|
|
194
|
+
// The running version is stated up front because this server is long-lived and never
|
|
195
|
+
// hot-reloads: a session can sit on a days-old build while `@latest` has moved, and
|
|
196
|
+
// "that tool doesn't exist for me" is indistinguishable from a bug without it. Saying
|
|
197
|
+
// it here means the model knows without spending a `version` tool call.
|
|
198
|
+
export function mcpInstructions() {
|
|
199
|
+
const version = cliVersion();
|
|
200
|
+
return (`Beryl CLI v${version} (call the \`version\` tool for the API URL, Node ` +
|
|
201
|
+
"version, and whether this build is behind npm's latest). " +
|
|
202
|
+
"Beryl authors, runs, and heals end-to-end tests for any web app: tests are JSON " +
|
|
203
|
+
"action plans replayed in real cloud browsers, signing in as a durable test " +
|
|
204
|
+
"account whose mail arrives at the project's own mailbox — so signup/OTP/" +
|
|
205
|
+
"magic-link flows are self-contained, with no human login needed. " +
|
|
206
|
+
"The full authoring guide (plan shape, outcome assertions, email/OTP wiring, " +
|
|
207
|
+
"run-fix loop) ships as both the beryl-test skill and the `guide` tool — same " +
|
|
208
|
+
`content. If a beryl-test skill stating v${version} is already loaded, do not ` +
|
|
209
|
+
"call `guide`; if no beryl-test skill is available or it states another version, " +
|
|
210
|
+
"call `guide` before authoring your first test plan.");
|
|
211
|
+
}
|
|
105
212
|
export async function serveMcp(baseCtx) {
|
|
106
213
|
// Fire-and-forget staleness warning: a stale MCP server silently exposes fewer
|
|
107
214
|
// tools, and stderr is the one channel a stdio MCP server can safely log to.
|
|
108
215
|
void warnIfStale(cliVersion(), (msg) => console.error(msg));
|
|
109
216
|
const server = new Server({ name: "beryl", version: cliVersion() }, {
|
|
110
217
|
capabilities: { tools: {} },
|
|
111
|
-
|
|
112
|
-
// hot-reloads: a session can sit on a days-old build while `@latest` has moved, and
|
|
113
|
-
// "that tool doesn't exist for me" is indistinguishable from a bug without it. Saying
|
|
114
|
-
// it here means the model knows without spending a `version` tool call.
|
|
115
|
-
instructions: `Beryl CLI v${cliVersion()} (call the \`version\` tool for the API URL, Node ` +
|
|
116
|
-
"version, and whether this build is behind npm's latest). " +
|
|
117
|
-
"Beryl authors, runs, and heals end-to-end tests for any web app: tests are JSON " +
|
|
118
|
-
"action plans replayed in real cloud browsers, signing in as a durable test " +
|
|
119
|
-
"account whose mail arrives at the project's own mailbox — so signup/OTP/" +
|
|
120
|
-
"magic-link flows are self-contained, with no human login needed. Before " +
|
|
121
|
-
"authoring your first test plan, call the `guide` tool — it returns the full " +
|
|
122
|
-
"authoring guide (plan shape, outcome assertions, email/OTP wiring, run-fix loop).",
|
|
218
|
+
instructions: mcpInstructions(),
|
|
123
219
|
});
|
|
124
220
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
125
221
|
tools: mcpTools().map((spec) => ({
|
|
126
222
|
name: toolName(spec),
|
|
127
|
-
description: spec
|
|
223
|
+
description: toolDescription(spec),
|
|
128
224
|
inputSchema: toolInputSchema(spec),
|
|
129
225
|
})),
|
|
130
226
|
}));
|