@buoy-gg/mcp 7.0.31 → 7.0.32
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 +13 -1
- package/dist/{chunk-HUL2D27L.mjs → chunk-HWYVQNMR.mjs} +576 -193
- package/dist/cli.js +595 -212
- package/dist/cli.mjs +1 -1
- package/dist/index.js +595 -212
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -675,7 +675,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
675
675
|
|
|
676
676
|
// src/server.ts
|
|
677
677
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
678
|
-
var
|
|
678
|
+
var import_zod24 = require("zod");
|
|
679
679
|
|
|
680
680
|
// src/tools/generic.ts
|
|
681
681
|
var import_zod = require("zod");
|
|
@@ -686,7 +686,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
686
686
|
var import_node_path = __toESM(require("path"));
|
|
687
687
|
|
|
688
688
|
// src/version.ts
|
|
689
|
-
var VERSION = "7.0.
|
|
689
|
+
var VERSION = "7.0.32" ? "7.0.32" : "0.0.0-dev";
|
|
690
690
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
691
691
|
|
|
692
692
|
// src/update/checkForUpdate.ts
|
|
@@ -1419,7 +1419,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1419
1419
|
let sawActive = false;
|
|
1420
1420
|
let idleStreak = 0;
|
|
1421
1421
|
let lastCount = -1;
|
|
1422
|
-
let
|
|
1422
|
+
let resolved2 = null;
|
|
1423
1423
|
while (Date.now() - start < cap3) {
|
|
1424
1424
|
let snap2;
|
|
1425
1425
|
try {
|
|
@@ -1431,7 +1431,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1431
1431
|
const completed = snap2.live?.automationCompleted;
|
|
1432
1432
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1433
1433
|
if (completed?.phase === "done") {
|
|
1434
|
-
|
|
1434
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1435
1435
|
break;
|
|
1436
1436
|
}
|
|
1437
1437
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1441,11 +1441,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1441
1441
|
const count = runCountFor(snap2.index, newest);
|
|
1442
1442
|
if (count === lastCount) {
|
|
1443
1443
|
if (sawActive && ++idleStreak >= 3) {
|
|
1444
|
-
|
|
1444
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1445
1445
|
break;
|
|
1446
1446
|
}
|
|
1447
1447
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1448
|
-
|
|
1448
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1449
1449
|
break;
|
|
1450
1450
|
}
|
|
1451
1451
|
} else {
|
|
@@ -1457,11 +1457,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1457
1457
|
}
|
|
1458
1458
|
await delay2(POLL_INTERVAL_MS);
|
|
1459
1459
|
}
|
|
1460
|
-
if (!
|
|
1460
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1461
1461
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1462
1462
|
});
|
|
1463
1463
|
const snap = await readPerfSnapshot(sync, target);
|
|
1464
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1464
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1465
1465
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1466
1466
|
});
|
|
1467
1467
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -1788,8 +1788,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
1788
1788
|
}
|
|
1789
1789
|
function bodyLines(e, fetched) {
|
|
1790
1790
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
1791
|
-
const label = (inline,
|
|
1792
|
-
const value = inline ??
|
|
1791
|
+
const label = (inline, resolved2, omitted) => {
|
|
1792
|
+
const value = inline ?? resolved2;
|
|
1793
1793
|
if (value !== void 0 && value !== null) return truncate(value);
|
|
1794
1794
|
if (!omitted) return "(none)";
|
|
1795
1795
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -3370,9 +3370,9 @@ function reasonMessage(res, query) {
|
|
|
3370
3370
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
3371
3371
|
}
|
|
3372
3372
|
}
|
|
3373
|
-
function candidatesText(
|
|
3374
|
-
if (!
|
|
3375
|
-
const lines =
|
|
3373
|
+
function candidatesText(candidates2) {
|
|
3374
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
3375
|
+
const lines = candidates2.map((c) => {
|
|
3376
3376
|
const id = c.testID || c.nativeID;
|
|
3377
3377
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
3378
3378
|
});
|
|
@@ -3471,14 +3471,383 @@ function registerScreenshotTools(server, sync) {
|
|
|
3471
3471
|
);
|
|
3472
3472
|
}
|
|
3473
3473
|
|
|
3474
|
-
// src/tools/packs/
|
|
3474
|
+
// src/tools/packs/camera.ts
|
|
3475
3475
|
var import_zod15 = require("zod");
|
|
3476
|
-
|
|
3476
|
+
|
|
3477
|
+
// src/capture/camera.ts
|
|
3478
|
+
var import_node_child_process2 = require("child_process");
|
|
3479
|
+
var import_node_util2 = require("util");
|
|
3480
|
+
var import_node_fs3 = require("fs");
|
|
3481
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3482
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3483
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3484
|
+
function candidates() {
|
|
3485
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3486
|
+
const list = [
|
|
3487
|
+
...fromEnv ? [import_node_path3.default.join(fromEnv, "buoycam")] : [],
|
|
3488
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3489
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3490
|
+
];
|
|
3491
|
+
let dir = __dirname;
|
|
3492
|
+
for (let i = 0; i < 8; i++) {
|
|
3493
|
+
list.push(import_node_path3.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3494
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
3495
|
+
if (parent === dir) break;
|
|
3496
|
+
dir = parent;
|
|
3497
|
+
}
|
|
3498
|
+
return list;
|
|
3499
|
+
}
|
|
3500
|
+
var resolved = null;
|
|
3501
|
+
function cliPath() {
|
|
3502
|
+
if (resolved) return resolved;
|
|
3503
|
+
resolved = candidates().find((c) => (0, import_node_fs3.existsSync)(c)) ?? null;
|
|
3504
|
+
return resolved;
|
|
3505
|
+
}
|
|
3506
|
+
async function run(args) {
|
|
3507
|
+
const cli = cliPath();
|
|
3508
|
+
if (!cli) {
|
|
3509
|
+
return {
|
|
3510
|
+
ok: false,
|
|
3511
|
+
error: "The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
try {
|
|
3515
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3516
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
const stdout = err.stdout ?? "";
|
|
3519
|
+
try {
|
|
3520
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3521
|
+
} catch {
|
|
3522
|
+
return {
|
|
3523
|
+
ok: false,
|
|
3524
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
function describeDiagnosis(checks) {
|
|
3530
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3531
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3532
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3533
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3534
|
+
}
|
|
3535
|
+
const lines = [];
|
|
3536
|
+
for (const c of [...failed, ...warned]) {
|
|
3537
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3538
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3539
|
+
}
|
|
3540
|
+
const headline = failed.length > 0 ? `${failed.length} problem${failed.length === 1 ? "" : "s"} found.` : `No hard failures, but ${warned.length} thing${warned.length === 1 ? "" : "s"} worth knowing.`;
|
|
3541
|
+
return [headline, "", ...lines].join("\n");
|
|
3542
|
+
}
|
|
3543
|
+
function describeDevices(devices, apps) {
|
|
3544
|
+
if (devices.length === 0) {
|
|
3545
|
+
return "No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._";
|
|
3546
|
+
}
|
|
3547
|
+
const lines = [
|
|
3548
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3549
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3550
|
+
];
|
|
3551
|
+
if (devices.length > 1 && apps === null) {
|
|
3552
|
+
lines.push(
|
|
3553
|
+
"",
|
|
3554
|
+
"_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._"
|
|
3555
|
+
);
|
|
3556
|
+
return lines.join("\n");
|
|
3557
|
+
}
|
|
3558
|
+
if (apps === null) {
|
|
3559
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3560
|
+
return lines.join("\n");
|
|
3561
|
+
}
|
|
3562
|
+
if (apps.length === 0) {
|
|
3563
|
+
lines.push("", "No apps installed on it.");
|
|
3564
|
+
return lines.join("\n");
|
|
3565
|
+
}
|
|
3566
|
+
lines.push(
|
|
3567
|
+
"",
|
|
3568
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3569
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3570
|
+
"",
|
|
3571
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3572
|
+
);
|
|
3573
|
+
return lines.join("\n");
|
|
3574
|
+
}
|
|
3575
|
+
function describeInputs(cameras, windows) {
|
|
3576
|
+
const lines = [];
|
|
3577
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3578
|
+
if (cameras.length) {
|
|
3579
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3580
|
+
} else {
|
|
3581
|
+
lines.push(
|
|
3582
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
lines.push("");
|
|
3586
|
+
lines.push(
|
|
3587
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3588
|
+
);
|
|
3589
|
+
if (windows.length) {
|
|
3590
|
+
lines.push(
|
|
3591
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3592
|
+
);
|
|
3593
|
+
} else {
|
|
3594
|
+
lines.push(
|
|
3595
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
return lines.join("\n");
|
|
3599
|
+
}
|
|
3600
|
+
function entitlementPaths() {
|
|
3601
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3602
|
+
if (fromEnv) return [fromEnv];
|
|
3603
|
+
return [
|
|
3604
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3605
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3606
|
+
];
|
|
3607
|
+
}
|
|
3608
|
+
function readEntitlement() {
|
|
3609
|
+
for (const p of entitlementPaths()) {
|
|
3610
|
+
try {
|
|
3611
|
+
if (!(0, import_node_fs3.existsSync)(p)) continue;
|
|
3612
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf8"));
|
|
3613
|
+
return {
|
|
3614
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3615
|
+
isPro: parsed.isPro === true
|
|
3616
|
+
};
|
|
3617
|
+
} catch {
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return { tier: "anonymous", isPro: false };
|
|
3621
|
+
}
|
|
3622
|
+
function proRequired(tool) {
|
|
3623
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3624
|
+
|
|
3625
|
+
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel without a licence. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
|
|
3626
|
+
|
|
3627
|
+
_Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// src/tools/packs/camera.ts
|
|
3477
3631
|
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3632
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3478
3633
|
var fail15 = (message) => ({
|
|
3479
3634
|
isError: true,
|
|
3480
3635
|
content: [{ type: "text", text: message }]
|
|
3481
3636
|
});
|
|
3637
|
+
function registerCameraTools(server) {
|
|
3638
|
+
server.registerTool(
|
|
3639
|
+
"camera_source",
|
|
3640
|
+
{
|
|
3641
|
+
description: "Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",
|
|
3642
|
+
inputSchema: {
|
|
3643
|
+
kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3644
|
+
value: import_zod15.z.string().optional().describe(
|
|
3645
|
+
"Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."
|
|
3646
|
+
),
|
|
3647
|
+
format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3648
|
+
}
|
|
3649
|
+
},
|
|
3650
|
+
async (args) => {
|
|
3651
|
+
const blocked = gate("camera_source");
|
|
3652
|
+
if (blocked) return blocked;
|
|
3653
|
+
const cmd = ["source", args.kind];
|
|
3654
|
+
if (args.value) cmd.push(args.value);
|
|
3655
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3656
|
+
const result = await run(cmd);
|
|
3657
|
+
if (!result.ok) {
|
|
3658
|
+
return fail15(
|
|
3659
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
const what = args.kind === "barcode" ? `a ${args.format ?? "qr"} code reading "${args.value}"` : args.kind === "screen" ? `this Mac's screen (${args.value ?? "behind the simulator window"})` : args.value ?? args.kind;
|
|
3663
|
+
return text13(
|
|
3664
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3665
|
+
|
|
3666
|
+
_Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
);
|
|
3670
|
+
server.registerTool(
|
|
3671
|
+
"camera_status",
|
|
3672
|
+
{
|
|
3673
|
+
description: "Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",
|
|
3674
|
+
inputSchema: {}
|
|
3675
|
+
},
|
|
3676
|
+
async () => {
|
|
3677
|
+
const blocked = gate("camera_status");
|
|
3678
|
+
if (blocked) return blocked;
|
|
3679
|
+
const result = await run(["status"]);
|
|
3680
|
+
if (!result.ok || !result.data) {
|
|
3681
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3682
|
+
}
|
|
3683
|
+
const { publishing, fps, codes } = result.data;
|
|
3684
|
+
if (!publishing) {
|
|
3685
|
+
return text13(
|
|
3686
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
return text13(
|
|
3690
|
+
[
|
|
3691
|
+
`Publishing at ${fps} fps.`,
|
|
3692
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3693
|
+
"",
|
|
3694
|
+
"_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"
|
|
3695
|
+
].join("\n")
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
);
|
|
3699
|
+
server.registerTool(
|
|
3700
|
+
"camera_launch",
|
|
3701
|
+
{
|
|
3702
|
+
description: "Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",
|
|
3703
|
+
inputSchema: {
|
|
3704
|
+
bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
|
|
3705
|
+
udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3706
|
+
}
|
|
3707
|
+
},
|
|
3708
|
+
async (args) => {
|
|
3709
|
+
const blocked = gate("camera_launch");
|
|
3710
|
+
if (blocked) return blocked;
|
|
3711
|
+
const cmd = ["launch", args.bundleId];
|
|
3712
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3713
|
+
const result = await run(cmd);
|
|
3714
|
+
if (!result.ok) {
|
|
3715
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3716
|
+
}
|
|
3717
|
+
return text13(
|
|
3718
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
);
|
|
3722
|
+
server.registerTool(
|
|
3723
|
+
"camera_diagnose",
|
|
3724
|
+
{
|
|
3725
|
+
description: "Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",
|
|
3726
|
+
inputSchema: {
|
|
3727
|
+
bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3728
|
+
}
|
|
3729
|
+
},
|
|
3730
|
+
async (args) => {
|
|
3731
|
+
const cmd = ["diagnose"];
|
|
3732
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3733
|
+
const result = await run(cmd);
|
|
3734
|
+
if (!result.data?.checks) {
|
|
3735
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3736
|
+
}
|
|
3737
|
+
const ent = readEntitlement();
|
|
3738
|
+
const tierNote = ent.isPro ? "" : "\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";
|
|
3739
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3740
|
+
}
|
|
3741
|
+
);
|
|
3742
|
+
server.registerTool(
|
|
3743
|
+
"camera_stop",
|
|
3744
|
+
{
|
|
3745
|
+
description: "Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",
|
|
3746
|
+
inputSchema: {}
|
|
3747
|
+
},
|
|
3748
|
+
async () => {
|
|
3749
|
+
const blocked = gate("camera_stop");
|
|
3750
|
+
if (blocked) return blocked;
|
|
3751
|
+
const result = await run(["stop"]);
|
|
3752
|
+
if (!result.ok) {
|
|
3753
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3754
|
+
}
|
|
3755
|
+
return text13("The simulator camera is off.");
|
|
3756
|
+
}
|
|
3757
|
+
);
|
|
3758
|
+
server.registerTool(
|
|
3759
|
+
"camera_devices",
|
|
3760
|
+
{
|
|
3761
|
+
description: "List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",
|
|
3762
|
+
inputSchema: {
|
|
3763
|
+
udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3764
|
+
}
|
|
3765
|
+
},
|
|
3766
|
+
async (args) => {
|
|
3767
|
+
const blocked = gate("camera_devices");
|
|
3768
|
+
if (blocked) return blocked;
|
|
3769
|
+
const devicesResult = await run(["devices"]);
|
|
3770
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3771
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3772
|
+
}
|
|
3773
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3774
|
+
let apps = null;
|
|
3775
|
+
if (devices.length === 1 || args.udid) {
|
|
3776
|
+
const appsResult = await run([
|
|
3777
|
+
"apps",
|
|
3778
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3779
|
+
]);
|
|
3780
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3781
|
+
}
|
|
3782
|
+
return text13(describeDevices(devices, apps));
|
|
3783
|
+
}
|
|
3784
|
+
);
|
|
3785
|
+
server.registerTool(
|
|
3786
|
+
"camera_inputs",
|
|
3787
|
+
{
|
|
3788
|
+
description: "List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",
|
|
3789
|
+
inputSchema: {}
|
|
3790
|
+
},
|
|
3791
|
+
async () => {
|
|
3792
|
+
const blocked = gate("camera_inputs");
|
|
3793
|
+
if (blocked) return blocked;
|
|
3794
|
+
const [cams, wins] = await Promise.all([
|
|
3795
|
+
run(["cameras"]),
|
|
3796
|
+
run(["windows"])
|
|
3797
|
+
]);
|
|
3798
|
+
if (!cams.ok && !wins.ok) {
|
|
3799
|
+
return fail15(
|
|
3800
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
return text13(
|
|
3804
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
);
|
|
3808
|
+
server.registerTool(
|
|
3809
|
+
"camera_zero_setup",
|
|
3810
|
+
{
|
|
3811
|
+
description: "Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",
|
|
3812
|
+
inputSchema: {
|
|
3813
|
+
action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3814
|
+
udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3815
|
+
}
|
|
3816
|
+
},
|
|
3817
|
+
async (args) => {
|
|
3818
|
+
const blocked = gate("camera_zero_setup");
|
|
3819
|
+
if (blocked) return blocked;
|
|
3820
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3821
|
+
if (!result.ok) {
|
|
3822
|
+
return fail15(
|
|
3823
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3824
|
+
);
|
|
3825
|
+
}
|
|
3826
|
+
const d = result.data ?? {};
|
|
3827
|
+
if (args.action === "status") {
|
|
3828
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3829
|
+
return text13(
|
|
3830
|
+
d.foreign ? `Zero-setup is on, but **another tool owns it** (\`${d.path}\`). Quit that tool before turning Buoy's on.` : "Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera."
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
if (args.action === "on") {
|
|
3834
|
+
return text13(
|
|
3835
|
+
"Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._"
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
return text13("Zero-setup mode is **off**.");
|
|
3839
|
+
}
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
// src/tools/packs/interact.ts
|
|
3844
|
+
var import_zod16 = require("zod");
|
|
3845
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3846
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3847
|
+
var fail16 = (message) => ({
|
|
3848
|
+
isError: true,
|
|
3849
|
+
content: [{ type: "text", text: message }]
|
|
3850
|
+
});
|
|
3482
3851
|
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3483
3852
|
function formatElement(e) {
|
|
3484
3853
|
let line = e.interactive ? "\u2022 " : " ";
|
|
@@ -3502,8 +3871,8 @@ function registerInteractTools(server, sync) {
|
|
|
3502
3871
|
{
|
|
3503
3872
|
description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. If you already know the sequence you want to drive, `run_flow` does the whole thing (taps, waits, assertions) in one call instead. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
|
|
3504
3873
|
inputSchema: {
|
|
3505
|
-
interactiveOnly:
|
|
3506
|
-
deviceId:
|
|
3874
|
+
interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3875
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3507
3876
|
}
|
|
3508
3877
|
},
|
|
3509
3878
|
async (args) => {
|
|
@@ -3513,16 +3882,16 @@ function registerInteractTools(server, sync) {
|
|
|
3513
3882
|
const all = res?.elements ?? [];
|
|
3514
3883
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3515
3884
|
if (elements.length === 0) {
|
|
3516
|
-
return
|
|
3885
|
+
return text14(
|
|
3517
3886
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3518
3887
|
);
|
|
3519
3888
|
}
|
|
3520
3889
|
const header = `Screen ${res.screen.width}\xD7${res.screen.height} \u2014 ${elements.length} element${elements.length === 1 ? "" : "s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).`;
|
|
3521
|
-
return
|
|
3890
|
+
return text14(`${header}
|
|
3522
3891
|
|
|
3523
3892
|
${elements.map(formatElement).join("\n")}`);
|
|
3524
3893
|
} catch (e) {
|
|
3525
|
-
return
|
|
3894
|
+
return fail16(errMsg15(e));
|
|
3526
3895
|
}
|
|
3527
3896
|
}
|
|
3528
3897
|
);
|
|
@@ -3531,20 +3900,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3531
3900
|
{
|
|
3532
3901
|
description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Driving MORE THAN ONE step (reproducing a bug, verifying a fix)? Use `run_flow` instead \u2014 it runs the whole sequence in one call. Requires Buoy Pro.",
|
|
3533
3902
|
inputSchema: {
|
|
3534
|
-
nativeTag:
|
|
3535
|
-
testID:
|
|
3536
|
-
query:
|
|
3537
|
-
value:
|
|
3538
|
-
text:
|
|
3539
|
-
longPress:
|
|
3540
|
-
scrollIntoView:
|
|
3541
|
-
deviceId:
|
|
3903
|
+
nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3904
|
+
testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
|
|
3905
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3906
|
+
value: import_zod16.z.union([import_zod16.z.number(), import_zod16.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
3907
|
+
text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3908
|
+
longPress: import_zod16.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3909
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3910
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3542
3911
|
}
|
|
3543
3912
|
},
|
|
3544
3913
|
async (args) => {
|
|
3545
3914
|
try {
|
|
3546
3915
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3547
|
-
return
|
|
3916
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3548
3917
|
}
|
|
3549
3918
|
const target = sync.requireProDevice(args.deviceId);
|
|
3550
3919
|
const res = await sync.callAction(
|
|
@@ -3569,25 +3938,25 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3569
3938
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3570
3939
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3571
3940
|
const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : m.via === "onLongPress" ? `Long-pressed "${label}"${id}` : `Tapped "${label}"${id}`;
|
|
3572
|
-
return
|
|
3941
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3573
3942
|
}
|
|
3574
3943
|
let msg = res?.reason ?? "Tap failed.";
|
|
3575
3944
|
if (res?.candidates?.length) {
|
|
3576
3945
|
msg += "\n\nTappable candidates:\n" + res.candidates.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n");
|
|
3577
3946
|
}
|
|
3578
|
-
return
|
|
3947
|
+
return fail16(msg);
|
|
3579
3948
|
} catch (e) {
|
|
3580
|
-
return
|
|
3949
|
+
return fail16(errMsg15(e));
|
|
3581
3950
|
}
|
|
3582
3951
|
}
|
|
3583
3952
|
);
|
|
3584
3953
|
}
|
|
3585
3954
|
|
|
3586
3955
|
// src/tools/packs/jsThread.ts
|
|
3587
|
-
var
|
|
3956
|
+
var import_zod17 = require("zod");
|
|
3588
3957
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3589
|
-
var
|
|
3590
|
-
var
|
|
3958
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3959
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3591
3960
|
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3592
3961
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3593
3962
|
function sparkline(values) {
|
|
@@ -3659,9 +4028,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3659
4028
|
{
|
|
3660
4029
|
description: 'Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. NOT the tool for React components re-rendering too often \u2014 this measures TASKS on the thread, not renders; for "the page re-renders constantly / typing lags / tapping one button re-renders everything" use measure_renders. Requires @buoy-gg/js-top on the device.',
|
|
3661
4030
|
inputSchema: {
|
|
3662
|
-
deviceId:
|
|
3663
|
-
durationMs:
|
|
3664
|
-
clearFirst:
|
|
4031
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4032
|
+
durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4033
|
+
clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3665
4034
|
}
|
|
3666
4035
|
},
|
|
3667
4036
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3674,7 +4043,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3674
4043
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3675
4044
|
target
|
|
3676
4045
|
);
|
|
3677
|
-
if (!snap) return
|
|
4046
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3678
4047
|
const md = formatTopMarkdown(snap, duration);
|
|
3679
4048
|
const slim = {
|
|
3680
4049
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3693,13 +4062,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3693
4062
|
worstFreezeMs: r.worstFreezeMs
|
|
3694
4063
|
}))
|
|
3695
4064
|
};
|
|
3696
|
-
return
|
|
4065
|
+
return text15(`${md}
|
|
3697
4066
|
|
|
3698
4067
|
\`\`\`json
|
|
3699
4068
|
${JSON.stringify(slim)}
|
|
3700
4069
|
\`\`\``);
|
|
3701
4070
|
} catch (e) {
|
|
3702
|
-
return
|
|
4071
|
+
return fail17(errMsg16(e));
|
|
3703
4072
|
}
|
|
3704
4073
|
}
|
|
3705
4074
|
);
|
|
@@ -3708,8 +4077,8 @@ ${JSON.stringify(slim)}
|
|
|
3708
4077
|
{
|
|
3709
4078
|
description: 'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',
|
|
3710
4079
|
inputSchema: {
|
|
3711
|
-
deviceId:
|
|
3712
|
-
key:
|
|
4080
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4081
|
+
key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3713
4082
|
}
|
|
3714
4083
|
},
|
|
3715
4084
|
async ({ deviceId, key }) => {
|
|
@@ -3722,18 +4091,18 @@ ${JSON.stringify(slim)}
|
|
|
3722
4091
|
target
|
|
3723
4092
|
);
|
|
3724
4093
|
if (!detail) {
|
|
3725
|
-
return
|
|
4094
|
+
return fail17(
|
|
3726
4095
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3727
4096
|
);
|
|
3728
4097
|
}
|
|
3729
4098
|
const md = formatDetailMarkdown(detail);
|
|
3730
|
-
return
|
|
4099
|
+
return text15(`${md}
|
|
3731
4100
|
|
|
3732
4101
|
\`\`\`json
|
|
3733
4102
|
${JSON.stringify(detail)}
|
|
3734
4103
|
\`\`\``);
|
|
3735
4104
|
} catch (e) {
|
|
3736
|
-
return
|
|
4105
|
+
return fail17(errMsg16(e));
|
|
3737
4106
|
}
|
|
3738
4107
|
}
|
|
3739
4108
|
);
|
|
@@ -3742,26 +4111,26 @@ ${JSON.stringify(detail)}
|
|
|
3742
4111
|
{
|
|
3743
4112
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3744
4113
|
inputSchema: {
|
|
3745
|
-
deviceId:
|
|
4114
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3746
4115
|
}
|
|
3747
4116
|
},
|
|
3748
4117
|
async ({ deviceId }) => {
|
|
3749
4118
|
try {
|
|
3750
4119
|
const target = sync.requireProDevice(deviceId);
|
|
3751
4120
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3752
|
-
return
|
|
4121
|
+
return text15("Cleared js-top stats.");
|
|
3753
4122
|
} catch (e) {
|
|
3754
|
-
return
|
|
4123
|
+
return fail17(errMsg16(e));
|
|
3755
4124
|
}
|
|
3756
4125
|
}
|
|
3757
4126
|
);
|
|
3758
4127
|
}
|
|
3759
4128
|
|
|
3760
4129
|
// src/tools/packs/images.ts
|
|
3761
|
-
var
|
|
4130
|
+
var import_zod18 = require("zod");
|
|
3762
4131
|
var IMAGES_TOOL_ID = "images";
|
|
3763
|
-
var
|
|
3764
|
-
var
|
|
4132
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4133
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3765
4134
|
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3766
4135
|
function uriTail(uri) {
|
|
3767
4136
|
if (!uri) return "(no source)";
|
|
@@ -3850,9 +4219,9 @@ function registerImagesTools(server, sync) {
|
|
|
3850
4219
|
{
|
|
3851
4220
|
description: "Live image registry from the app: every RN <Image> and expo-image load with cache verdict (memory/disk/network), load ms, decoded vs displayed size with an oversize/upscale verdict and wasted-KB estimate, error details, and any active simulation overrides. Image HTTP never hits the JS network stack, so this is the only visibility into image loading. Requires @buoy-gg/images on the device.",
|
|
3852
4221
|
inputSchema: {
|
|
3853
|
-
deviceId:
|
|
3854
|
-
limit:
|
|
3855
|
-
status:
|
|
4222
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4223
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4224
|
+
status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3856
4225
|
}
|
|
3857
4226
|
},
|
|
3858
4227
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3865,7 +4234,7 @@ function registerImagesTools(server, sync) {
|
|
|
3865
4234
|
{ limit: capped, status },
|
|
3866
4235
|
target
|
|
3867
4236
|
);
|
|
3868
|
-
if (!list) return
|
|
4237
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3869
4238
|
const md = formatListMarkdown(list, capped);
|
|
3870
4239
|
const slim = {
|
|
3871
4240
|
stats: list.stats,
|
|
@@ -3892,13 +4261,13 @@ function registerImagesTools(server, sync) {
|
|
|
3892
4261
|
layoutShifts: r.layoutShifts
|
|
3893
4262
|
}))
|
|
3894
4263
|
};
|
|
3895
|
-
return
|
|
4264
|
+
return text16(`${md}
|
|
3896
4265
|
|
|
3897
4266
|
\`\`\`json
|
|
3898
4267
|
${JSON.stringify(slim)}
|
|
3899
4268
|
\`\`\``);
|
|
3900
4269
|
} catch (e) {
|
|
3901
|
-
return
|
|
4270
|
+
return fail18(errMsg17(e));
|
|
3902
4271
|
}
|
|
3903
4272
|
}
|
|
3904
4273
|
);
|
|
@@ -3907,9 +4276,9 @@ ${JSON.stringify(slim)}
|
|
|
3907
4276
|
{
|
|
3908
4277
|
description: "Act on one image from get_images (by id): hard_reload (bypass caches + refetch), retry, evict_disk (expo disk entry), flash (red border to locate it on screen), prove_savings (re-encode at displayed size as WebP ON DEVICE and return real byte savings + a previewable file:// uri), or simulation overrides \u2014 force_error (instant offline-safe failure), force_loading (request hangs forever), force_blank, swap_url (requires url), restore (remove override). Returns the record's state after the action so the effect is visible.",
|
|
3909
4278
|
inputSchema: {
|
|
3910
|
-
deviceId:
|
|
3911
|
-
id:
|
|
3912
|
-
action:
|
|
4279
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4280
|
+
id: import_zod18.z.number().describe("Record id from get_images."),
|
|
4281
|
+
action: import_zod18.z.enum([
|
|
3913
4282
|
"hard_reload",
|
|
3914
4283
|
"retry",
|
|
3915
4284
|
"evict_disk",
|
|
@@ -3921,7 +4290,7 @@ ${JSON.stringify(slim)}
|
|
|
3921
4290
|
"swap_url",
|
|
3922
4291
|
"restore"
|
|
3923
4292
|
]).describe("What to do."),
|
|
3924
|
-
url:
|
|
4293
|
+
url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3925
4294
|
}
|
|
3926
4295
|
},
|
|
3927
4296
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3929,8 +4298,8 @@ ${JSON.stringify(slim)}
|
|
|
3929
4298
|
const target = sync.requireProDevice(deviceId);
|
|
3930
4299
|
if (action === "prove_savings") {
|
|
3931
4300
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3932
|
-
if (!savings) return
|
|
3933
|
-
if (!savings.ok) return
|
|
4301
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4302
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3934
4303
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3935
4304
|
if (savings.originalBytes != null)
|
|
3936
4305
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3942,11 +4311,11 @@ ${JSON.stringify(slim)}
|
|
|
3942
4311
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3943
4312
|
if (savings.optimizedUri)
|
|
3944
4313
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3945
|
-
return
|
|
4314
|
+
return text16(parts.join("\n"));
|
|
3946
4315
|
}
|
|
3947
4316
|
const entry = ACTION_MAP[action];
|
|
3948
|
-
if (!entry) return
|
|
3949
|
-
if (action === "swap_url" && !url) return
|
|
4317
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4318
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3950
4319
|
const result = await sync.callAction(
|
|
3951
4320
|
IMAGES_TOOL_ID,
|
|
3952
4321
|
entry.action,
|
|
@@ -3966,9 +4335,9 @@ ${JSON.stringify(slim)}
|
|
|
3966
4335
|
);
|
|
3967
4336
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3968
4337
|
}
|
|
3969
|
-
return
|
|
4338
|
+
return text16(lines.join("\n"));
|
|
3970
4339
|
} catch (e) {
|
|
3971
|
-
return
|
|
4340
|
+
return fail18(errMsg17(e));
|
|
3972
4341
|
}
|
|
3973
4342
|
}
|
|
3974
4343
|
);
|
|
@@ -3977,12 +4346,12 @@ ${JSON.stringify(slim)}
|
|
|
3977
4346
|
{
|
|
3978
4347
|
description: "App-wide image simulation modes: network 'offline' (every NETWORK image load fails immediately, both libs \u2014 bundled assets still load; reload/remount on-screen images to see it), 'cold' (every image bypasses caches \u2014 first-launch behavior, nothing cleared), 'normal' to reset. blankImages renders every image without a source. massAction applies per-image simulations to everything mounted. Also clears the registry or expo caches when asked.",
|
|
3979
4348
|
inputSchema: {
|
|
3980
|
-
deviceId:
|
|
3981
|
-
network:
|
|
3982
|
-
blankImages:
|
|
3983
|
-
massAction:
|
|
3984
|
-
clearRegistry:
|
|
3985
|
-
clearExpoCaches:
|
|
4349
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4350
|
+
network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4351
|
+
blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
|
|
4352
|
+
massAction: import_zod18.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
4353
|
+
clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
|
|
4354
|
+
clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3986
4355
|
}
|
|
3987
4356
|
},
|
|
3988
4357
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -4009,17 +4378,17 @@ ${JSON.stringify(slim)}
|
|
|
4009
4378
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
4010
4379
|
done.push(r?.message ?? "expo caches cleared");
|
|
4011
4380
|
}
|
|
4012
|
-
if (done.length === 0) return
|
|
4013
|
-
return
|
|
4381
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4382
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
4014
4383
|
} catch (e) {
|
|
4015
|
-
return
|
|
4384
|
+
return fail18(errMsg17(e));
|
|
4016
4385
|
}
|
|
4017
4386
|
}
|
|
4018
4387
|
);
|
|
4019
4388
|
}
|
|
4020
4389
|
|
|
4021
4390
|
// src/tools/packs/assets.ts
|
|
4022
|
-
var
|
|
4391
|
+
var import_zod19 = require("zod");
|
|
4023
4392
|
|
|
4024
4393
|
// src/tools/packs/assetsGraphNote.ts
|
|
4025
4394
|
function graphUnavailableNote(ss) {
|
|
@@ -4034,8 +4403,8 @@ function graphUnavailableNote(ss) {
|
|
|
4034
4403
|
|
|
4035
4404
|
// src/tools/packs/assets.ts
|
|
4036
4405
|
var ASSETS_TOOL_ID = "assets";
|
|
4037
|
-
var
|
|
4038
|
-
var
|
|
4406
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4407
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4039
4408
|
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
4040
4409
|
function kb2(bytes) {
|
|
4041
4410
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
@@ -4124,10 +4493,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4124
4493
|
{
|
|
4125
4494
|
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
4126
4495
|
inputSchema: {
|
|
4127
|
-
deviceId:
|
|
4128
|
-
limit:
|
|
4129
|
-
kind:
|
|
4130
|
-
unusedOnly:
|
|
4496
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4497
|
+
limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4498
|
+
kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4499
|
+
unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4131
4500
|
}
|
|
4132
4501
|
},
|
|
4133
4502
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4140,7 +4509,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4140
4509
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4141
4510
|
target
|
|
4142
4511
|
);
|
|
4143
|
-
if (!list) return
|
|
4512
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4144
4513
|
if (unusedOnly) {
|
|
4145
4514
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4146
4515
|
}
|
|
@@ -4164,13 +4533,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4164
4533
|
sizeSource: r.sizeSource
|
|
4165
4534
|
}))
|
|
4166
4535
|
};
|
|
4167
|
-
return
|
|
4536
|
+
return text17(`${md}
|
|
4168
4537
|
|
|
4169
4538
|
\`\`\`json
|
|
4170
4539
|
${JSON.stringify(slim)}
|
|
4171
4540
|
\`\`\``);
|
|
4172
4541
|
} catch (e) {
|
|
4173
|
-
return
|
|
4542
|
+
return fail19(errMsg18(e));
|
|
4174
4543
|
}
|
|
4175
4544
|
}
|
|
4176
4545
|
);
|
|
@@ -4179,9 +4548,9 @@ ${JSON.stringify(slim)}
|
|
|
4179
4548
|
{
|
|
4180
4549
|
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
4181
4550
|
inputSchema: {
|
|
4182
|
-
deviceId:
|
|
4183
|
-
action:
|
|
4184
|
-
id:
|
|
4551
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4552
|
+
action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4553
|
+
id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4185
4554
|
}
|
|
4186
4555
|
},
|
|
4187
4556
|
async ({ deviceId, action, id }) => {
|
|
@@ -4191,19 +4560,19 @@ ${JSON.stringify(slim)}
|
|
|
4191
4560
|
case "rescan": {
|
|
4192
4561
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4193
4562
|
const ss = result?.scanStatus;
|
|
4194
|
-
if (!ss) return
|
|
4195
|
-
return
|
|
4563
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4564
|
+
return text17(
|
|
4196
4565
|
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
4197
4566
|
);
|
|
4198
4567
|
}
|
|
4199
4568
|
case "measure_sizes": {
|
|
4200
4569
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4201
|
-
return
|
|
4570
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4202
4571
|
}
|
|
4203
4572
|
case "detail": {
|
|
4204
|
-
if (id == null) return
|
|
4573
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
4205
4574
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4206
|
-
if (!detail) return
|
|
4575
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4207
4576
|
const lines = [
|
|
4208
4577
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4209
4578
|
"",
|
|
@@ -4221,32 +4590,32 @@ ${JSON.stringify(slim)}
|
|
|
4221
4590
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4222
4591
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4223
4592
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4224
|
-
return
|
|
4593
|
+
return text17(lines.join("\n"));
|
|
4225
4594
|
}
|
|
4226
4595
|
case "save_baseline": {
|
|
4227
4596
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4228
|
-
return
|
|
4597
|
+
return text17(
|
|
4229
4598
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4230
4599
|
);
|
|
4231
4600
|
}
|
|
4232
4601
|
case "clear_baseline": {
|
|
4233
4602
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4234
|
-
return
|
|
4603
|
+
return text17("Baseline cleared.");
|
|
4235
4604
|
}
|
|
4236
4605
|
case "clear_records": {
|
|
4237
4606
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4238
|
-
return
|
|
4607
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
4239
4608
|
}
|
|
4240
4609
|
}
|
|
4241
4610
|
} catch (e) {
|
|
4242
|
-
return
|
|
4611
|
+
return fail19(errMsg18(e));
|
|
4243
4612
|
}
|
|
4244
4613
|
}
|
|
4245
4614
|
);
|
|
4246
4615
|
}
|
|
4247
4616
|
|
|
4248
4617
|
// src/tools/packs/reload.ts
|
|
4249
|
-
var
|
|
4618
|
+
var import_zod20 = require("zod");
|
|
4250
4619
|
|
|
4251
4620
|
// src/capture/devServerReload.ts
|
|
4252
4621
|
var import_ws = require("ws");
|
|
@@ -4309,8 +4678,8 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4309
4678
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4310
4679
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4311
4680
|
var PROBE_INTERVAL_MS = 400;
|
|
4312
|
-
var
|
|
4313
|
-
var
|
|
4681
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4682
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4314
4683
|
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4315
4684
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4316
4685
|
async function ping(sync, deviceId, timeoutMs) {
|
|
@@ -4350,12 +4719,12 @@ function registerReloadTools(server, sync) {
|
|
|
4350
4719
|
{
|
|
4351
4720
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, to re-run app startup, or to RECOVER A CRASHED APP: after a fatal error the other tools stop answering, and reloading is how you get them back (it falls through to the dev server when the app is too broken to reload itself). In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost \u2014 read anything you need from get_console/get_storage BEFORE reloading. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
4352
4721
|
inputSchema: {
|
|
4353
|
-
strategy:
|
|
4722
|
+
strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4354
4723
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4355
4724
|
),
|
|
4356
|
-
wait:
|
|
4357
|
-
timeoutMs:
|
|
4358
|
-
deviceId:
|
|
4725
|
+
wait: import_zod20.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4726
|
+
timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4727
|
+
deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4359
4728
|
}
|
|
4360
4729
|
},
|
|
4361
4730
|
async (args) => {
|
|
@@ -4397,28 +4766,28 @@ function registerReloadTools(server, sync) {
|
|
|
4397
4766
|
}
|
|
4398
4767
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4399
4768
|
if (args.wait === false) {
|
|
4400
|
-
return
|
|
4769
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4401
4770
|
}
|
|
4402
4771
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4403
4772
|
if (elapsed == null) {
|
|
4404
|
-
return
|
|
4773
|
+
return fail20(
|
|
4405
4774
|
`Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
|
|
4406
4775
|
);
|
|
4407
4776
|
}
|
|
4408
|
-
return
|
|
4777
|
+
return text18(
|
|
4409
4778
|
`Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
|
|
4410
4779
|
);
|
|
4411
4780
|
} catch (e) {
|
|
4412
|
-
return
|
|
4781
|
+
return fail20(errMsg19(e));
|
|
4413
4782
|
}
|
|
4414
4783
|
}
|
|
4415
4784
|
);
|
|
4416
4785
|
}
|
|
4417
4786
|
|
|
4418
4787
|
// src/tools/packs/triage.ts
|
|
4419
|
-
var
|
|
4420
|
-
var
|
|
4421
|
-
var
|
|
4788
|
+
var import_zod21 = require("zod");
|
|
4789
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4790
|
+
var fail21 = (message) => ({
|
|
4422
4791
|
isError: true,
|
|
4423
4792
|
content: [{ type: "text", text: message }]
|
|
4424
4793
|
});
|
|
@@ -4453,9 +4822,9 @@ async function otherBackends(sync, target, opts) {
|
|
|
4453
4822
|
return notes.length ? `
|
|
4454
4823
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4455
4824
|
}
|
|
4456
|
-
async function section(title,
|
|
4825
|
+
async function section(title, run2) {
|
|
4457
4826
|
try {
|
|
4458
|
-
const body = await
|
|
4827
|
+
const body = await run2();
|
|
4459
4828
|
return body ? `${title}
|
|
4460
4829
|
${body}` : null;
|
|
4461
4830
|
} catch (e) {
|
|
@@ -4474,7 +4843,7 @@ function registerTriageTools(server, sync) {
|
|
|
4474
4843
|
{
|
|
4475
4844
|
description: "START HERE when debugging a running app. One call = the app's current runtime state: connected device, recent console errors (including [FATAL] crashes), recent network requests with statuses, persisted storage values, and what's on screen right now. Use it as the first step for ANY 'why is the app doing X' question \u2014 it is far cheaper than calling get_console + get_network_requests + get_storage + describe_screen separately, and it tells you which of those to drill into next. If the app has crashed it says so and shows the crash. Requires Buoy Pro.",
|
|
4476
4845
|
inputSchema: {
|
|
4477
|
-
deviceId:
|
|
4846
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4478
4847
|
}
|
|
4479
4848
|
},
|
|
4480
4849
|
async (args) => {
|
|
@@ -4581,7 +4950,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4581
4950
|
|
|
4582
4951
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4583
4952
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4584
|
-
return
|
|
4953
|
+
return text19(
|
|
4585
4954
|
`${head}${banner}
|
|
4586
4955
|
|
|
4587
4956
|
${body}
|
|
@@ -4589,18 +4958,18 @@ ${body}
|
|
|
4589
4958
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4590
4959
|
);
|
|
4591
4960
|
} catch (e) {
|
|
4592
|
-
return
|
|
4961
|
+
return fail21(errMsg20(e));
|
|
4593
4962
|
}
|
|
4594
4963
|
}
|
|
4595
4964
|
);
|
|
4596
4965
|
}
|
|
4597
4966
|
|
|
4598
4967
|
// src/tools/packs/flow.ts
|
|
4599
|
-
var
|
|
4968
|
+
var import_zod22 = require("zod");
|
|
4600
4969
|
var UI_TOOL_ID = "highlight-updates";
|
|
4601
4970
|
var APP_TOOL_ID2 = "app";
|
|
4602
|
-
var
|
|
4603
|
-
var
|
|
4971
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4972
|
+
var fail22 = (message) => ({
|
|
4604
4973
|
isError: true,
|
|
4605
4974
|
content: [{ type: "text", text: message }]
|
|
4606
4975
|
});
|
|
@@ -4621,19 +4990,19 @@ function formatElement2(e) {
|
|
|
4621
4990
|
const display = e.text ?? e.label;
|
|
4622
4991
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4623
4992
|
}
|
|
4624
|
-
var stepSchema =
|
|
4625
|
-
tap:
|
|
4626
|
-
longPress:
|
|
4627
|
-
type:
|
|
4628
|
-
into:
|
|
4629
|
-
set:
|
|
4630
|
-
reload:
|
|
4631
|
-
waitMs:
|
|
4632
|
-
waitFor:
|
|
4633
|
-
waitGone:
|
|
4634
|
-
expect:
|
|
4635
|
-
expectGone:
|
|
4636
|
-
timeoutMs:
|
|
4993
|
+
var stepSchema = import_zod22.z.object({
|
|
4994
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4995
|
+
longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4996
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4997
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4998
|
+
set: import_zod22.z.union([import_zod22.z.number(), import_zod22.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4999
|
+
reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5000
|
+
waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5001
|
+
waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5002
|
+
waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5003
|
+
expect: import_zod22.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
5004
|
+
expectGone: import_zod22.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
5005
|
+
timeoutMs: import_zod22.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4637
5006
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4638
5007
|
function registerFlowTools(server, sync) {
|
|
4639
5008
|
server.registerTool(
|
|
@@ -4641,10 +5010,10 @@ function registerFlowTools(server, sync) {
|
|
|
4641
5010
|
{
|
|
4642
5011
|
description: "Run a scripted sequence of UI interactions in ONE call and report what happened: per-step results, the final screen, plus the network requests and console errors the flow produced. Use this instead of alternating describe_screen/tap_element \u2014 reproducing a bug ('open cart, checkout, submit') or verifying a fix after an edit is one call, not a dozen. Steps run in order and stop at the first failure, returning the screen at that point so you can see WHERE it broke. Each step can tap, type, set a toggle/slider, reload, wait for an element, and assert. Requires Buoy Pro.",
|
|
4643
5012
|
inputSchema: {
|
|
4644
|
-
steps:
|
|
4645
|
-
include:
|
|
4646
|
-
continueOnFailure:
|
|
4647
|
-
deviceId:
|
|
5013
|
+
steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5014
|
+
include: import_zod22.z.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final screen + network/console produced by the flow; 'screen' = final screen only; 'none' = step results only."),
|
|
5015
|
+
continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5016
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4648
5017
|
}
|
|
4649
5018
|
},
|
|
4650
5019
|
async (args) => {
|
|
@@ -4868,9 +5237,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4868
5237
|
}
|
|
4869
5238
|
}
|
|
4870
5239
|
}
|
|
4871
|
-
return
|
|
5240
|
+
return text20(parts.join("\n\n"));
|
|
4872
5241
|
} catch (e) {
|
|
4873
|
-
return
|
|
5242
|
+
return fail22(errMsg21(e));
|
|
4874
5243
|
}
|
|
4875
5244
|
}
|
|
4876
5245
|
);
|
|
@@ -4891,10 +5260,10 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4891
5260
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4892
5261
|
|
|
4893
5262
|
// src/tools/packs/renders.ts
|
|
4894
|
-
var
|
|
5263
|
+
var import_zod23 = require("zod");
|
|
4895
5264
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4896
|
-
var
|
|
4897
|
-
var
|
|
5265
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5266
|
+
var fail23 = (message) => ({
|
|
4898
5267
|
isError: true,
|
|
4899
5268
|
content: [{ type: "text", text: message }]
|
|
4900
5269
|
});
|
|
@@ -4904,12 +5273,12 @@ var DEFAULT_IDLE_MS = 3e3;
|
|
|
4904
5273
|
var SETTLE_MS = 400;
|
|
4905
5274
|
var DEFAULT_TOP = 12;
|
|
4906
5275
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4907
|
-
var stepSchema2 =
|
|
4908
|
-
tap:
|
|
4909
|
-
type:
|
|
4910
|
-
into:
|
|
4911
|
-
waitMs:
|
|
4912
|
-
repeat:
|
|
5276
|
+
var stepSchema2 = import_zod23.z.object({
|
|
5277
|
+
tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5278
|
+
type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5279
|
+
into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5280
|
+
waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5281
|
+
repeat: import_zod23.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4913
5282
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4914
5283
|
function selector2(needle) {
|
|
4915
5284
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -5002,14 +5371,14 @@ function registerRenderTools(server, sync) {
|
|
|
5002
5371
|
{
|
|
5003
5372
|
description: "Find out WHAT re-rendered on the current screen and WHY, in one call \u2014 no test route, no scaffolding. THE tool for 'this page is slow/laggy/janky', 'typing lags', 'the whole page re-renders when I tap one button'. Give it `steps` to measure a specific interaction (tap a button, type in a field) \u2014 measuring an idle window instead tells you nothing about what taps cost, which is how re-render fixes usually go wrong. Returns per-component render COST in ms (sorted by it \u2014 a component rendering 40x for 0.4ms is noise, one rendering 4x for 38ms is the answer), the CAUSE of each (parent cascade / own state / changed props), which parent dragged it along, and which props changed. `wasted` counts renders that produced nothing new \u2014 that's the removable part. AFTER YOU EDIT, call it again with the same steps and compareToPrevious:true to prove the fix landed and didn't move the cost somewhere else. (For choosing between several implementations of a screen, use run_benchmark_batch instead \u2014 that ranks variants; this explains one screen.) Requires Buoy Pro and @buoy-gg/highlight-updates.",
|
|
5004
5373
|
inputSchema: {
|
|
5005
|
-
steps:
|
|
5374
|
+
steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
|
|
5006
5375
|
"The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."
|
|
5007
5376
|
),
|
|
5008
|
-
idleMs:
|
|
5009
|
-
label:
|
|
5010
|
-
compareToPrevious:
|
|
5011
|
-
top:
|
|
5012
|
-
deviceId:
|
|
5377
|
+
idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5378
|
+
label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5379
|
+
compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5380
|
+
top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5381
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
5013
5382
|
}
|
|
5014
5383
|
},
|
|
5015
5384
|
async (args) => {
|
|
@@ -5018,18 +5387,18 @@ function registerRenderTools(server, sync) {
|
|
|
5018
5387
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
5019
5388
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
5020
5389
|
if (!uiTool) {
|
|
5021
|
-
return
|
|
5390
|
+
return fail23(
|
|
5022
5391
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
5023
5392
|
);
|
|
5024
5393
|
}
|
|
5025
5394
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
5026
|
-
return
|
|
5395
|
+
return fail23(
|
|
5027
5396
|
"This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route."
|
|
5028
5397
|
);
|
|
5029
5398
|
}
|
|
5030
5399
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
5031
5400
|
if (began && began.ok === false) {
|
|
5032
|
-
return
|
|
5401
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
5033
5402
|
}
|
|
5034
5403
|
await delay5(SETTLE_MS);
|
|
5035
5404
|
const startedAt = Date.now();
|
|
@@ -5074,7 +5443,7 @@ function registerRenderTools(server, sync) {
|
|
|
5074
5443
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5075
5444
|
const summary = ended?.summary;
|
|
5076
5445
|
if (!summary) {
|
|
5077
|
-
return
|
|
5446
|
+
return fail23(
|
|
5078
5447
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5079
5448
|
);
|
|
5080
5449
|
}
|
|
@@ -5099,17 +5468,17 @@ function registerRenderTools(server, sync) {
|
|
|
5099
5468
|
} else if (!args.compareToPrevious) {
|
|
5100
5469
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5101
5470
|
}
|
|
5102
|
-
return
|
|
5471
|
+
return text21(parts.join("\n"));
|
|
5103
5472
|
} catch (e) {
|
|
5104
|
-
return
|
|
5473
|
+
return fail23(errMsg22(e));
|
|
5105
5474
|
}
|
|
5106
5475
|
}
|
|
5107
5476
|
);
|
|
5108
5477
|
}
|
|
5109
5478
|
|
|
5110
5479
|
// src/verify/editWatcher.ts
|
|
5111
|
-
var
|
|
5112
|
-
var
|
|
5480
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
5481
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5113
5482
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5114
5483
|
".ts",
|
|
5115
5484
|
".tsx",
|
|
@@ -5152,14 +5521,14 @@ var EditWatcher = class {
|
|
|
5152
5521
|
if (mode === "never" || this.started) return;
|
|
5153
5522
|
this.started = true;
|
|
5154
5523
|
try {
|
|
5155
|
-
this.watcher =
|
|
5524
|
+
this.watcher = import_node_fs4.default.watch(
|
|
5156
5525
|
rootDir,
|
|
5157
5526
|
{ recursive: true },
|
|
5158
5527
|
(_event, filename) => {
|
|
5159
5528
|
if (!filename) return;
|
|
5160
5529
|
const rel2 = String(filename);
|
|
5161
|
-
if (rel2.split(
|
|
5162
|
-
if (!WATCHED_EXT.has(
|
|
5530
|
+
if (rel2.split(import_node_path4.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5531
|
+
if (!WATCHED_EXT.has(import_node_path4.default.extname(rel2))) return;
|
|
5163
5532
|
this.noteEdit(rel2);
|
|
5164
5533
|
}
|
|
5165
5534
|
);
|
|
@@ -5259,6 +5628,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
5259
5628
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
5260
5629
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
5261
5630
|
|
|
5631
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5632
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5633
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5634
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5635
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5636
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5637
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5638
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5639
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5640
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5641
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5642
|
+
and no Buoy integration in the app.
|
|
5643
|
+
|
|
5262
5644
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
5263
5645
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
5264
5646
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -5375,6 +5757,7 @@ function createMcpServer(sync) {
|
|
|
5375
5757
|
registerTimeMachineTools(server, sync);
|
|
5376
5758
|
registerScenariosTools(server, sync);
|
|
5377
5759
|
registerScreenshotTools(server, sync);
|
|
5760
|
+
registerCameraTools(server);
|
|
5378
5761
|
registerInteractTools(server, sync);
|
|
5379
5762
|
registerJsThreadTools(server, sync);
|
|
5380
5763
|
registerImagesTools(server, sync);
|
|
@@ -5383,16 +5766,16 @@ function createMcpServer(sync) {
|
|
|
5383
5766
|
return server;
|
|
5384
5767
|
}
|
|
5385
5768
|
var deviceIdField = {
|
|
5386
|
-
deviceId:
|
|
5769
|
+
deviceId: import_zod24.z.string().optional().describe(
|
|
5387
5770
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5388
5771
|
)
|
|
5389
5772
|
};
|
|
5390
5773
|
|
|
5391
5774
|
// src/init.ts
|
|
5392
|
-
var
|
|
5393
|
-
var
|
|
5394
|
-
var
|
|
5395
|
-
var
|
|
5775
|
+
var import_node_child_process3 = require("child_process");
|
|
5776
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5777
|
+
var import_node_os4 = __toESM(require("os"));
|
|
5778
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5396
5779
|
function defaultEnv() {
|
|
5397
5780
|
return { BUOY_VERIFY: "auto" };
|
|
5398
5781
|
}
|
|
@@ -5400,19 +5783,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5400
5783
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5401
5784
|
function runInit(opts = {}) {
|
|
5402
5785
|
const cwd = opts.cwd ?? process.cwd();
|
|
5403
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5786
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path5.default.resolve(__dirname, "../skill");
|
|
5404
5787
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5405
5788
|
const entry = resolveEntry(opts, log);
|
|
5406
5789
|
const done = [];
|
|
5407
|
-
upsertMcpConfig(
|
|
5408
|
-
|
|
5409
|
-
upsertMcpConfig(
|
|
5410
|
-
if (
|
|
5411
|
-
upsertMcpConfig(
|
|
5790
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5791
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.join(cwd, ".cursor"), { recursive: true });
|
|
5792
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5793
|
+
if (import_node_fs5.default.existsSync(import_node_path5.default.join(cwd, ".vscode"))) {
|
|
5794
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5412
5795
|
}
|
|
5413
5796
|
installSkills(skillSrcDir, cwd, done);
|
|
5414
|
-
appendDebuggingGuide(
|
|
5415
|
-
appendDebuggingGuide(
|
|
5797
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5798
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
5416
5799
|
log("Buoy MCP installed:");
|
|
5417
5800
|
for (const line of done) log(" \u2022 " + line);
|
|
5418
5801
|
log("");
|
|
@@ -5454,21 +5837,21 @@ function npxEntry() {
|
|
|
5454
5837
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5455
5838
|
}
|
|
5456
5839
|
function localEntry(opts, log) {
|
|
5457
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5840
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path5.default.join(import_node_os4.default.homedir(), ".buoy-mcp");
|
|
5458
5841
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5459
|
-
const
|
|
5460
|
-
if (
|
|
5461
|
-
return { type: "stdio", command: "node", args: [
|
|
5842
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5843
|
+
if (cliPath2) {
|
|
5844
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5462
5845
|
}
|
|
5463
5846
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5464
5847
|
return npxEntry();
|
|
5465
5848
|
}
|
|
5466
5849
|
function installLocalCopy(installDir, registry, log) {
|
|
5467
5850
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
5468
|
-
|
|
5851
|
+
import_node_fs5.default.mkdirSync(installDir, { recursive: true });
|
|
5469
5852
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5470
5853
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
5471
|
-
const res = (0,
|
|
5854
|
+
const res = (0, import_node_child_process3.spawnSync)(
|
|
5472
5855
|
npm,
|
|
5473
5856
|
[
|
|
5474
5857
|
"install",
|
|
@@ -5488,12 +5871,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5488
5871
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5489
5872
|
return null;
|
|
5490
5873
|
}
|
|
5491
|
-
const
|
|
5492
|
-
if (!
|
|
5493
|
-
log(`\u26A0 install finished but ${
|
|
5874
|
+
const cliPath2 = import_node_path5.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5875
|
+
if (!import_node_fs5.default.existsSync(cliPath2)) {
|
|
5876
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5494
5877
|
return null;
|
|
5495
5878
|
}
|
|
5496
|
-
return
|
|
5879
|
+
return cliPath2;
|
|
5497
5880
|
}
|
|
5498
5881
|
function probeRegistry() {
|
|
5499
5882
|
const registry = effectiveRegistry();
|
|
@@ -5511,14 +5894,14 @@ function effectiveRegistry() {
|
|
|
5511
5894
|
}
|
|
5512
5895
|
function npmConfigGet(key) {
|
|
5513
5896
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5514
|
-
const res = (0,
|
|
5897
|
+
const res = (0, import_node_child_process3.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
|
|
5515
5898
|
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
5516
5899
|
const val = res.stdout.trim();
|
|
5517
5900
|
if (!val || val === "undefined" || val === "null") return null;
|
|
5518
5901
|
return val;
|
|
5519
5902
|
}
|
|
5520
5903
|
function hostReachable(registry) {
|
|
5521
|
-
const done = (0,
|
|
5904
|
+
const done = (0, import_node_child_process3.spawnSync)(
|
|
5522
5905
|
process.execPath,
|
|
5523
5906
|
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS3)],
|
|
5524
5907
|
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 + 2e3 }
|
|
@@ -5541,22 +5924,22 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
5541
5924
|
`;
|
|
5542
5925
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
5543
5926
|
let json3 = {};
|
|
5544
|
-
if (
|
|
5927
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5545
5928
|
try {
|
|
5546
|
-
json3 = JSON.parse(
|
|
5929
|
+
json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
|
|
5547
5930
|
} catch {
|
|
5548
|
-
|
|
5931
|
+
import_node_fs5.default.copyFileSync(file, file + ".bak");
|
|
5549
5932
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
5550
5933
|
json3 = {};
|
|
5551
5934
|
}
|
|
5552
5935
|
} else {
|
|
5553
|
-
|
|
5936
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
|
|
5554
5937
|
}
|
|
5555
5938
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5556
5939
|
const existed = "buoy" in servers;
|
|
5557
5940
|
servers.buoy = entry;
|
|
5558
5941
|
json3[key] = servers;
|
|
5559
|
-
|
|
5942
|
+
import_node_fs5.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
5560
5943
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
5561
5944
|
}
|
|
5562
5945
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
@@ -5616,9 +5999,9 @@ mentions it.
|
|
|
5616
5999
|
`;
|
|
5617
6000
|
function appendDebuggingGuide(file, cwd, done) {
|
|
5618
6001
|
let existing = "";
|
|
5619
|
-
if (
|
|
6002
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5620
6003
|
try {
|
|
5621
|
-
existing =
|
|
6004
|
+
existing = import_node_fs5.default.readFileSync(file, "utf8");
|
|
5622
6005
|
} catch {
|
|
5623
6006
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5624
6007
|
return;
|
|
@@ -5630,40 +6013,40 @@ function appendDebuggingGuide(file, cwd, done) {
|
|
|
5630
6013
|
}
|
|
5631
6014
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
5632
6015
|
try {
|
|
5633
|
-
|
|
6016
|
+
import_node_fs5.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
5634
6017
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
5635
6018
|
} catch {
|
|
5636
6019
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5637
6020
|
}
|
|
5638
6021
|
}
|
|
5639
6022
|
function installSkills(skillSrcDir, cwd, done) {
|
|
5640
|
-
if (!
|
|
6023
|
+
if (!import_node_fs5.default.existsSync(skillSrcDir)) {
|
|
5641
6024
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5642
6025
|
return;
|
|
5643
6026
|
}
|
|
5644
|
-
const skills =
|
|
6027
|
+
const skills = import_node_fs5.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs5.default.existsSync(import_node_path5.default.join(skillSrcDir, e.name, "SKILL.md")));
|
|
5645
6028
|
if (skills.length === 0) {
|
|
5646
6029
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5647
6030
|
return;
|
|
5648
6031
|
}
|
|
5649
6032
|
for (const skill of skills) {
|
|
5650
|
-
const from =
|
|
5651
|
-
const to =
|
|
6033
|
+
const from = import_node_path5.default.join(skillSrcDir, skill.name);
|
|
6034
|
+
const to = import_node_path5.default.join(cwd, ".claude", "skills", skill.name);
|
|
5652
6035
|
copyDir(from, to);
|
|
5653
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6036
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path5.default.join(to, "SKILL.md"), cwd)}`);
|
|
5654
6037
|
}
|
|
5655
6038
|
}
|
|
5656
6039
|
function copyDir(from, to) {
|
|
5657
|
-
|
|
5658
|
-
for (const entry of
|
|
5659
|
-
const s =
|
|
5660
|
-
const d =
|
|
6040
|
+
import_node_fs5.default.mkdirSync(to, { recursive: true });
|
|
6041
|
+
for (const entry of import_node_fs5.default.readdirSync(from, { withFileTypes: true })) {
|
|
6042
|
+
const s = import_node_path5.default.join(from, entry.name);
|
|
6043
|
+
const d = import_node_path5.default.join(to, entry.name);
|
|
5661
6044
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5662
|
-
else
|
|
6045
|
+
else import_node_fs5.default.copyFileSync(s, d);
|
|
5663
6046
|
}
|
|
5664
6047
|
}
|
|
5665
6048
|
function rel(file, cwd) {
|
|
5666
|
-
const r =
|
|
6049
|
+
const r = import_node_path5.default.relative(cwd, file);
|
|
5667
6050
|
return r && !r.startsWith("..") ? r : file;
|
|
5668
6051
|
}
|
|
5669
6052
|
// Annotate the CommonJS export names for ESM import in node:
|