@beryl-so/cli 0.52.0 → 0.54.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 +4 -3
- package/dist/adapters/mcp.js +6 -2
- package/dist/commands/auth.js +3 -3
- package/dist/commands/init.js +173 -49
- package/dist/telemetry.js +37 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,9 +13,10 @@ npx @beryl-so/cli@latest init
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Signs you in (browser approval by default, emailed one-time code as fallback; new
|
|
16
|
-
emails are signed up on the spot), wires the beryl + playwright MCP servers into
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
emails are signed up on the spot), wires the beryl + playwright MCP servers into the
|
|
17
|
+
agent you pick with --claude, --cursor, --vscode or --opencode (it asks when you pass
|
|
18
|
+
none), installs the beryl-test authoring skill, and installs Playwright for local runs.
|
|
19
|
+
Safe to re-run.
|
|
19
20
|
|
|
20
21
|
## Install
|
|
21
22
|
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ import { AuthError, CliError } from "../errors.js";
|
|
|
9
9
|
import { ApiClient, ApiError } from "../http.js";
|
|
10
10
|
import { parseArgv } from "./cli.js";
|
|
11
11
|
import { commands } from "../registry/index.js";
|
|
12
|
-
import { analyticsOptions, createPostHogClient, setClient, setSurface, shutdownTelemetry, withTool, } from "../telemetry.js";
|
|
12
|
+
import { analyticsOptions, createPostHogClient, captureMcpSessionStarted, setClient, setSurface, shutdownTelemetry, withTool, } from "../telemetry.js";
|
|
13
13
|
import { refreshInstalledSkills } from "../commands/init.js";
|
|
14
14
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
15
15
|
export function toolName(spec) {
|
|
@@ -353,7 +353,11 @@ export async function serveMcp(baseCtx) {
|
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
|
-
|
|
356
|
+
// Emitted here, not at boot: the handshake is what carries the client's name.
|
|
357
|
+
server.oninitialized = () => {
|
|
358
|
+
setClient(server.getClientVersion());
|
|
359
|
+
void captureMcpSessionStarted(baseCtx.config.apiUrl, identifyUser(baseCtx));
|
|
360
|
+
};
|
|
357
361
|
instrument(server, createPostHogClient(), analyticsOptions(baseCtx.config.apiUrl, identifyUser(baseCtx)));
|
|
358
362
|
// A coding agent ends an MCP server either by closing the pipe or by signalling it, and
|
|
359
363
|
// only the first path reaches onclose — without these the last batch of events dies with
|
package/dist/commands/auth.js
CHANGED
|
@@ -73,7 +73,7 @@ async function otpLogin(ctx, input, codeFlag) {
|
|
|
73
73
|
const why = `${email} signs in with Google, so there is no emailed code`;
|
|
74
74
|
if (!ctx.interactive) {
|
|
75
75
|
throw new UsageError(`${why} — run \`beryl login\` in a terminal to sign in via the browser, ` +
|
|
76
|
-
"or pass --token with a token from beryl.so → Account →
|
|
76
|
+
"or pass --token with a token from beryl.so → Account → Personal access tokens");
|
|
77
77
|
}
|
|
78
78
|
ctx.err(`${why} — confirm the sign-in in your browser instead.`);
|
|
79
79
|
const fromBrowser = await deviceLogin(ctx, process.stdin, flagStr(input, "token-name"));
|
|
@@ -99,8 +99,8 @@ export const authCommands = [
|
|
|
99
99
|
description: "Signs in via your browser (a code you confirm at beryl.so) and stores a personal " +
|
|
100
100
|
"access token in the CLI config. Pass --otp (or --email) to skip the browser and " +
|
|
101
101
|
"sign in with an emailed one-time code instead. A new email gets an account " +
|
|
102
|
-
"created automatically. Pass --token to use an existing token from Account →
|
|
103
|
-
"tokens. Pass --email plus --code (the 6 digits from the email, e.g. read from a " +
|
|
102
|
+
"created automatically. Pass --token to use an existing token from Account → " +
|
|
103
|
+
"Personal access tokens. Pass --email plus --code (the 6 digits from the email, e.g. read from a " +
|
|
104
104
|
"`beryl inbox`) to complete the OTP flow without a prompt. In CI, prefer the " +
|
|
105
105
|
"BERYL_API_KEY environment variable.",
|
|
106
106
|
interactive: true,
|
package/dist/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { BERYL_COPILOT_MARK_BEGIN, BERYL_COPILOT_MARK_END, BERYL_COPILOT_SECTION, BERYL_CURSOR_RULE, BERYL_CURSOR_RULE_FILENAME, BERYL_TEST_SKILL, BERYL_TEST_SKILL_DIR, BERYL_TEST_SKILL_FILENAME, } from "../beryl-test-skill.js";
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
|
-
import { AuthError, CliError } from "../errors.js";
|
|
7
|
+
import { AuthError, CliError, UsageError } from "../errors.js";
|
|
8
8
|
import { ApiClient } from "../http.js";
|
|
9
9
|
import { bold, cyan, dim, green, red, yellow } from "../output.js";
|
|
10
10
|
import { anyGap, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
@@ -31,9 +31,27 @@ const PLAYWRIGHT_SERVER_ENTRY = {
|
|
|
31
31
|
// with `type`, unlike Cursor/Claude's `mcpServers`. Same pinned commands, different shape.
|
|
32
32
|
const VSCODE_MCP_SERVER_ENTRY = { type: "stdio", ...MCP_SERVER_ENTRY };
|
|
33
33
|
const VSCODE_PLAYWRIGHT_SERVER_ENTRY = { type: "stdio", ...PLAYWRIGHT_SERVER_ENTRY };
|
|
34
|
-
|
|
34
|
+
// opencode flattens command + args into one array under `mcp`.
|
|
35
|
+
function opencodeEntry(entry) {
|
|
36
|
+
return { type: "local", command: [entry.command, ...entry.args], enabled: true };
|
|
37
|
+
}
|
|
38
|
+
const MANUAL_SETUP_DOCS = "https://beryl.so/docs/mcp#manual";
|
|
39
|
+
// Printed for the agents init cannot wire, from the same entries the flags write.
|
|
40
|
+
// Hand-formatted rather than JSON.stringify so args stay on one line.
|
|
41
|
+
function snippetEntry(name, entry) {
|
|
42
|
+
const args = entry.args.map((a) => JSON.stringify(a)).join(", ");
|
|
43
|
+
return ` "${name}": { "command": ${JSON.stringify(entry.command)}, "args": [${args}] }`;
|
|
44
|
+
}
|
|
45
|
+
const MANUAL_CONFIG_SNIPPET = [
|
|
46
|
+
" {",
|
|
47
|
+
' "mcpServers": {',
|
|
48
|
+
`${snippetEntry("beryl", MCP_SERVER_ENTRY)},`,
|
|
49
|
+
snippetEntry("playwright", PLAYWRIGHT_SERVER_ENTRY),
|
|
50
|
+
" }",
|
|
51
|
+
" }",
|
|
52
|
+
].join("\n");
|
|
35
53
|
// `key` is the container the editor expects: `mcpServers` for Claude/Cursor, `servers` for
|
|
36
|
-
// VS Code. `entries` are the editor-shaped server objects
|
|
54
|
+
// VS Code, `mcp` for opencode. `entries` are the editor-shaped server objects.
|
|
37
55
|
function mergeMcpConfig(file, key, entries) {
|
|
38
56
|
let existing = {};
|
|
39
57
|
if (fs.existsSync(file)) {
|
|
@@ -105,6 +123,33 @@ function vscodeUserConfigDir() {
|
|
|
105
123
|
const dir = vscodeUserConfigDirFor(process.platform, os.homedir(), process.env);
|
|
106
124
|
return fs.existsSync(dir) ? dir : null;
|
|
107
125
|
}
|
|
126
|
+
// The Claude DESKTOP app, which is a different surface from Claude Code and a different
|
|
127
|
+
// file from ~/.claude.json: macOS ~/Library/Application Support/Claude, Windows
|
|
128
|
+
// %APPDATA%/Claude, Linux ~/.config/Claude. Pure resolver so all three are testable.
|
|
129
|
+
export function claudeDesktopConfigDirFor(platform, home, env) {
|
|
130
|
+
return platform === "darwin"
|
|
131
|
+
? path.join(home, "Library", "Application Support", "Claude")
|
|
132
|
+
: platform === "win32"
|
|
133
|
+
? path.join(env.APPDATA ?? path.join(home, "AppData", "Roaming"), "Claude")
|
|
134
|
+
: path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "Claude");
|
|
135
|
+
}
|
|
136
|
+
// JSONC may carry comments JSON.parse rejects, so never rewrite it.
|
|
137
|
+
export function opencodeConfigFileFor(dir) {
|
|
138
|
+
const jsonc = path.join(dir, "opencode.jsonc");
|
|
139
|
+
if (fs.existsSync(jsonc) && !fs.existsSync(path.join(dir, "opencode.json"))) {
|
|
140
|
+
return { file: jsonc, jsonc: true };
|
|
141
|
+
}
|
|
142
|
+
return { file: path.join(dir, "opencode.json"), jsonc: false };
|
|
143
|
+
}
|
|
144
|
+
function opencodeConfigFile(scope, cwd) {
|
|
145
|
+
return opencodeConfigFileFor(scope === "user" ? path.join(os.homedir(), ".config", "opencode") : cwd);
|
|
146
|
+
}
|
|
147
|
+
// Null when the app isn't installed. Checked BEFORE writing because mergeMcpConfig
|
|
148
|
+
// mkdirs its parent, which would leave a stray config dir on machines without the app.
|
|
149
|
+
export function claudeDesktopConfigFile() {
|
|
150
|
+
const dir = claudeDesktopConfigDirFor(process.platform, os.homedir(), process.env);
|
|
151
|
+
return fs.existsSync(dir) ? path.join(dir, "claude_desktop_config.json") : null;
|
|
152
|
+
}
|
|
108
153
|
// Write generated content that ships with the CLI and changes every release, so anything
|
|
109
154
|
// that differs is overwritten: a stale copy silently feeds agents months-old guidance,
|
|
110
155
|
// which is worse than losing a local edit the user can redo (and `beryl guide` still prints
|
|
@@ -217,59 +262,88 @@ async function ensureLocalPlaywright(ctx, cwd) {
|
|
|
217
262
|
` ${cyan(installCommandsFor(gaps, cwd))}`);
|
|
218
263
|
}
|
|
219
264
|
}
|
|
265
|
+
const CLIENTS = [
|
|
266
|
+
["claude", "Claude Code + Claude desktop app"],
|
|
267
|
+
["cursor", "Cursor"],
|
|
268
|
+
["vscode", "VS Code / Copilot"],
|
|
269
|
+
["opencode", "opencode"],
|
|
270
|
+
];
|
|
271
|
+
// Whole commands, not bare flags: this is printed for someone who has to go run it.
|
|
272
|
+
const CLIENT_COMMAND_HINT = CLIENTS.map(([k, label]) => ` npx @beryl-so/cli@latest init --${k.padEnd(9)} ${label}`).join("\n");
|
|
273
|
+
// Picker labels are precise ("Claude Code + Claude desktop app"); prose wants short.
|
|
274
|
+
const CLIENT_SHORT_NAMES = CLIENTS.map(([, label]) => label.split(" + ")[0].split(" / ")[0]);
|
|
275
|
+
const CLIENT_LABEL_LIST = `${CLIENT_SHORT_NAMES.slice(0, -1).join(", ")} or ${CLIENT_SHORT_NAMES.at(-1)}`;
|
|
276
|
+
// Flags win; no flag and no TTY selects nothing.
|
|
277
|
+
async function selectClients(ctx, flags) {
|
|
278
|
+
const flagged = CLIENTS.map(([k]) => k).filter((k) => flags[k]);
|
|
279
|
+
if (flagged.length > 0)
|
|
280
|
+
return new Set(flagged);
|
|
281
|
+
if (!ctx.interactive)
|
|
282
|
+
return new Set();
|
|
283
|
+
ctx.err("Which agent should Beryl wire?");
|
|
284
|
+
CLIENTS.forEach(([, label], i) => ctx.err(` ${i + 1}. ${label}`));
|
|
285
|
+
ctx.err(` ${CLIENTS.length + 1}. Other`);
|
|
286
|
+
const answer = await ctx.prompt("Selection (comma-separated) [1]: ");
|
|
287
|
+
const picks = (answer.trim() || "1")
|
|
288
|
+
.split(",")
|
|
289
|
+
.map((part) => Number.parseInt(part.trim(), 10));
|
|
290
|
+
if (picks.some((n) => !Number.isInteger(n) || n < 1 || n > CLIENTS.length + 1)) {
|
|
291
|
+
throw new UsageError(`Pick 1-${CLIENTS.length + 1}, or pass a client flag:\n${CLIENT_COMMAND_HINT}`);
|
|
292
|
+
}
|
|
293
|
+
return new Set(picks.filter((n) => n <= CLIENTS.length).map((n) => CLIENTS[n - 1][0]));
|
|
294
|
+
}
|
|
220
295
|
export const initCommands = [
|
|
221
296
|
{
|
|
222
297
|
name: "init",
|
|
223
298
|
summary: "Set up Beryl in this repo: sign in and wire up your coding agent",
|
|
224
|
-
description: "
|
|
225
|
-
"and
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
".
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"
|
|
232
|
-
"
|
|
233
|
-
"-s user`; pass --scope project to write a committed .mcp.json for a shared repo " +
|
|
234
|
-
"instead. With --cursor the same two servers are also wired into Cursor and a repo-local " +
|
|
235
|
-
".cursor/rules/beryl.mdc authoring rule is written (Cursor's equivalent of the skill, " +
|
|
236
|
-
"since it reads neither skills dir); --vscode does the same for VS Code / GitHub Copilot " +
|
|
237
|
-
"(VS Code's user mcp.json, or .vscode/mcp.json with --scope project, plus a marked " +
|
|
238
|
-
"section in .github/copilot-instructions.md). No " +
|
|
239
|
-
"workspace/project pin and no URL prompt: ask Claude to write tests for " +
|
|
240
|
-
"your site and it resolves the workspace, project, and URL. Safe to re-run: every run " +
|
|
241
|
-
"refreshes the authoring skill to this CLI's version (overwriting an older or edited " +
|
|
242
|
-
"copy; `beryl guide` prints the same content), and skips whatever else is already set " +
|
|
243
|
-
"up.",
|
|
299
|
+
description: "Signs you in, wires the beryl and playwright MCP servers into the coding agent you " +
|
|
300
|
+
"pick, writes the beryl-test authoring skill, and installs Chromium if it is missing. " +
|
|
301
|
+
"Pick an agent with --claude, --cursor, --vscode and/or --opencode. With no flag it " +
|
|
302
|
+
"prompts (Enter picks Claude); with no flag and no TTY it wires nothing and prints the " +
|
|
303
|
+
"flags. --claude also wires the Claude DESKTOP app when installed, which needs a quit " +
|
|
304
|
+
"and reopen to pick the config up. --scope user (default) configures everything " +
|
|
305
|
+
"per-user, matching where your login token lives; --scope project writes config " +
|
|
306
|
+
"committed to the repo. Safe to re-run: it refreshes the skill to this CLI's version " +
|
|
307
|
+
"and skips whatever is already set up.",
|
|
244
308
|
interactive: true,
|
|
245
309
|
flags: [
|
|
246
310
|
{
|
|
247
311
|
name: "scope",
|
|
248
312
|
type: "string",
|
|
249
313
|
enum: ["user", "project"],
|
|
250
|
-
description: "Where to wire
|
|
251
|
-
"
|
|
252
|
-
|
|
253
|
-
|
|
314
|
+
description: "Where to wire: `user` (default) is per-user, `project` is committed to the repo. " +
|
|
315
|
+
"Every teammate still runs `beryl login` to authenticate",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "claude",
|
|
319
|
+
type: "boolean",
|
|
320
|
+
description: "Wire Claude Code (`claude mcp add -s user`, or a committed .mcp.json under " +
|
|
321
|
+
"--scope project), plus the Claude desktop app when it is installed",
|
|
254
322
|
},
|
|
255
323
|
{
|
|
256
324
|
name: "cursor",
|
|
257
325
|
type: "boolean",
|
|
258
|
-
description: "
|
|
259
|
-
"
|
|
326
|
+
description: "Wire Cursor, plus a repo-local .cursor/rules/beryl.mdc authoring rule, since " +
|
|
327
|
+
"Cursor reads no skills dir",
|
|
260
328
|
},
|
|
261
329
|
{
|
|
262
330
|
name: "vscode",
|
|
263
331
|
type: "boolean",
|
|
264
|
-
description: "
|
|
265
|
-
"
|
|
332
|
+
description: "Wire VS Code's user mcp.json (or .vscode/mcp.json under --scope project), plus a " +
|
|
333
|
+
"Beryl section in .github/copilot-instructions.md for Copilot",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "opencode",
|
|
337
|
+
type: "boolean",
|
|
338
|
+
description: "Wire ~/.config/opencode/opencode.json (or the repo's opencode.json under " +
|
|
339
|
+
"--scope project), in opencode's own config shape",
|
|
266
340
|
},
|
|
267
341
|
],
|
|
268
342
|
examples: [
|
|
269
343
|
"npx @beryl-so/cli@latest init",
|
|
270
|
-
"beryl init --
|
|
271
|
-
"beryl init --cursor",
|
|
272
|
-
"beryl init --vscode",
|
|
344
|
+
"beryl init --claude",
|
|
345
|
+
"beryl init --cursor --opencode",
|
|
346
|
+
"beryl init --vscode --scope project",
|
|
273
347
|
],
|
|
274
348
|
async run(ctx, input) {
|
|
275
349
|
const cwd = process.cwd();
|
|
@@ -311,12 +385,19 @@ export const initCommands = [
|
|
|
311
385
|
else {
|
|
312
386
|
await signIn();
|
|
313
387
|
}
|
|
388
|
+
// A signed-in init with no account id is a bug, not a reason to skip: the alias is
|
|
389
|
+
// the only thing joining pre-login CLI events to the person who later signs up.
|
|
314
390
|
if (accountId) {
|
|
315
391
|
aliasMachineToAccount(accountId);
|
|
316
|
-
flushTelemetry();
|
|
317
392
|
}
|
|
393
|
+
else if (config.token) {
|
|
394
|
+
captureCliEvent("cli_alias_skipped", config.apiUrl, { reason: "no_account_id" });
|
|
395
|
+
}
|
|
396
|
+
flushTelemetry();
|
|
318
397
|
const report = (label, fresh, where) => ctx.err(`${green("✓")} ${label} ${fresh ? "configured" : "already configured"} ${dim(where)}`);
|
|
319
|
-
|
|
398
|
+
// No "nothing wired" notice here: the closing message below says it once, in full.
|
|
399
|
+
const clients = await selectClients(ctx, input.flags);
|
|
400
|
+
if (clients.has("claude") && scope === "user") {
|
|
320
401
|
// Claude Code owns ~/.claude.json — shell out to `claude mcp add` rather than write it.
|
|
321
402
|
const berylOk = claudeUserAdd("beryl", MCP_SERVER_ENTRY);
|
|
322
403
|
const playwrightOk = claudeUserAdd("playwright", PLAYWRIGHT_SERVER_ENTRY);
|
|
@@ -329,7 +410,7 @@ export const initCommands = [
|
|
|
329
410
|
ctx.err(` ${cyan(claudeAddHint("playwright", PLAYWRIGHT_SERVER_ENTRY))}`);
|
|
330
411
|
}
|
|
331
412
|
}
|
|
332
|
-
else {
|
|
413
|
+
else if (clients.has("claude")) {
|
|
333
414
|
const file = path.join(cwd, ".mcp.json");
|
|
334
415
|
const wrote = mergeMcpConfig(file, "mcpServers", {
|
|
335
416
|
beryl: MCP_SERVER_ENTRY,
|
|
@@ -339,7 +420,20 @@ export const initCommands = [
|
|
|
339
420
|
report("beryl MCP", wrote.beryl, where);
|
|
340
421
|
report("playwright MCP", wrote.playwright, where);
|
|
341
422
|
}
|
|
342
|
-
|
|
423
|
+
// Separate surface from Claude Code, read only at launch.
|
|
424
|
+
const desktopFile = clients.has("claude") && scope === "user" ? claudeDesktopConfigFile() : null;
|
|
425
|
+
if (desktopFile) {
|
|
426
|
+
const wrote = mergeMcpConfig(desktopFile, "mcpServers", {
|
|
427
|
+
beryl: MCP_SERVER_ENTRY,
|
|
428
|
+
playwright: PLAYWRIGHT_SERVER_ENTRY,
|
|
429
|
+
});
|
|
430
|
+
report("Claude desktop beryl MCP", wrote.beryl, "(Claude desktop app)");
|
|
431
|
+
report("Claude desktop playwright MCP", wrote.playwright, "(Claude desktop app)");
|
|
432
|
+
if (wrote.beryl || wrote.playwright) {
|
|
433
|
+
ctx.err(yellow("• Quit and reopen Claude desktop: it reads that config at launch."));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (clients.has("cursor")) {
|
|
343
437
|
const file = scope === "user" ? cursorUserConfigPath() : path.join(cwd, ".cursor", "mcp.json");
|
|
344
438
|
const wrote = mergeMcpConfig(file, "mcpServers", {
|
|
345
439
|
beryl: MCP_SERVER_ENTRY,
|
|
@@ -357,7 +451,7 @@ export const initCommands = [
|
|
|
357
451
|
// VS Code (Copilot) is always repo-local: `.vscode/mcp.json` for the servers and a
|
|
358
452
|
// marked section in `.github/copilot-instructions.md` for the guide pointer. VS Code's
|
|
359
453
|
// per-user MCP config is managed through its UI, not a portable file, so scope is moot.
|
|
360
|
-
if (
|
|
454
|
+
if (clients.has("vscode")) {
|
|
361
455
|
// User scope (default) targets VS Code's real profile mcp.json, so it works in
|
|
362
456
|
// every project. Project scope, or a missing profile dir, falls back to the repo.
|
|
363
457
|
const userDir = scope === "user" ? vscodeUserConfigDir() : null;
|
|
@@ -383,6 +477,26 @@ export const initCommands = [
|
|
|
383
477
|
"Copilot may not read it. Run --vscode from your project root."));
|
|
384
478
|
}
|
|
385
479
|
}
|
|
480
|
+
// opencode reads ~/.claude/skills/, so writeSkills covers it.
|
|
481
|
+
if (clients.has("opencode")) {
|
|
482
|
+
const { file, jsonc } = opencodeConfigFile(scope, cwd);
|
|
483
|
+
if (jsonc) {
|
|
484
|
+
ctx.err(yellow(`• ${file} is JSONC and may hold comments, so it was left alone.`));
|
|
485
|
+
ctx.err(cyan(` Add these under "mcp": ${JSON.stringify({
|
|
486
|
+
beryl: opencodeEntry(MCP_SERVER_ENTRY),
|
|
487
|
+
playwright: opencodeEntry(PLAYWRIGHT_SERVER_ENTRY),
|
|
488
|
+
})}`));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
const wrote = mergeMcpConfig(file, "mcp", {
|
|
492
|
+
beryl: opencodeEntry(MCP_SERVER_ENTRY),
|
|
493
|
+
playwright: opencodeEntry(PLAYWRIGHT_SERVER_ENTRY),
|
|
494
|
+
});
|
|
495
|
+
const where = scope === "user" ? file : path.relative(cwd, file);
|
|
496
|
+
report("opencode beryl MCP", wrote.beryl, where);
|
|
497
|
+
report("opencode playwright MCP", wrote.playwright, where);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
386
500
|
const skills = writeSkills(cwd, scope);
|
|
387
501
|
for (const skill of skills) {
|
|
388
502
|
const rel = path.relative(cwd, skill.file);
|
|
@@ -398,17 +512,27 @@ export const initCommands = [
|
|
|
398
512
|
ctx.err(`${green("✓")} Beryl authoring skill ${what} ${dim(where)}`);
|
|
399
513
|
}
|
|
400
514
|
await ensureLocalPlaywright(ctx, cwd);
|
|
401
|
-
// Name the
|
|
402
|
-
const ed =
|
|
403
|
-
? { open: "open
|
|
404
|
-
:
|
|
515
|
+
// Name the agent actually wired, not the raw flag: the picker sets `clients` too.
|
|
516
|
+
const ed = clients.has("claude")
|
|
517
|
+
? { open: "open your editor", agent: "Claude" }
|
|
518
|
+
: clients.has("cursor")
|
|
405
519
|
? { open: "open Cursor", agent: "Cursor" }
|
|
406
|
-
:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
520
|
+
: clients.has("vscode")
|
|
521
|
+
? { open: "open VS Code", agent: "Copilot" }
|
|
522
|
+
: { open: "open opencode", agent: "opencode" };
|
|
523
|
+
const nextSteps = clients.size === 0
|
|
524
|
+
? `\n${bold("Beryl is signed in and the authoring skill is installed.")}\n` +
|
|
525
|
+
` ${dim("• No MCP server is wired yet, so no agent can reach Beryl.")}\n` +
|
|
526
|
+
` ${dim(`• Using ${CLIENT_LABEL_LIST}? Re-run and pick it:`)}\n` +
|
|
527
|
+
` ${cyan("npx @beryl-so/cli@latest init")}\n` +
|
|
528
|
+
` ${dim("• Any other agent: add this to its MCP config, then restart it.")}\n` +
|
|
529
|
+
`${cyan(MANUAL_CONFIG_SNIPPET)}\n` +
|
|
530
|
+
` ${dim("Some clients name that key `servers` or `mcp`.")}\n` +
|
|
531
|
+
` ${yellow(`Details: ${MANUAL_SETUP_DOCS}`)}\n`
|
|
532
|
+
: `\n${bold(`Beryl is set up. Now ${ed.open} and ask ${ed.agent} to write tests.`)}\n` +
|
|
533
|
+
` ${dim(`• Say: "write tests for https://example.com" and ${ed.agent} picks your workspace/project`)}\n` +
|
|
534
|
+
` ${dim(" and sets the URL for you (no pin, no prompt).")}\n` +
|
|
535
|
+
` ${dim(`• Playwright MCP is wired, so ${ed.agent} can drive a real browser to author from your plan.`)}\n`;
|
|
412
536
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
413
537
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
|
414
538
|
await warnIfStale(cliVersion(), (msg) => ctx.err(yellow(msg)));
|
package/dist/telemetry.js
CHANGED
|
@@ -227,6 +227,9 @@ export function baseEventProperties(apiUrl) {
|
|
|
227
227
|
// Same uuid as the X-Beryl-Session header, so a PostHog session and the Sentry tags
|
|
228
228
|
// on the API requests it caused describe the same thing.
|
|
229
229
|
beryl_session_id: sessionId,
|
|
230
|
+
// Also a property, not only the distinct id: an event whose alias never landed
|
|
231
|
+
// is still joinable to the install afterwards.
|
|
232
|
+
machine_id: machineId(),
|
|
230
233
|
};
|
|
231
234
|
}
|
|
232
235
|
let posthog;
|
|
@@ -263,6 +266,40 @@ export function aliasMachineToAccount(userId) {
|
|
|
263
266
|
// telemetry must never break a command
|
|
264
267
|
}
|
|
265
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* One event per MCP server boot, so "connected but never called a tool" is measurable.
|
|
271
|
+
*
|
|
272
|
+
* Until this existed the surface was invisible until its first `test_created` /
|
|
273
|
+
* `run_created`, which hid the drop-off between wiring the MCP up and using it. Attributed
|
|
274
|
+
* to the account when a token resolves, else to the install, which a later alias merges.
|
|
275
|
+
* Best-effort: a failed identify still emits the event rather than losing the boot.
|
|
276
|
+
*/
|
|
277
|
+
export async function captureMcpSessionStarted(apiUrl, resolveIdentity) {
|
|
278
|
+
let identity = null;
|
|
279
|
+
try {
|
|
280
|
+
identity = await resolveIdentity();
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// unauthenticated, or the lookup failed: fall back to the install
|
|
284
|
+
}
|
|
285
|
+
const client = currentClient();
|
|
286
|
+
try {
|
|
287
|
+
createPostHogClient().capture({
|
|
288
|
+
distinctId: identity?.distinctId ?? machineId(),
|
|
289
|
+
event: "mcp_session_started",
|
|
290
|
+
properties: {
|
|
291
|
+
...baseEventProperties(apiUrl),
|
|
292
|
+
surface: "mcp",
|
|
293
|
+
authenticated: identity !== null,
|
|
294
|
+
client: client?.name,
|
|
295
|
+
client_version: client?.version,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// telemetry must never break a command
|
|
301
|
+
}
|
|
302
|
+
}
|
|
266
303
|
export function flushTelemetry() {
|
|
267
304
|
// Fire-and-forget: init keeps running (login, installs) while the batch goes out, and a
|
|
268
305
|
// failed send must never surface to the user.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beryl-so/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Beryl on the command line
|
|
3
|
+
"version": "0.54.0",
|
|
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",
|
|
7
7
|
"homepage": "https://beryl.so/docs/cli",
|