@beryl-so/cli 0.14.1 → 0.21.4

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.
@@ -25,6 +25,15 @@ export const ACTION_PLAN_SCHEMA = {
25
25
  "title": "ActionType",
26
26
  "type": "string"
27
27
  },
28
+ "AuthMode": {
29
+ "description": "How an authenticated test reaches its logged-in start state. The AUTHOR sets\nthis, explicitly, on every plan that sets ``requires_auth`` \u2014 a submitted plan\nthat omits it is rejected, because the wrong guess banks a plan that runs logged\nout and fails somewhere misleading.\n\n``inline`` \u2014 the plan signs itself in: its own steps fill ``{{login_email}}`` /\n``{{login_password}}``, or it rides the project's captured session.\n\n``session`` \u2014 the test's account signs in ONCE per run, ahead of the tests; the\nresulting browser session is proved against a fresh context and then handed to every\nsession-mode test in the run. The plan itself carries NO sign-in steps, so it starts\nwhere the flow it actually tests begins. Requires the account to have a stored\nlogin plan (``accounts set-login``).",
30
+ "enum": [
31
+ "inline",
32
+ "session"
33
+ ],
34
+ "title": "AuthMode",
35
+ "type": "string"
36
+ },
28
37
  "DialogChoice": {
29
38
  "enum": [
30
39
  "accept",
@@ -863,6 +872,9 @@ export const ACTION_PLAN_SCHEMA = {
863
872
  "not": {
864
873
  "enum": [
865
874
  "inbox_address",
875
+ "login_email",
876
+ "login_password",
877
+ "mailbox_address",
866
878
  "timestamp",
867
879
  "unique",
868
880
  "uuid"
@@ -1128,6 +1140,24 @@ export const ACTION_PLAN_SCHEMA = {
1128
1140
  "$id": "https://api.beryl.so/api/v1/schemas/action-plan.schema.json",
1129
1141
  "$schema": "https://json-schema.org/draft/2020-12/schema",
1130
1142
  "allOf": [
1143
+ {
1144
+ "$comment": "requires_auth demands an explicit auth_mode \u2014 there is no default. 'inline' when the plan signs itself in, 'session' when it rides its account's once-per-run session and carries no sign-in steps.",
1145
+ "if": {
1146
+ "properties": {
1147
+ "requires_auth": {
1148
+ "const": true
1149
+ }
1150
+ },
1151
+ "required": [
1152
+ "requires_auth"
1153
+ ]
1154
+ },
1155
+ "then": {
1156
+ "required": [
1157
+ "auth_mode"
1158
+ ]
1159
+ }
1160
+ },
1131
1161
  {
1132
1162
  "$comment": "The first executed step must be a goto, so the test loads a page before acting (that is before[0] when there is a setup section, else steps[0]).",
1133
1163
  "else": {
@@ -1240,6 +1270,10 @@ export const ACTION_PLAN_SCHEMA = {
1240
1270
  "default": null,
1241
1271
  "title": "Auth Label"
1242
1272
  },
1273
+ "auth_mode": {
1274
+ "$ref": "#/$defs/AuthMode",
1275
+ "default": "inline"
1276
+ },
1243
1277
  "before": {
1244
1278
  "items": {
1245
1279
  "$ref": "#/$defs/PlanStep"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beryl-so/cli",
3
- "version": "0.14.1",
3
+ "version": "0.21.4",
4
4
  "description": "Beryl on the command line — projects, runs, the exploring agent, and an MCP server over the same commands.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "prepublishOnly": "npm run typecheck && npm test && npm run build",
28
28
  "typecheck": "tsc --noEmit",
29
29
  "test": "vitest run",
30
- "dev": "tsx src/index.ts",
30
+ "dev": "BERYL_API_URL=http://localhost:8000 BERYL_CONFIG_DIR=/tmp/beryl-dev tsx src/index.ts",
31
31
  "docs": "tsx scripts/gen-docs.ts"
32
32
  },
33
33
  "dependencies": {
@@ -1,135 +0,0 @@
1
- import { dim, green, yellow } from "../output.js";
2
- import { arg, flagBool } from "./util.js";
3
- const capturePath = (ws, p) => `/auth-capture/workspaces/${ws}/projects/${p}/sessions`;
4
- export const credentialCommands = [
5
- {
6
- name: "credentials list",
7
- summary: "List the workspace's saved logins",
8
- scope: "workspace",
9
- groupSummary: "Manage saved logins Beryl reuses to test behind authentication, and attach them to projects.",
10
- async run(ctx, input) {
11
- const ws = await ctx.requireWorkspace(input);
12
- return { data: await ctx.client.get(`/workspaces/${ws}/credentials`) };
13
- },
14
- },
15
- {
16
- name: "credentials get",
17
- summary: "Show one saved login (status and freshness — never the session itself)",
18
- args: [{ name: "credential-id", description: "Credential id", required: true }],
19
- async run(ctx, input) {
20
- return { data: await ctx.client.get(`/credentials/${arg(input, "credential-id")}`) };
21
- },
22
- },
23
- {
24
- name: "credentials projects",
25
- summary: "List the projects using a saved login",
26
- args: [{ name: "credential-id", description: "Credential id", required: true }],
27
- async run(ctx, input) {
28
- return {
29
- data: await ctx.client.get(`/credentials/${arg(input, "credential-id")}/projects`),
30
- };
31
- },
32
- },
33
- {
34
- name: "credentials delete",
35
- summary: "Delete a saved login",
36
- args: [{ name: "credential-id", description: "Credential id", required: true }],
37
- flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
38
- async run(ctx, input) {
39
- const id = arg(input, "credential-id");
40
- await ctx.confirm(`Delete credential ${id}?`, flagBool(input, "force"));
41
- await ctx.client.del(`/credentials/${id}`);
42
- return { human: "Deleted." };
43
- },
44
- },
45
- {
46
- name: "credentials attach",
47
- summary: "Attach a saved login to a project",
48
- scope: "project",
49
- args: [{ name: "credential-id", description: "Credential id", required: true }],
50
- async run(ctx, input) {
51
- const { projectId } = await ctx.requireProject(input);
52
- return {
53
- data: await ctx.client.put(`/projects/${projectId}/credential`, {
54
- credential_id: arg(input, "credential-id"),
55
- }),
56
- };
57
- },
58
- },
59
- {
60
- name: "credentials detach",
61
- summary: "Detach the project's saved login",
62
- scope: "project",
63
- async run(ctx, input) {
64
- const { projectId } = await ctx.requireProject(input);
65
- await ctx.client.del(`/projects/${projectId}/credential`);
66
- return { human: "Detached." };
67
- },
68
- },
69
- {
70
- name: "credentials recapture",
71
- summary: "Start a re-capture for an expiring saved login (returns a live browser URL)",
72
- args: [{ name: "credential-id", description: "Credential id", required: true }],
73
- async run(ctx, input) {
74
- return {
75
- data: await ctx.client.post(`/credentials/${arg(input, "credential-id")}/recaptures`),
76
- };
77
- },
78
- },
79
- {
80
- name: "credentials capture",
81
- summary: "Capture a login for the project interactively: log in once in a real browser",
82
- description: "Opens a live cloud-browser session on the project's site. Log in there like a normal " +
83
- "user, come back, and press Enter — Beryl captures the session (encrypted at rest, " +
84
- "never shown to anyone) so the agent can test the authenticated app.",
85
- scope: "project",
86
- interactive: true,
87
- async run(ctx, input) {
88
- const { workspaceId, projectId } = await ctx.requireProject(input);
89
- const session = (await ctx.client.post(capturePath(workspaceId, projectId)));
90
- ctx.err(`\nOpen this URL and log in to the site:\n\n ${yellow(session.live_view_url)}\n`);
91
- await ctx.prompt("Press Enter once you are fully logged in… ");
92
- try {
93
- await ctx.client.post(`${capturePath(workspaceId, projectId)}/${session.session_id}/capture`);
94
- }
95
- finally {
96
- await ctx.client
97
- .del(`${capturePath(workspaceId, projectId)}/${session.session_id}`)
98
- .catch(() => { });
99
- }
100
- return { human: `${green("Login captured.")} ${dim("The agent can test the gated app with it.")}` };
101
- },
102
- },
103
- {
104
- name: "auth-capture start",
105
- summary: "Start a login-capture browser session for the project (non-interactive)",
106
- scope: "project",
107
- groupSummary: "Drive a browser session that captures a target-site login for Beryl to reuse.",
108
- async run(ctx, input) {
109
- const { workspaceId, projectId } = await ctx.requireProject(input);
110
- return { data: await ctx.client.post(capturePath(workspaceId, projectId)) };
111
- },
112
- },
113
- {
114
- name: "auth-capture capture",
115
- summary: "Save the session after the user has logged in via the live-view URL (first login or re-login)",
116
- scope: "project",
117
- args: [{ name: "session-id", description: "Session id from auth-capture start", required: true }],
118
- async run(ctx, input) {
119
- const { workspaceId, projectId } = await ctx.requireProject(input);
120
- await ctx.client.post(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}/capture`);
121
- return { human: "Captured." };
122
- },
123
- },
124
- {
125
- name: "auth-capture release",
126
- summary: "Release a login-capture browser session without capturing",
127
- scope: "project",
128
- args: [{ name: "session-id", description: "Session id from auth-capture start", required: true }],
129
- async run(ctx, input) {
130
- const { workspaceId, projectId } = await ctx.requireProject(input);
131
- await ctx.client.del(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}`);
132
- return { human: "Released." };
133
- },
134
- },
135
- ];
@@ -1,166 +0,0 @@
1
- import { dim, green } from "../output.js";
2
- import { arg, flagBool, flagNum, flagStr } from "./util.js";
3
- // Mirrors the API's own extractor (email_inbox/step_resolver.py): a labelled digit run
4
- // ("your code is 654321") beats the bare fenced pattern, because a real sign-in mail is
5
- // full of innocent 4-8 digit runs — "© 2026", a support number — and the bare fence
6
- // would happily return the first of them.
7
- const CODE_PATTERN = /(?<!\d)(\d{4,8})(?!\d)/;
8
- const LABELLED_CODE_PATTERN = /(?:verification|security|one[\s-]?time|login|sign[\s-]?in|access|confirmation)?\s*(?:code|otp|passcode|pin)\b[^0-9]{0,20}(?<!\d)(\d{4,8})(?!\d)/i;
9
- function visibleText(html) {
10
- return html
11
- .replace(/<(style|script|head)\b[\s\S]*?<\/\1>/gi, " ")
12
- .replace(/<[^>]+>/g, " ");
13
- }
14
- function extractCode(email) {
15
- const body = email.body_text || (email.body_html ? visibleText(email.body_html) : "");
16
- for (const pattern of [LABELLED_CODE_PATTERN, CODE_PATTERN]) {
17
- for (const text of [body, email.subject ?? ""]) {
18
- const match = text.match(pattern);
19
- if (match)
20
- return match[1];
21
- }
22
- }
23
- return null;
24
- }
25
- export const inboxCommands = [
26
- {
27
- name: "inbox create",
28
- summary: "Mint an email inbox that Beryl receives mail for",
29
- groupSummary: "Email inboxes for testing flows that send mail — signups, OTPs, receipts.",
30
- description: "Creates a receiving address under Beryl's inbound email domain and returns it. " +
31
- "Use it wherever a test needs a real, readable mailbox — e.g. as the --email for " +
32
- "`beryl signup`, then read the code back with `beryl inbox read --extract-code`. " +
33
- "Pass --permanent to mint the workspace's single permanent mailbox (no TTL).",
34
- scope: "workspace",
35
- flags: [
36
- {
37
- name: "permanent",
38
- type: "boolean",
39
- description: "Mint the workspace's permanent mailbox (no TTL); one per workspace",
40
- },
41
- {
42
- name: "ttl-hours",
43
- type: "number",
44
- description: "Hours before the inbox expires and stops receiving (1-168, default 24; ignored with --permanent)",
45
- },
46
- { name: "project", type: "string", description: "Attach the inbox to a project id" },
47
- ],
48
- examples: [
49
- "beryl inbox create --json",
50
- "beryl inbox create --ttl-hours 2",
51
- "beryl inbox create --permanent",
52
- ],
53
- async run(ctx, input) {
54
- const ws = await ctx.requireWorkspace(input);
55
- const inbox = (await ctx.client.post(`/workspaces/${ws}/inboxes`, {
56
- ttl_hours: flagBool(input, "permanent") ? null : (flagNum(input, "ttl-hours") ?? 24),
57
- project_id: flagStr(input, "project") ?? null,
58
- }));
59
- return {
60
- data: inbox,
61
- human: `${green("Created")} inbox ${inbox.id}\n\n ${inbox.address}\n\n` +
62
- dim(`Read it with: beryl inbox read ${inbox.id}`),
63
- };
64
- },
65
- },
66
- {
67
- name: "inbox list",
68
- summary: "List the workspace's inboxes, newest first",
69
- description: "Every inbox the workspace has minted with `beryl inbox create`. Expired inboxes " +
70
- "stop receiving and are hard-deleted by a background sweep, so they drop off " +
71
- "this list shortly after their TTL. Pass --permanent for just the permanent mailbox.",
72
- scope: "workspace",
73
- flags: [
74
- {
75
- name: "permanent",
76
- type: "boolean",
77
- description: "Only the workspace's permanent mailbox (no TTL, not run/project scoped)",
78
- },
79
- ],
80
- async run(ctx, input) {
81
- const ws = await ctx.requireWorkspace(input);
82
- const query = flagBool(input, "permanent") ? { permanent: true } : undefined;
83
- return { data: await ctx.client.get(`/workspaces/${ws}/inboxes`, query) };
84
- },
85
- },
86
- {
87
- name: "inbox delete",
88
- summary: "Delete an inbox and every email it has received",
89
- scope: "workspace",
90
- args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
91
- flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
92
- async run(ctx, input) {
93
- const ws = await ctx.requireWorkspace(input);
94
- const id = arg(input, "inbox-id");
95
- await ctx.confirm(`Delete inbox ${id} and its emails?`, flagBool(input, "force"));
96
- await ctx.client.del(`/workspaces/${ws}/inboxes/${id}`);
97
- return { human: "Deleted." };
98
- },
99
- },
100
- {
101
- name: "inbox read",
102
- summary: "Read the latest email from an inbox (waits for one to arrive)",
103
- description: "Waits up to --timeout-s for a matching email and returns it (one blocking request; " +
104
- "the server caps the wait at 50s — re-run to keep waiting). With --extract-code, " +
105
- "also pulls the one-time code (4-8 digits) out of the body/subject — handy for " +
106
- "completing `beryl login --email <addr> --code <code>` unattended. Exits non-zero " +
107
- "if nothing arrives before the timeout.",
108
- scope: "workspace",
109
- args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
110
- flags: [
111
- {
112
- name: "timeout-s",
113
- type: "number",
114
- description: "Seconds to wait for a matching email (0 = don't wait; max 50, default 30)",
115
- },
116
- { name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
117
- { name: "from-contains", type: "string", description: "Only emails whose sender contains this" },
118
- {
119
- name: "subject-contains",
120
- type: "string",
121
- description: "Only emails whose subject contains this",
122
- },
123
- {
124
- name: "extract-code",
125
- type: "boolean",
126
- description: "Also return the one-time code found in the email as `code`",
127
- },
128
- ],
129
- examples: [
130
- "beryl inbox read ibx_123 --timeout-s 45 --json",
131
- "beryl inbox read ibx_123 --subject-contains code --extract-code --json",
132
- ],
133
- async run(ctx, input) {
134
- const ws = await ctx.requireWorkspace(input);
135
- const email = (await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails/latest`, {
136
- timeout_s: flagNum(input, "timeout-s"),
137
- since: flagStr(input, "since"),
138
- from_contains: flagStr(input, "from-contains"),
139
- subject_contains: flagStr(input, "subject-contains"),
140
- }));
141
- if (!flagBool(input, "extract-code"))
142
- return { data: email };
143
- return { data: { ...email, code: extractCode(email) } };
144
- },
145
- },
146
- {
147
- name: "inbox emails",
148
- summary: "List the emails an inbox has received",
149
- scope: "workspace",
150
- args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
151
- flags: [
152
- { name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
153
- {
154
- name: "limit",
155
- type: "number",
156
- description: "Return only the most recent N emails (newest first)",
157
- },
158
- ],
159
- async run(ctx, input) {
160
- const ws = await ctx.requireWorkspace(input);
161
- return {
162
- data: await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails`, { since: flagStr(input, "since"), limit: flagNum(input, "limit") }),
163
- };
164
- },
165
- },
166
- ];