@beryl-so/cli 0.5.0 → 0.6.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 +33 -0
- package/dist/commands/auth.js +61 -7
- package/dist/commands/credentials.js +33 -1
- package/dist/commands/explorations.js +14 -0
- package/dist/commands/inboxes.js +117 -0
- package/dist/commands/slack.js +82 -0
- package/dist/registry/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,14 @@ Authenticate the CLI with your Beryl account
|
|
|
73
73
|
| --- | --- | --- |
|
|
74
74
|
| `beryl login` | Authenticate the CLI with your Beryl account | — |
|
|
75
75
|
|
|
76
|
+
### signup
|
|
77
|
+
|
|
78
|
+
Register a new Beryl account (emails a 6-digit verification code)
|
|
79
|
+
|
|
80
|
+
| Command | Summary | MCP tool |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `beryl signup` | Register a new Beryl account (emails a 6-digit verification code) | `signup` |
|
|
83
|
+
|
|
76
84
|
### logout
|
|
77
85
|
|
|
78
86
|
Remove the stored token from the CLI config
|
|
@@ -232,6 +240,7 @@ Inspect the agent's exploration runs — how it crawled a site and authored its
|
|
|
232
240
|
| `beryl explorations list` | List the agent's exploration passes for a project | `explorations_list` |
|
|
233
241
|
| `beryl explorations get <exploration-id>` | Show one exploration: authored tests, abandoned flows, coverage, frontier | `explorations_get` |
|
|
234
242
|
| `beryl explorations steps <exploration-id>` | List every step the agent took in an exploration | `explorations_steps` |
|
|
243
|
+
| `beryl explorations cancel <exploration-id>` | Cancel an in-flight exploration | `explorations_cancel` |
|
|
235
244
|
| `beryl explorations watch <exploration-id>` | Stream an exploration live — watch the agent explore and author tests | `explorations_watch` |
|
|
236
245
|
|
|
237
246
|
### config
|
|
@@ -251,6 +260,19 @@ Manage the variables, secrets, and files the agent can use while exploring and r
|
|
|
251
260
|
| `beryl config files download <file-id>` | Get a short-lived download URL for a file | `config_files_download` |
|
|
252
261
|
| `beryl config files delete <file-id>` | Delete an uploaded file | `config_files_delete` |
|
|
253
262
|
|
|
263
|
+
### slack
|
|
264
|
+
|
|
265
|
+
Send run outcomes to a Slack channel via a per-project incoming webhook.
|
|
266
|
+
|
|
267
|
+
`beryl slack` with no subcommand runs `slack show`.
|
|
268
|
+
|
|
269
|
+
| Command | Summary | MCP tool |
|
|
270
|
+
| --- | --- | --- |
|
|
271
|
+
| `beryl slack show` | Show the project's Slack alert config (webhook is masked) | `slack_show` |
|
|
272
|
+
| `beryl slack set-webhook <url>` | Set the Slack incoming-webhook URL run alerts post to | `slack_set_webhook` |
|
|
273
|
+
| `beryl slack clear` | Remove the project's Slack webhook (stops all alerts) | `slack_clear` |
|
|
274
|
+
| `beryl slack test` | Post a sample alert to the configured webhook | `slack_test` |
|
|
275
|
+
|
|
254
276
|
### credentials
|
|
255
277
|
|
|
256
278
|
Manage saved logins Beryl reuses to test behind authentication, and attach them to projects.
|
|
@@ -273,10 +295,21 @@ Drive a browser session that captures a target-site login for Beryl to reuse.
|
|
|
273
295
|
| Command | Summary | MCP tool |
|
|
274
296
|
| --- | --- | --- |
|
|
275
297
|
| `beryl auth-capture start` | Start a login-capture browser session for the project (non-interactive) | `auth_capture_start` |
|
|
298
|
+
| `beryl auth-capture login <session-id>` | Log into the target site headlessly with credentials (no human at the browser) | `auth_capture_login` |
|
|
276
299
|
| `beryl auth-capture capture <session-id>` | Capture the session after the user has logged in via the live-view URL | `auth_capture_capture` |
|
|
277
300
|
| `beryl auth-capture refresh <session-id>` | Capture a refreshed session for a project whose login is expiring | `auth_capture_refresh` |
|
|
278
301
|
| `beryl auth-capture release <session-id>` | Release a login-capture browser session without capturing | `auth_capture_release` |
|
|
279
302
|
|
|
303
|
+
### inbox
|
|
304
|
+
|
|
305
|
+
Disposable email inboxes for testing flows that send mail — signups, OTPs, receipts.
|
|
306
|
+
|
|
307
|
+
| Command | Summary | MCP tool |
|
|
308
|
+
| --- | --- | --- |
|
|
309
|
+
| `beryl inbox create` | Mint a disposable email inbox that Beryl receives mail for | `inbox_create` |
|
|
310
|
+
| `beryl inbox read <inbox-id>` | Read the latest email from an inbox (waits for one to arrive) | `inbox_read` |
|
|
311
|
+
| `beryl inbox emails <inbox-id>` | List the emails an inbox has received | `inbox_emails` |
|
|
312
|
+
|
|
280
313
|
### account
|
|
281
314
|
|
|
282
315
|
View and update your personal account profile.
|
package/dist/commands/auth.js
CHANGED
|
@@ -54,7 +54,9 @@ export const authCommands = [
|
|
|
54
54
|
summary: "Authenticate the CLI with your Beryl account",
|
|
55
55
|
description: "Signs in with an emailed one-time code and mints a personal access token, " +
|
|
56
56
|
"which is stored in the CLI config. Pass --token to use an existing token " +
|
|
57
|
-
"from Account → API tokens instead.
|
|
57
|
+
"from Account → API tokens instead. Pass --email plus --code (the 6 digits " +
|
|
58
|
+
"from the email, e.g. read from a `beryl inbox`) to complete the OTP flow " +
|
|
59
|
+
"without a prompt. In CI, prefer the BERYL_API_KEY environment variable.",
|
|
58
60
|
interactive: true,
|
|
59
61
|
flags: [
|
|
60
62
|
{ name: "token", type: "string", description: "Use an existing personal access token" },
|
|
@@ -64,26 +66,48 @@ export const authCommands = [
|
|
|
64
66
|
description: "Email for the one-time code sign-in (default: your last sign-in, or your git " +
|
|
65
67
|
"user.email — offered as the prompt default)",
|
|
66
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "code",
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "The emailed 6-digit code — skips the prompt for non-interactive use " +
|
|
73
|
+
"(requires --email; also skips sending a fresh code, so pair it with a " +
|
|
74
|
+
"code already requested via `beryl login`, `beryl signup`, or the API)",
|
|
75
|
+
},
|
|
67
76
|
{
|
|
68
77
|
name: "token-name",
|
|
69
78
|
type: "string",
|
|
70
79
|
description: "Name for the minted token (default: CLI on <hostname>)",
|
|
71
80
|
},
|
|
72
81
|
],
|
|
73
|
-
examples: [
|
|
82
|
+
examples: [
|
|
83
|
+
"beryl login",
|
|
84
|
+
"beryl login --token beryl_pat_…",
|
|
85
|
+
"beryl login --email you@example.com",
|
|
86
|
+
"beryl login --email agent@example.com --code 123456 --json",
|
|
87
|
+
],
|
|
74
88
|
async run(ctx, input) {
|
|
75
89
|
const apiUrl = ctx.client.baseUrl;
|
|
76
90
|
let token = flagStr(input, "token");
|
|
77
91
|
if (!token) {
|
|
92
|
+
const codeFlag = flagStr(input, "code")?.replace(/\s/g, "");
|
|
93
|
+
if (codeFlag !== undefined) {
|
|
94
|
+
if (!/^\d{6}$/.test(codeFlag))
|
|
95
|
+
throw new UsageError("--code must be the 6 digits from the email");
|
|
96
|
+
if (!flagStr(input, "email"))
|
|
97
|
+
throw new UsageError("--code requires --email");
|
|
98
|
+
}
|
|
78
99
|
const email = flagStr(input, "email") ?? (await promptForEmail(ctx));
|
|
79
100
|
if (!email.includes("@"))
|
|
80
101
|
throw new UsageError(`"${email}" is not an email address`);
|
|
81
102
|
const anon = new ApiClient(apiUrl);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
let code = codeFlag;
|
|
104
|
+
if (!code) {
|
|
105
|
+
await anon.post("/auth/request-login-otp", { email });
|
|
106
|
+
ctx.err(dim(`Sent a 6-digit code to ${email}`));
|
|
107
|
+
ctx.err(dim("Not arriving? Check spam; if you're new to Beryl, sign up at https://beryl.so first — " +
|
|
108
|
+
"or use `beryl login --token` with a token from beryl.so → Account → API tokens."));
|
|
109
|
+
code = await promptForOtpCode(ctx);
|
|
110
|
+
}
|
|
87
111
|
const login = (await anon.post("/auth/verify-otp", { email, code }));
|
|
88
112
|
if (!login.access_token)
|
|
89
113
|
throw new CliError("Login did not return an access token");
|
|
@@ -125,6 +149,36 @@ export const authCommands = [
|
|
|
125
149
|
};
|
|
126
150
|
},
|
|
127
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: "signup",
|
|
154
|
+
summary: "Register a new Beryl account (emails a 6-digit verification code)",
|
|
155
|
+
description: "Creates a passwordless account for the email and sends it a 6-digit code. " +
|
|
156
|
+
"Finish with `beryl login --email <addr> --code <the 6 digits>` — that verifies " +
|
|
157
|
+
"the account, creates its workspace, and signs the CLI in. With an inbox from " +
|
|
158
|
+
"`beryl inbox create` as the address, an agent can provision a fresh account " +
|
|
159
|
+
"end-to-end with no human at a prompt.",
|
|
160
|
+
flags: [
|
|
161
|
+
{
|
|
162
|
+
name: "email",
|
|
163
|
+
type: "string",
|
|
164
|
+
required: true,
|
|
165
|
+
description: "Email address for the new account",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
examples: ["beryl signup --email agent+ci@example.com --json"],
|
|
169
|
+
async run(ctx, input) {
|
|
170
|
+
const email = flagStr(input, "email") ?? "";
|
|
171
|
+
if (!email.includes("@"))
|
|
172
|
+
throw new UsageError(`"${email}" is not an email address`);
|
|
173
|
+
const anon = new ApiClient(ctx.client.baseUrl);
|
|
174
|
+
const created = (await anon.post("/auth/signup", { email }));
|
|
175
|
+
return {
|
|
176
|
+
data: created,
|
|
177
|
+
human: `${green("Signup started")} for ${email}\n` +
|
|
178
|
+
dim(`Sent a 6-digit code to ${email} — finish with: beryl login --email ${email} --code <code>`),
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
},
|
|
128
182
|
{
|
|
129
183
|
name: "logout",
|
|
130
184
|
summary: "Remove the stored token from the CLI config",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { UsageError } from "../errors.js";
|
|
1
2
|
import { dim, green, yellow } from "../output.js";
|
|
2
|
-
import { arg, flagBool } from "./util.js";
|
|
3
|
+
import { arg, flagBool, flagStr } from "./util.js";
|
|
3
4
|
const capturePath = (ws, p) => `/auth-capture/workspaces/${ws}/projects/${p}/sessions`;
|
|
4
5
|
export const credentialCommands = [
|
|
5
6
|
{
|
|
@@ -110,6 +111,37 @@ export const credentialCommands = [
|
|
|
110
111
|
return { data: await ctx.client.post(capturePath(workspaceId, projectId)) };
|
|
111
112
|
},
|
|
112
113
|
},
|
|
114
|
+
{
|
|
115
|
+
name: "auth-capture login",
|
|
116
|
+
summary: "Log into the target site headlessly with credentials (no human at the browser)",
|
|
117
|
+
description: "Drives the login inside the capture session started by `auth-capture start`, so " +
|
|
118
|
+
"an agent can complete start → login → capture with zero human intervention. The " +
|
|
119
|
+
"credentials are sent to the server, typed into the target site over the wire, and " +
|
|
120
|
+
"never stored, logged, or returned — the captured session stays encrypted " +
|
|
121
|
+
"server-side. Follow with `auth-capture capture` to snapshot the authenticated session.",
|
|
122
|
+
scope: "project",
|
|
123
|
+
args: [
|
|
124
|
+
{ name: "session-id", description: "Session id from auth-capture start", required: true },
|
|
125
|
+
],
|
|
126
|
+
flags: [
|
|
127
|
+
{ name: "username", type: "string", description: "Login username / email", required: true },
|
|
128
|
+
{ name: "password", type: "string", description: "Login password", required: true },
|
|
129
|
+
{
|
|
130
|
+
name: "login-url",
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Explicit login page URL (defaults to the session's current page)",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
async run(ctx, input) {
|
|
136
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
137
|
+
const username = flagStr(input, "username");
|
|
138
|
+
const password = flagStr(input, "password");
|
|
139
|
+
if (!username || !password)
|
|
140
|
+
throw new UsageError("--username and --password are required");
|
|
141
|
+
await ctx.client.post(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}/login`, { username, password, login_url: flagStr(input, "login-url") ?? null });
|
|
142
|
+
return { human: "Logged in." };
|
|
143
|
+
},
|
|
144
|
+
},
|
|
113
145
|
{
|
|
114
146
|
name: "auth-capture capture",
|
|
115
147
|
summary: "Capture the session after the user has logged in via the live-view URL",
|
|
@@ -35,6 +35,20 @@ export const explorationCommands = [
|
|
|
35
35
|
};
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
name: "explorations cancel",
|
|
40
|
+
summary: "Cancel an in-flight exploration",
|
|
41
|
+
description: "Stops a running exploration without touching the project or its tests — the " +
|
|
42
|
+
"lever for a runaway/non-convergent pass burning agent budget.",
|
|
43
|
+
scope: "project",
|
|
44
|
+
args: [{ name: "exploration-id", description: "Exploration id", required: true }],
|
|
45
|
+
async run(ctx, input) {
|
|
46
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
47
|
+
return {
|
|
48
|
+
data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/explorations/${arg(input, "exploration-id")}/cancel`),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
},
|
|
38
52
|
{
|
|
39
53
|
name: "explorations watch",
|
|
40
54
|
summary: "Stream an exploration live — watch the agent explore and author tests",
|
|
@@ -0,0 +1,117 @@
|
|
|
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 a disposable email inbox that Beryl receives mail for",
|
|
29
|
+
groupSummary: "Disposable 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
|
+
scope: "workspace",
|
|
34
|
+
flags: [
|
|
35
|
+
{
|
|
36
|
+
name: "ttl-hours",
|
|
37
|
+
type: "number",
|
|
38
|
+
description: "Hours before the inbox expires and stops receiving (1-168, default 24)",
|
|
39
|
+
},
|
|
40
|
+
{ name: "project", type: "string", description: "Attach the inbox to a project id" },
|
|
41
|
+
],
|
|
42
|
+
examples: ["beryl inbox create --json", "beryl inbox create --ttl-hours 2"],
|
|
43
|
+
async run(ctx, input) {
|
|
44
|
+
const ws = await ctx.requireWorkspace(input);
|
|
45
|
+
const inbox = (await ctx.client.post(`/workspaces/${ws}/inboxes`, {
|
|
46
|
+
ttl_hours: flagNum(input, "ttl-hours") ?? 24,
|
|
47
|
+
project_id: flagStr(input, "project") ?? null,
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
data: inbox,
|
|
51
|
+
human: `${green("Created")} inbox ${inbox.id}\n\n ${inbox.address}\n\n` +
|
|
52
|
+
dim(`Read it with: beryl inbox read ${inbox.id}`),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "inbox read",
|
|
58
|
+
summary: "Read the latest email from an inbox (waits for one to arrive)",
|
|
59
|
+
description: "Waits up to --timeout-s for a matching email and returns it (one blocking request; " +
|
|
60
|
+
"the server caps the wait at 50s — re-run to keep waiting). With --extract-code, " +
|
|
61
|
+
"also pulls the one-time code (4-8 digits) out of the body/subject — handy for " +
|
|
62
|
+
"completing `beryl login --email <addr> --code <code>` unattended. Exits non-zero " +
|
|
63
|
+
"if nothing arrives before the timeout.",
|
|
64
|
+
scope: "workspace",
|
|
65
|
+
args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
|
|
66
|
+
flags: [
|
|
67
|
+
{
|
|
68
|
+
name: "timeout-s",
|
|
69
|
+
type: "number",
|
|
70
|
+
description: "Seconds to wait for a matching email (0 = don't wait; max 50, default 30)",
|
|
71
|
+
},
|
|
72
|
+
{ name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
|
|
73
|
+
{ name: "from-contains", type: "string", description: "Only emails whose sender contains this" },
|
|
74
|
+
{
|
|
75
|
+
name: "subject-contains",
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Only emails whose subject contains this",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "extract-code",
|
|
81
|
+
type: "boolean",
|
|
82
|
+
description: "Also return the one-time code found in the email as `code`",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
examples: [
|
|
86
|
+
"beryl inbox read ibx_123 --timeout-s 45 --json",
|
|
87
|
+
"beryl inbox read ibx_123 --subject-contains code --extract-code --json",
|
|
88
|
+
],
|
|
89
|
+
async run(ctx, input) {
|
|
90
|
+
const ws = await ctx.requireWorkspace(input);
|
|
91
|
+
const email = (await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails/latest`, {
|
|
92
|
+
timeout_s: flagNum(input, "timeout-s"),
|
|
93
|
+
since: flagStr(input, "since"),
|
|
94
|
+
from_contains: flagStr(input, "from-contains"),
|
|
95
|
+
subject_contains: flagStr(input, "subject-contains"),
|
|
96
|
+
}));
|
|
97
|
+
if (!flagBool(input, "extract-code"))
|
|
98
|
+
return { data: email };
|
|
99
|
+
return { data: { ...email, code: extractCode(email) } };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "inbox emails",
|
|
104
|
+
summary: "List the emails an inbox has received",
|
|
105
|
+
scope: "workspace",
|
|
106
|
+
args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
|
|
107
|
+
flags: [
|
|
108
|
+
{ name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
|
|
109
|
+
],
|
|
110
|
+
async run(ctx, input) {
|
|
111
|
+
const ws = await ctx.requireWorkspace(input);
|
|
112
|
+
return {
|
|
113
|
+
data: await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails`, { since: flagStr(input, "since") }),
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { arg, flagBool, projectPath } from "./util.js";
|
|
2
|
+
const slackPath = (ws, p) => `${projectPath(ws, p)}/slack`;
|
|
3
|
+
export const slackCommands = [
|
|
4
|
+
{
|
|
5
|
+
name: "slack show",
|
|
6
|
+
summary: "Show the project's Slack alert config (webhook is masked)",
|
|
7
|
+
groupSummary: "Send run outcomes to a Slack channel via a per-project incoming webhook.",
|
|
8
|
+
scope: "project",
|
|
9
|
+
groupDefault: true,
|
|
10
|
+
async run(ctx, input) {
|
|
11
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
12
|
+
return { data: await ctx.client.get(slackPath(workspaceId, projectId)) };
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "slack set-webhook",
|
|
17
|
+
summary: "Set the Slack incoming-webhook URL run alerts post to",
|
|
18
|
+
scope: "project",
|
|
19
|
+
args: [
|
|
20
|
+
{
|
|
21
|
+
name: "url",
|
|
22
|
+
description: "Slack incoming-webhook URL (https://hooks.slack.com/…)",
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
flags: [
|
|
27
|
+
{
|
|
28
|
+
name: "all-runs",
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Post on every run, not only on a new regression",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "failures-only",
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description: "Post only on a new regression (the default)",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
async run(ctx, input) {
|
|
39
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
40
|
+
const path = slackPath(workspaceId, projectId);
|
|
41
|
+
// Preserve the stored notify preference unless the caller states one, so
|
|
42
|
+
// rotating the webhook doesn't silently reset an existing all-runs opt-in.
|
|
43
|
+
let notifyAllRuns;
|
|
44
|
+
if (flagBool(input, "all-runs")) {
|
|
45
|
+
notifyAllRuns = true;
|
|
46
|
+
}
|
|
47
|
+
else if (flagBool(input, "failures-only")) {
|
|
48
|
+
notifyAllRuns = false;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const current = (await ctx.client.get(path));
|
|
52
|
+
notifyAllRuns = current?.notify_all_runs ?? false;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
data: await ctx.client.put(path, {
|
|
56
|
+
webhook_url: arg(input, "url"),
|
|
57
|
+
notify_all_runs: notifyAllRuns,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "slack clear",
|
|
64
|
+
summary: "Remove the project's Slack webhook (stops all alerts)",
|
|
65
|
+
scope: "project",
|
|
66
|
+
async run(ctx, input) {
|
|
67
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
68
|
+
await ctx.client.del(slackPath(workspaceId, projectId));
|
|
69
|
+
return { human: "Cleared." };
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "slack test",
|
|
74
|
+
summary: "Post a sample alert to the configured webhook",
|
|
75
|
+
scope: "project",
|
|
76
|
+
async run(ctx, input) {
|
|
77
|
+
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
78
|
+
await ctx.client.post(`${slackPath(workspaceId, projectId)}/test`);
|
|
79
|
+
return { human: "Sent a test message to Slack." };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
package/dist/registry/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { configCommands } from "../commands/config-vars.js";
|
|
|
4
4
|
import { credentialCommands } from "../commands/credentials.js";
|
|
5
5
|
import { environmentCommands } from "../commands/environments.js";
|
|
6
6
|
import { explorationCommands } from "../commands/explorations.js";
|
|
7
|
+
import { inboxCommands } from "../commands/inboxes.js";
|
|
7
8
|
import { initCommands } from "../commands/init.js";
|
|
8
9
|
import { mcpCommands } from "../commands/mcp.js";
|
|
9
10
|
import { projectCommands } from "../commands/projects.js";
|
|
10
11
|
import { runCommands } from "../commands/runs.js";
|
|
12
|
+
import { slackCommands } from "../commands/slack.js";
|
|
11
13
|
import { testCommands } from "../commands/tests.js";
|
|
12
14
|
import { workspaceCommands } from "../commands/workspaces.js";
|
|
13
15
|
export const WORKSPACE_FLAG = {
|
|
@@ -43,7 +45,9 @@ export const commands = [
|
|
|
43
45
|
...runCommands,
|
|
44
46
|
...explorationCommands,
|
|
45
47
|
...configCommands,
|
|
48
|
+
...slackCommands,
|
|
46
49
|
...credentialCommands,
|
|
50
|
+
...inboxCommands,
|
|
47
51
|
...accountCommands,
|
|
48
52
|
...mcpCommands,
|
|
49
53
|
].map(withScopeFlags);
|
package/package.json
CHANGED