@beryl-so/cli 0.17.0 → 0.22.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/README.md CHANGED
@@ -288,17 +288,37 @@ Send run outcomes to a Slack channel via a per-project incoming webhook.
288
288
  | `beryl slack clear` | Remove the project's Slack webhook (stops all alerts) | — |
289
289
  | `beryl slack test` | Post a sample alert to the configured webhook | — |
290
290
 
291
- ### inbox
291
+ ### mailbox
292
292
 
293
- Email inboxes for testing flows that send mail signups, OTPs, receipts.
293
+ The project's standing email addresses where its tests receive sign-in mail.
294
+
295
+ `beryl mailbox` with no subcommand runs `mailbox get`.
296
+
297
+ | Command | Summary | MCP tool |
298
+ | --- | --- | --- |
299
+ | `beryl mailbox get` | The project's mailbox address | `mailbox_get` |
300
+ | `beryl mailbox list` | List the project's mailboxes | `mailbox_list` |
301
+ | `beryl mailbox create` | Add a second mailbox to the project | `mailbox_create` |
302
+ | `beryl mailbox delete <mailbox-id>` | Delete a mailbox and every email it has received | `mailbox_delete` |
303
+ | `beryl mailbox read <mailbox-id>` | Read the latest email in a mailbox (waits for one to arrive) | `mailbox_read` |
304
+ | `beryl mailbox emails <mailbox-id>` | List the emails a mailbox has received | `mailbox_emails` |
305
+
306
+ ### accounts
307
+
308
+ Durable identities on the site under test — what an authenticated test signs in as.
309
+
310
+ `beryl accounts` with no subcommand runs `accounts list`.
294
311
 
295
312
  | Command | Summary | MCP tool |
296
313
  | --- | --- | --- |
297
- | `beryl inbox create` | Mint an email inbox that Beryl receives mail for | `inbox_create` |
298
- | `beryl inbox list` | List the workspace's inboxes, newest first | `inbox_list` |
299
- | `beryl inbox delete <inbox-id>` | Delete an inbox and every email it has received | `inbox_delete` |
300
- | `beryl inbox read <inbox-id>` | Read the latest email from an inbox (waits for one to arrive) | `inbox_read` |
301
- | `beryl inbox emails <inbox-id>` | List the emails an inbox has received | `inbox_emails` |
314
+ | `beryl accounts list` | List the test accounts an environment's tests sign in as | `accounts_list` |
315
+ | `beryl accounts create` | Add a test account — the customer's own, or one Beryl signs up | `accounts_create` |
316
+ | `beryl accounts provision <account-id>` | Prove a test account can get in, by replaying a plan that ends logged in | `accounts_provision` |
317
+ | `beryl accounts set-login <account-id>` | Store the sign-in plan a run replays once, plus the probe that proves it | `accounts_set_login` |
318
+ | `beryl accounts get-login <account-id>` | Read the stored sign-in plan, its probe, and the hash a safe write must cite | `accounts_get_login` |
319
+ | `beryl accounts check <account-id>` | Sign in now and prove the session survives into a fresh browser | `accounts_check` |
320
+ | `beryl accounts update <account-id>` | Change a test account's password, login method, or default flag | `accounts_update` |
321
+ | `beryl accounts delete <account-id>` | Delete a test account | `accounts_delete` |
302
322
 
303
323
  ### account
304
324
 
@@ -329,6 +349,14 @@ Review a workspace's plan usage, subscription, and invoices.
329
349
  | `beryl billing invoices` | List recent invoices | — |
330
350
  | `beryl billing portal` | Get a Stripe billing-portal link for the workspace | — |
331
351
 
352
+ ### version
353
+
354
+ Show the running CLI version, API URL, and Node version
355
+
356
+ | Command | Summary | MCP tool |
357
+ | --- | --- | --- |
358
+ | `beryl version` | Show the running CLI version, API URL, and Node version | `version` |
359
+
332
360
  ### mcp
333
361
 
334
362
  Run the Beryl MCP server (stdio) — every CLI command as an agent tool
@@ -230,6 +230,8 @@ export async function runCli(argv) {
230
230
  // flag we don't recognise as global — that one belongs to the command.
231
231
  const words = [];
232
232
  const wordIndices = [];
233
+ let sawGlobalFlag = false;
234
+ let sawVersionFlag = false;
233
235
  for (let i = 0; i < argv.length; i++) {
234
236
  const tok = argv[i];
235
237
  if (tok === "--")
@@ -238,6 +240,9 @@ export async function runCli(argv) {
238
240
  const span = globalFlagSpan(argv, i);
239
241
  if (span === 0)
240
242
  break;
243
+ sawGlobalFlag = true;
244
+ if (tok === "--version" || tok === "-V")
245
+ sawVersionFlag = true;
241
246
  i += span - 1;
242
247
  continue;
243
248
  }
@@ -246,7 +251,16 @@ export async function runCli(argv) {
246
251
  }
247
252
  let rest = argv;
248
253
  let restWordIndices = wordIndices;
249
- if (argv.includes("--version") || argv.includes("-V") || words[0] === "version") {
254
+ // `-V`, `--version`, and a plain `beryl version` keep printing just the number: that
255
+ // one-line output is a contract CI users parse, so it stays intercepted here rather than
256
+ // going through the registry (which would render the full key/value block instead).
257
+ // Any global flag alongside the `version` word (`--json`, `--api-url`, `--help`, …) means
258
+ // the caller wants more than the number, so it falls through to the `version` spec — the
259
+ // same build picture the `version` MCP tool returns (api_url, Node, staleness). Only flags
260
+ // the scanner above actually recognised count, so it honours `--` the same way (tokens
261
+ // after it are inert positionals, not a request for the spec).
262
+ const versionWord = words[0] === "version" && words.length === 1;
263
+ if (sawVersionFlag || (versionWord && !sawGlobalFlag)) {
250
264
  process.stdout.write(cliVersion() + "\n");
251
265
  return EXIT_OK;
252
266
  }
@@ -1,8 +1,11 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import fs from "node:fs";
5
+ import { loadConfig } from "../config.js";
4
6
  import { createContext } from "../context.js";
5
7
  import { CliError } from "../errors.js";
8
+ import { ApiClient } from "../http.js";
6
9
  import { commands } from "../registry/index.js";
7
10
  import { cliVersion, warnIfStale } from "../version-check.js";
8
11
  export function toolName(spec) {
@@ -71,15 +74,50 @@ function toInput(spec, params) {
71
74
  }
72
75
  return { args, flags };
73
76
  }
77
+ let authCache;
78
+ /** The credentials as they are on disk RIGHT NOW, not as they were at spawn.
79
+ *
80
+ * This server is long-lived and never re-execs, so reading the token once would leave a
81
+ * `beryl login` performed afterwards invisible to it forever — and the 401 it then
82
+ * returns tells you to run the login you just ran, which is unescapable without knowing
83
+ * to reconnect. Keyed on mtime so the common path is a stat, not a rebuilt client. */
84
+ export function currentAuth(fallback) {
85
+ let mtimeMs = 0;
86
+ try {
87
+ mtimeMs = fs.statSync(fallback.config.globalConfigPath).mtimeMs;
88
+ }
89
+ catch {
90
+ // No config file (env-var auth, or never logged in) — nothing to watch.
91
+ }
92
+ if (authCache?.mtimeMs === mtimeMs)
93
+ return authCache.auth;
94
+ const config = loadConfig();
95
+ const auth = {
96
+ client: new ApiClient(config.apiUrl, config.token),
97
+ config,
98
+ };
99
+ authCache = { mtimeMs, auth };
100
+ return auth;
101
+ }
102
+ export function __resetAuthCacheForTests() {
103
+ authCache = undefined;
104
+ }
74
105
  export async function serveMcp(baseCtx) {
75
106
  // Fire-and-forget staleness warning: a stale MCP server silently exposes fewer
76
107
  // tools, and stderr is the one channel a stdio MCP server can safely log to.
77
108
  void warnIfStale(cliVersion(), (msg) => console.error(msg));
78
109
  const server = new Server({ name: "beryl", version: cliVersion() }, {
79
110
  capabilities: { tools: {} },
80
- instructions: "Beryl authors, runs, and heals end-to-end tests for any web app: tests are JSON " +
81
- "action plans replayed in real cloud browsers, with per-run email inboxes that make " +
82
- "signup/OTP/magic-link flows fully self-contained (no human login needed). Before " +
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 " +
83
121
  "authoring your first test plan, call the `guide` tool — it returns the full " +
84
122
  "authoring guide (plan shape, outcome assertions, email/OTP wiring, run-fix loop).",
85
123
  });
@@ -105,9 +143,10 @@ export async function serveMcp(baseCtx) {
105
143
  if (lines.length > 400)
106
144
  lines.splice(0, lines.length - 400);
107
145
  };
146
+ const auth = currentAuth(baseCtx);
108
147
  const ctx = createContext({
109
- client: baseCtx.client,
110
- config: baseCtx.config,
148
+ client: auth.client,
149
+ config: auth.config,
111
150
  json: true,
112
151
  interactive: false,
113
152
  mcp: true,