@beryl-so/cli 0.24.0 → 0.25.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/dist/adapters/mcp.js +111 -15
- package/dist/beryl-test-skill.js +241 -264
- package/dist/commands/accounts.js +8 -5
- package/dist/commands/config-vars.js +5 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/mailboxes.js +22 -5
- package/dist/commands/runs.js +19 -6
- package/dist/commands/tests.js +36 -6
- package/dist/email-pump.js +34 -3
- package/dist/skill-tables.js +74 -0
- package/package.json +1 -1
- package/dist/email-extract.js +0 -99
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
|
}));
|