@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 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, pins this repo to a project (creating it if needed and asking whether
16
- you want to author tests locally with your own coding agent, the default, or let Beryl's
17
- agent explore and author them), and wires the MCP server into Claude Code or Cursor.
18
- Safe to re-run.
15
+ Signs you in (browser approval by default, emailed one-time code as fallbacknew
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 # emailed one-time code; mints + stores a personal access token
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
 
@@ -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
- : { type: f.type, description: f.description, ...(f.enum ? { enum: f.enum } : {}) };
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
- // The running version is stated up front because this server is long-lived and never
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.description ? `${spec.summary}. ${spec.description}` : spec.summary,
223
+ description: toolDescription(spec),
128
224
  inputSchema: toolInputSchema(spec),
129
225
  })),
130
226
  }));