@buoy-gg/mcp 7.0.31 → 7.0.33
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-6HRZM4CM.mjs} +581 -193
- package/dist/cli.js +600 -212
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +600 -212
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -287,6 +287,7 @@ var SyncClient = class {
|
|
|
287
287
|
isConnected: d.isConnected !== false,
|
|
288
288
|
isPro: d.isPro === true,
|
|
289
289
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
290
|
+
collision: d.collision === true,
|
|
290
291
|
tools: caps?.tools ?? []
|
|
291
292
|
};
|
|
292
293
|
});
|
|
@@ -675,7 +676,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
675
676
|
|
|
676
677
|
// src/server.ts
|
|
677
678
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
678
|
-
var
|
|
679
|
+
var import_zod24 = require("zod");
|
|
679
680
|
|
|
680
681
|
// src/tools/generic.ts
|
|
681
682
|
var import_zod = require("zod");
|
|
@@ -686,7 +687,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
686
687
|
var import_node_path = __toESM(require("path"));
|
|
687
688
|
|
|
688
689
|
// src/version.ts
|
|
689
|
-
var VERSION = "7.0.
|
|
690
|
+
var VERSION = "7.0.33" ? "7.0.33" : "0.0.0-dev";
|
|
690
691
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
691
692
|
|
|
692
693
|
// src/update/checkForUpdate.ts
|
|
@@ -780,8 +781,12 @@ function registerGenericTools(server, sync) {
|
|
|
780
781
|
const connected = devices.filter((d) => d.isConnected);
|
|
781
782
|
const anyPro = connected.some((d) => d.isPro);
|
|
782
783
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
784
|
+
const collided = connected.filter((d) => d.collision);
|
|
783
785
|
return json({
|
|
784
786
|
devices,
|
|
787
|
+
...collided.length > 0 ? {
|
|
788
|
+
warning: `${collided.map((d) => `"${d.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`
|
|
789
|
+
} : {},
|
|
785
790
|
...anyPro ? {} : {
|
|
786
791
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
787
792
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -1419,7 +1424,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1419
1424
|
let sawActive = false;
|
|
1420
1425
|
let idleStreak = 0;
|
|
1421
1426
|
let lastCount = -1;
|
|
1422
|
-
let
|
|
1427
|
+
let resolved2 = null;
|
|
1423
1428
|
while (Date.now() - start < cap3) {
|
|
1424
1429
|
let snap2;
|
|
1425
1430
|
try {
|
|
@@ -1431,7 +1436,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1431
1436
|
const completed = snap2.live?.automationCompleted;
|
|
1432
1437
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1433
1438
|
if (completed?.phase === "done") {
|
|
1434
|
-
|
|
1439
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1435
1440
|
break;
|
|
1436
1441
|
}
|
|
1437
1442
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1441,11 +1446,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1441
1446
|
const count = runCountFor(snap2.index, newest);
|
|
1442
1447
|
if (count === lastCount) {
|
|
1443
1448
|
if (sawActive && ++idleStreak >= 3) {
|
|
1444
|
-
|
|
1449
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1445
1450
|
break;
|
|
1446
1451
|
}
|
|
1447
1452
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1448
|
-
|
|
1453
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1449
1454
|
break;
|
|
1450
1455
|
}
|
|
1451
1456
|
} else {
|
|
@@ -1457,11 +1462,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1457
1462
|
}
|
|
1458
1463
|
await delay2(POLL_INTERVAL_MS);
|
|
1459
1464
|
}
|
|
1460
|
-
if (!
|
|
1465
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1461
1466
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1462
1467
|
});
|
|
1463
1468
|
const snap = await readPerfSnapshot(sync, target);
|
|
1464
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1469
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1465
1470
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1466
1471
|
});
|
|
1467
1472
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -1788,8 +1793,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
1788
1793
|
}
|
|
1789
1794
|
function bodyLines(e, fetched) {
|
|
1790
1795
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
1791
|
-
const label = (inline,
|
|
1792
|
-
const value = inline ??
|
|
1796
|
+
const label = (inline, resolved2, omitted) => {
|
|
1797
|
+
const value = inline ?? resolved2;
|
|
1793
1798
|
if (value !== void 0 && value !== null) return truncate(value);
|
|
1794
1799
|
if (!omitted) return "(none)";
|
|
1795
1800
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -3370,9 +3375,9 @@ function reasonMessage(res, query) {
|
|
|
3370
3375
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
3371
3376
|
}
|
|
3372
3377
|
}
|
|
3373
|
-
function candidatesText(
|
|
3374
|
-
if (!
|
|
3375
|
-
const lines =
|
|
3378
|
+
function candidatesText(candidates2) {
|
|
3379
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
3380
|
+
const lines = candidates2.map((c) => {
|
|
3376
3381
|
const id = c.testID || c.nativeID;
|
|
3377
3382
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
3378
3383
|
});
|
|
@@ -3471,14 +3476,383 @@ function registerScreenshotTools(server, sync) {
|
|
|
3471
3476
|
);
|
|
3472
3477
|
}
|
|
3473
3478
|
|
|
3474
|
-
// src/tools/packs/
|
|
3479
|
+
// src/tools/packs/camera.ts
|
|
3475
3480
|
var import_zod15 = require("zod");
|
|
3476
|
-
|
|
3481
|
+
|
|
3482
|
+
// src/capture/camera.ts
|
|
3483
|
+
var import_node_child_process2 = require("child_process");
|
|
3484
|
+
var import_node_util2 = require("util");
|
|
3485
|
+
var import_node_fs3 = require("fs");
|
|
3486
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3487
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3488
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3489
|
+
function candidates() {
|
|
3490
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3491
|
+
const list = [
|
|
3492
|
+
...fromEnv ? [import_node_path3.default.join(fromEnv, "buoycam")] : [],
|
|
3493
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3494
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3495
|
+
];
|
|
3496
|
+
let dir = __dirname;
|
|
3497
|
+
for (let i = 0; i < 8; i++) {
|
|
3498
|
+
list.push(import_node_path3.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3499
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
3500
|
+
if (parent === dir) break;
|
|
3501
|
+
dir = parent;
|
|
3502
|
+
}
|
|
3503
|
+
return list;
|
|
3504
|
+
}
|
|
3505
|
+
var resolved = null;
|
|
3506
|
+
function cliPath() {
|
|
3507
|
+
if (resolved) return resolved;
|
|
3508
|
+
resolved = candidates().find((c) => (0, import_node_fs3.existsSync)(c)) ?? null;
|
|
3509
|
+
return resolved;
|
|
3510
|
+
}
|
|
3511
|
+
async function run(args) {
|
|
3512
|
+
const cli = cliPath();
|
|
3513
|
+
if (!cli) {
|
|
3514
|
+
return {
|
|
3515
|
+
ok: false,
|
|
3516
|
+
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`."
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
try {
|
|
3520
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3521
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
const stdout = err.stdout ?? "";
|
|
3524
|
+
try {
|
|
3525
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3526
|
+
} catch {
|
|
3527
|
+
return {
|
|
3528
|
+
ok: false,
|
|
3529
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
function describeDiagnosis(checks) {
|
|
3535
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3536
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3537
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3538
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3539
|
+
}
|
|
3540
|
+
const lines = [];
|
|
3541
|
+
for (const c of [...failed, ...warned]) {
|
|
3542
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3543
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3544
|
+
}
|
|
3545
|
+
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.`;
|
|
3546
|
+
return [headline, "", ...lines].join("\n");
|
|
3547
|
+
}
|
|
3548
|
+
function describeDevices(devices, apps) {
|
|
3549
|
+
if (devices.length === 0) {
|
|
3550
|
+
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._";
|
|
3551
|
+
}
|
|
3552
|
+
const lines = [
|
|
3553
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3554
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3555
|
+
];
|
|
3556
|
+
if (devices.length > 1 && apps === null) {
|
|
3557
|
+
lines.push(
|
|
3558
|
+
"",
|
|
3559
|
+
"_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._"
|
|
3560
|
+
);
|
|
3561
|
+
return lines.join("\n");
|
|
3562
|
+
}
|
|
3563
|
+
if (apps === null) {
|
|
3564
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3565
|
+
return lines.join("\n");
|
|
3566
|
+
}
|
|
3567
|
+
if (apps.length === 0) {
|
|
3568
|
+
lines.push("", "No apps installed on it.");
|
|
3569
|
+
return lines.join("\n");
|
|
3570
|
+
}
|
|
3571
|
+
lines.push(
|
|
3572
|
+
"",
|
|
3573
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3574
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3575
|
+
"",
|
|
3576
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3577
|
+
);
|
|
3578
|
+
return lines.join("\n");
|
|
3579
|
+
}
|
|
3580
|
+
function describeInputs(cameras, windows) {
|
|
3581
|
+
const lines = [];
|
|
3582
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3583
|
+
if (cameras.length) {
|
|
3584
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3585
|
+
} else {
|
|
3586
|
+
lines.push(
|
|
3587
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
lines.push("");
|
|
3591
|
+
lines.push(
|
|
3592
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3593
|
+
);
|
|
3594
|
+
if (windows.length) {
|
|
3595
|
+
lines.push(
|
|
3596
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3597
|
+
);
|
|
3598
|
+
} else {
|
|
3599
|
+
lines.push(
|
|
3600
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3601
|
+
);
|
|
3602
|
+
}
|
|
3603
|
+
return lines.join("\n");
|
|
3604
|
+
}
|
|
3605
|
+
function entitlementPaths() {
|
|
3606
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3607
|
+
if (fromEnv) return [fromEnv];
|
|
3608
|
+
return [
|
|
3609
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3610
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3611
|
+
];
|
|
3612
|
+
}
|
|
3613
|
+
function readEntitlement() {
|
|
3614
|
+
for (const p of entitlementPaths()) {
|
|
3615
|
+
try {
|
|
3616
|
+
if (!(0, import_node_fs3.existsSync)(p)) continue;
|
|
3617
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf8"));
|
|
3618
|
+
return {
|
|
3619
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3620
|
+
isPro: parsed.isPro === true
|
|
3621
|
+
};
|
|
3622
|
+
} catch {
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return { tier: "anonymous", isPro: false };
|
|
3626
|
+
}
|
|
3627
|
+
function proRequired(tool) {
|
|
3628
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3629
|
+
|
|
3630
|
+
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.
|
|
3631
|
+
|
|
3632
|
+
_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._`;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
// src/tools/packs/camera.ts
|
|
3477
3636
|
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3637
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3478
3638
|
var fail15 = (message) => ({
|
|
3479
3639
|
isError: true,
|
|
3480
3640
|
content: [{ type: "text", text: message }]
|
|
3481
3641
|
});
|
|
3642
|
+
function registerCameraTools(server) {
|
|
3643
|
+
server.registerTool(
|
|
3644
|
+
"camera_source",
|
|
3645
|
+
{
|
|
3646
|
+
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.",
|
|
3647
|
+
inputSchema: {
|
|
3648
|
+
kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3649
|
+
value: import_zod15.z.string().optional().describe(
|
|
3650
|
+
"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`."
|
|
3651
|
+
),
|
|
3652
|
+
format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3653
|
+
}
|
|
3654
|
+
},
|
|
3655
|
+
async (args) => {
|
|
3656
|
+
const blocked = gate("camera_source");
|
|
3657
|
+
if (blocked) return blocked;
|
|
3658
|
+
const cmd = ["source", args.kind];
|
|
3659
|
+
if (args.value) cmd.push(args.value);
|
|
3660
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3661
|
+
const result = await run(cmd);
|
|
3662
|
+
if (!result.ok) {
|
|
3663
|
+
return fail15(
|
|
3664
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
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;
|
|
3668
|
+
return text13(
|
|
3669
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3670
|
+
|
|
3671
|
+
_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._`
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
);
|
|
3675
|
+
server.registerTool(
|
|
3676
|
+
"camera_status",
|
|
3677
|
+
{
|
|
3678
|
+
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.",
|
|
3679
|
+
inputSchema: {}
|
|
3680
|
+
},
|
|
3681
|
+
async () => {
|
|
3682
|
+
const blocked = gate("camera_status");
|
|
3683
|
+
if (blocked) return blocked;
|
|
3684
|
+
const result = await run(["status"]);
|
|
3685
|
+
if (!result.ok || !result.data) {
|
|
3686
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3687
|
+
}
|
|
3688
|
+
const { publishing, fps, codes } = result.data;
|
|
3689
|
+
if (!publishing) {
|
|
3690
|
+
return text13(
|
|
3691
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
return text13(
|
|
3695
|
+
[
|
|
3696
|
+
`Publishing at ${fps} fps.`,
|
|
3697
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3698
|
+
"",
|
|
3699
|
+
"_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._"
|
|
3700
|
+
].join("\n")
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3703
|
+
);
|
|
3704
|
+
server.registerTool(
|
|
3705
|
+
"camera_launch",
|
|
3706
|
+
{
|
|
3707
|
+
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.",
|
|
3708
|
+
inputSchema: {
|
|
3709
|
+
bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
|
|
3710
|
+
udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3711
|
+
}
|
|
3712
|
+
},
|
|
3713
|
+
async (args) => {
|
|
3714
|
+
const blocked = gate("camera_launch");
|
|
3715
|
+
if (blocked) return blocked;
|
|
3716
|
+
const cmd = ["launch", args.bundleId];
|
|
3717
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3718
|
+
const result = await run(cmd);
|
|
3719
|
+
if (!result.ok) {
|
|
3720
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3721
|
+
}
|
|
3722
|
+
return text13(
|
|
3723
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3724
|
+
);
|
|
3725
|
+
}
|
|
3726
|
+
);
|
|
3727
|
+
server.registerTool(
|
|
3728
|
+
"camera_diagnose",
|
|
3729
|
+
{
|
|
3730
|
+
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.",
|
|
3731
|
+
inputSchema: {
|
|
3732
|
+
bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3733
|
+
}
|
|
3734
|
+
},
|
|
3735
|
+
async (args) => {
|
|
3736
|
+
const cmd = ["diagnose"];
|
|
3737
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3738
|
+
const result = await run(cmd);
|
|
3739
|
+
if (!result.data?.checks) {
|
|
3740
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3741
|
+
}
|
|
3742
|
+
const ent = readEntitlement();
|
|
3743
|
+
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._";
|
|
3744
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3745
|
+
}
|
|
3746
|
+
);
|
|
3747
|
+
server.registerTool(
|
|
3748
|
+
"camera_stop",
|
|
3749
|
+
{
|
|
3750
|
+
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.",
|
|
3751
|
+
inputSchema: {}
|
|
3752
|
+
},
|
|
3753
|
+
async () => {
|
|
3754
|
+
const blocked = gate("camera_stop");
|
|
3755
|
+
if (blocked) return blocked;
|
|
3756
|
+
const result = await run(["stop"]);
|
|
3757
|
+
if (!result.ok) {
|
|
3758
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3759
|
+
}
|
|
3760
|
+
return text13("The simulator camera is off.");
|
|
3761
|
+
}
|
|
3762
|
+
);
|
|
3763
|
+
server.registerTool(
|
|
3764
|
+
"camera_devices",
|
|
3765
|
+
{
|
|
3766
|
+
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.",
|
|
3767
|
+
inputSchema: {
|
|
3768
|
+
udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
async (args) => {
|
|
3772
|
+
const blocked = gate("camera_devices");
|
|
3773
|
+
if (blocked) return blocked;
|
|
3774
|
+
const devicesResult = await run(["devices"]);
|
|
3775
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3776
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3777
|
+
}
|
|
3778
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3779
|
+
let apps = null;
|
|
3780
|
+
if (devices.length === 1 || args.udid) {
|
|
3781
|
+
const appsResult = await run([
|
|
3782
|
+
"apps",
|
|
3783
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3784
|
+
]);
|
|
3785
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3786
|
+
}
|
|
3787
|
+
return text13(describeDevices(devices, apps));
|
|
3788
|
+
}
|
|
3789
|
+
);
|
|
3790
|
+
server.registerTool(
|
|
3791
|
+
"camera_inputs",
|
|
3792
|
+
{
|
|
3793
|
+
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.",
|
|
3794
|
+
inputSchema: {}
|
|
3795
|
+
},
|
|
3796
|
+
async () => {
|
|
3797
|
+
const blocked = gate("camera_inputs");
|
|
3798
|
+
if (blocked) return blocked;
|
|
3799
|
+
const [cams, wins] = await Promise.all([
|
|
3800
|
+
run(["cameras"]),
|
|
3801
|
+
run(["windows"])
|
|
3802
|
+
]);
|
|
3803
|
+
if (!cams.ok && !wins.ok) {
|
|
3804
|
+
return fail15(
|
|
3805
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
return text13(
|
|
3809
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
);
|
|
3813
|
+
server.registerTool(
|
|
3814
|
+
"camera_zero_setup",
|
|
3815
|
+
{
|
|
3816
|
+
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.",
|
|
3817
|
+
inputSchema: {
|
|
3818
|
+
action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3819
|
+
udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3820
|
+
}
|
|
3821
|
+
},
|
|
3822
|
+
async (args) => {
|
|
3823
|
+
const blocked = gate("camera_zero_setup");
|
|
3824
|
+
if (blocked) return blocked;
|
|
3825
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3826
|
+
if (!result.ok) {
|
|
3827
|
+
return fail15(
|
|
3828
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
const d = result.data ?? {};
|
|
3832
|
+
if (args.action === "status") {
|
|
3833
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3834
|
+
return text13(
|
|
3835
|
+
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."
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
if (args.action === "on") {
|
|
3839
|
+
return text13(
|
|
3840
|
+
"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._"
|
|
3841
|
+
);
|
|
3842
|
+
}
|
|
3843
|
+
return text13("Zero-setup mode is **off**.");
|
|
3844
|
+
}
|
|
3845
|
+
);
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// src/tools/packs/interact.ts
|
|
3849
|
+
var import_zod16 = require("zod");
|
|
3850
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3851
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3852
|
+
var fail16 = (message) => ({
|
|
3853
|
+
isError: true,
|
|
3854
|
+
content: [{ type: "text", text: message }]
|
|
3855
|
+
});
|
|
3482
3856
|
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3483
3857
|
function formatElement(e) {
|
|
3484
3858
|
let line = e.interactive ? "\u2022 " : " ";
|
|
@@ -3502,8 +3876,8 @@ function registerInteractTools(server, sync) {
|
|
|
3502
3876
|
{
|
|
3503
3877
|
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
3878
|
inputSchema: {
|
|
3505
|
-
interactiveOnly:
|
|
3506
|
-
deviceId:
|
|
3879
|
+
interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3880
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3507
3881
|
}
|
|
3508
3882
|
},
|
|
3509
3883
|
async (args) => {
|
|
@@ -3513,16 +3887,16 @@ function registerInteractTools(server, sync) {
|
|
|
3513
3887
|
const all = res?.elements ?? [];
|
|
3514
3888
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3515
3889
|
if (elements.length === 0) {
|
|
3516
|
-
return
|
|
3890
|
+
return text14(
|
|
3517
3891
|
"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
3892
|
);
|
|
3519
3893
|
}
|
|
3520
3894
|
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
|
|
3895
|
+
return text14(`${header}
|
|
3522
3896
|
|
|
3523
3897
|
${elements.map(formatElement).join("\n")}`);
|
|
3524
3898
|
} catch (e) {
|
|
3525
|
-
return
|
|
3899
|
+
return fail16(errMsg15(e));
|
|
3526
3900
|
}
|
|
3527
3901
|
}
|
|
3528
3902
|
);
|
|
@@ -3531,20 +3905,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3531
3905
|
{
|
|
3532
3906
|
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
3907
|
inputSchema: {
|
|
3534
|
-
nativeTag:
|
|
3535
|
-
testID:
|
|
3536
|
-
query:
|
|
3537
|
-
value:
|
|
3538
|
-
text:
|
|
3539
|
-
longPress:
|
|
3540
|
-
scrollIntoView:
|
|
3541
|
-
deviceId:
|
|
3908
|
+
nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3909
|
+
testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
|
|
3910
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3911
|
+
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)."),
|
|
3912
|
+
text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3913
|
+
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."),
|
|
3914
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3915
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3542
3916
|
}
|
|
3543
3917
|
},
|
|
3544
3918
|
async (args) => {
|
|
3545
3919
|
try {
|
|
3546
3920
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3547
|
-
return
|
|
3921
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3548
3922
|
}
|
|
3549
3923
|
const target = sync.requireProDevice(args.deviceId);
|
|
3550
3924
|
const res = await sync.callAction(
|
|
@@ -3569,25 +3943,25 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3569
3943
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3570
3944
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3571
3945
|
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
|
|
3946
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3573
3947
|
}
|
|
3574
3948
|
let msg = res?.reason ?? "Tap failed.";
|
|
3575
3949
|
if (res?.candidates?.length) {
|
|
3576
3950
|
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
3951
|
}
|
|
3578
|
-
return
|
|
3952
|
+
return fail16(msg);
|
|
3579
3953
|
} catch (e) {
|
|
3580
|
-
return
|
|
3954
|
+
return fail16(errMsg15(e));
|
|
3581
3955
|
}
|
|
3582
3956
|
}
|
|
3583
3957
|
);
|
|
3584
3958
|
}
|
|
3585
3959
|
|
|
3586
3960
|
// src/tools/packs/jsThread.ts
|
|
3587
|
-
var
|
|
3961
|
+
var import_zod17 = require("zod");
|
|
3588
3962
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3589
|
-
var
|
|
3590
|
-
var
|
|
3963
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3964
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3591
3965
|
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3592
3966
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3593
3967
|
function sparkline(values) {
|
|
@@ -3659,9 +4033,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3659
4033
|
{
|
|
3660
4034
|
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
4035
|
inputSchema: {
|
|
3662
|
-
deviceId:
|
|
3663
|
-
durationMs:
|
|
3664
|
-
clearFirst:
|
|
4036
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4037
|
+
durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4038
|
+
clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3665
4039
|
}
|
|
3666
4040
|
},
|
|
3667
4041
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3674,7 +4048,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3674
4048
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3675
4049
|
target
|
|
3676
4050
|
);
|
|
3677
|
-
if (!snap) return
|
|
4051
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3678
4052
|
const md = formatTopMarkdown(snap, duration);
|
|
3679
4053
|
const slim = {
|
|
3680
4054
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3693,13 +4067,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3693
4067
|
worstFreezeMs: r.worstFreezeMs
|
|
3694
4068
|
}))
|
|
3695
4069
|
};
|
|
3696
|
-
return
|
|
4070
|
+
return text15(`${md}
|
|
3697
4071
|
|
|
3698
4072
|
\`\`\`json
|
|
3699
4073
|
${JSON.stringify(slim)}
|
|
3700
4074
|
\`\`\``);
|
|
3701
4075
|
} catch (e) {
|
|
3702
|
-
return
|
|
4076
|
+
return fail17(errMsg16(e));
|
|
3703
4077
|
}
|
|
3704
4078
|
}
|
|
3705
4079
|
);
|
|
@@ -3708,8 +4082,8 @@ ${JSON.stringify(slim)}
|
|
|
3708
4082
|
{
|
|
3709
4083
|
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
4084
|
inputSchema: {
|
|
3711
|
-
deviceId:
|
|
3712
|
-
key:
|
|
4085
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4086
|
+
key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3713
4087
|
}
|
|
3714
4088
|
},
|
|
3715
4089
|
async ({ deviceId, key }) => {
|
|
@@ -3722,18 +4096,18 @@ ${JSON.stringify(slim)}
|
|
|
3722
4096
|
target
|
|
3723
4097
|
);
|
|
3724
4098
|
if (!detail) {
|
|
3725
|
-
return
|
|
4099
|
+
return fail17(
|
|
3726
4100
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3727
4101
|
);
|
|
3728
4102
|
}
|
|
3729
4103
|
const md = formatDetailMarkdown(detail);
|
|
3730
|
-
return
|
|
4104
|
+
return text15(`${md}
|
|
3731
4105
|
|
|
3732
4106
|
\`\`\`json
|
|
3733
4107
|
${JSON.stringify(detail)}
|
|
3734
4108
|
\`\`\``);
|
|
3735
4109
|
} catch (e) {
|
|
3736
|
-
return
|
|
4110
|
+
return fail17(errMsg16(e));
|
|
3737
4111
|
}
|
|
3738
4112
|
}
|
|
3739
4113
|
);
|
|
@@ -3742,26 +4116,26 @@ ${JSON.stringify(detail)}
|
|
|
3742
4116
|
{
|
|
3743
4117
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3744
4118
|
inputSchema: {
|
|
3745
|
-
deviceId:
|
|
4119
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3746
4120
|
}
|
|
3747
4121
|
},
|
|
3748
4122
|
async ({ deviceId }) => {
|
|
3749
4123
|
try {
|
|
3750
4124
|
const target = sync.requireProDevice(deviceId);
|
|
3751
4125
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3752
|
-
return
|
|
4126
|
+
return text15("Cleared js-top stats.");
|
|
3753
4127
|
} catch (e) {
|
|
3754
|
-
return
|
|
4128
|
+
return fail17(errMsg16(e));
|
|
3755
4129
|
}
|
|
3756
4130
|
}
|
|
3757
4131
|
);
|
|
3758
4132
|
}
|
|
3759
4133
|
|
|
3760
4134
|
// src/tools/packs/images.ts
|
|
3761
|
-
var
|
|
4135
|
+
var import_zod18 = require("zod");
|
|
3762
4136
|
var IMAGES_TOOL_ID = "images";
|
|
3763
|
-
var
|
|
3764
|
-
var
|
|
4137
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4138
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3765
4139
|
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3766
4140
|
function uriTail(uri) {
|
|
3767
4141
|
if (!uri) return "(no source)";
|
|
@@ -3850,9 +4224,9 @@ function registerImagesTools(server, sync) {
|
|
|
3850
4224
|
{
|
|
3851
4225
|
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
4226
|
inputSchema: {
|
|
3853
|
-
deviceId:
|
|
3854
|
-
limit:
|
|
3855
|
-
status:
|
|
4227
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4228
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4229
|
+
status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3856
4230
|
}
|
|
3857
4231
|
},
|
|
3858
4232
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3865,7 +4239,7 @@ function registerImagesTools(server, sync) {
|
|
|
3865
4239
|
{ limit: capped, status },
|
|
3866
4240
|
target
|
|
3867
4241
|
);
|
|
3868
|
-
if (!list) return
|
|
4242
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3869
4243
|
const md = formatListMarkdown(list, capped);
|
|
3870
4244
|
const slim = {
|
|
3871
4245
|
stats: list.stats,
|
|
@@ -3892,13 +4266,13 @@ function registerImagesTools(server, sync) {
|
|
|
3892
4266
|
layoutShifts: r.layoutShifts
|
|
3893
4267
|
}))
|
|
3894
4268
|
};
|
|
3895
|
-
return
|
|
4269
|
+
return text16(`${md}
|
|
3896
4270
|
|
|
3897
4271
|
\`\`\`json
|
|
3898
4272
|
${JSON.stringify(slim)}
|
|
3899
4273
|
\`\`\``);
|
|
3900
4274
|
} catch (e) {
|
|
3901
|
-
return
|
|
4275
|
+
return fail18(errMsg17(e));
|
|
3902
4276
|
}
|
|
3903
4277
|
}
|
|
3904
4278
|
);
|
|
@@ -3907,9 +4281,9 @@ ${JSON.stringify(slim)}
|
|
|
3907
4281
|
{
|
|
3908
4282
|
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
4283
|
inputSchema: {
|
|
3910
|
-
deviceId:
|
|
3911
|
-
id:
|
|
3912
|
-
action:
|
|
4284
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4285
|
+
id: import_zod18.z.number().describe("Record id from get_images."),
|
|
4286
|
+
action: import_zod18.z.enum([
|
|
3913
4287
|
"hard_reload",
|
|
3914
4288
|
"retry",
|
|
3915
4289
|
"evict_disk",
|
|
@@ -3921,7 +4295,7 @@ ${JSON.stringify(slim)}
|
|
|
3921
4295
|
"swap_url",
|
|
3922
4296
|
"restore"
|
|
3923
4297
|
]).describe("What to do."),
|
|
3924
|
-
url:
|
|
4298
|
+
url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3925
4299
|
}
|
|
3926
4300
|
},
|
|
3927
4301
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3929,8 +4303,8 @@ ${JSON.stringify(slim)}
|
|
|
3929
4303
|
const target = sync.requireProDevice(deviceId);
|
|
3930
4304
|
if (action === "prove_savings") {
|
|
3931
4305
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3932
|
-
if (!savings) return
|
|
3933
|
-
if (!savings.ok) return
|
|
4306
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4307
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3934
4308
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3935
4309
|
if (savings.originalBytes != null)
|
|
3936
4310
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3942,11 +4316,11 @@ ${JSON.stringify(slim)}
|
|
|
3942
4316
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3943
4317
|
if (savings.optimizedUri)
|
|
3944
4318
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3945
|
-
return
|
|
4319
|
+
return text16(parts.join("\n"));
|
|
3946
4320
|
}
|
|
3947
4321
|
const entry = ACTION_MAP[action];
|
|
3948
|
-
if (!entry) return
|
|
3949
|
-
if (action === "swap_url" && !url) return
|
|
4322
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4323
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3950
4324
|
const result = await sync.callAction(
|
|
3951
4325
|
IMAGES_TOOL_ID,
|
|
3952
4326
|
entry.action,
|
|
@@ -3966,9 +4340,9 @@ ${JSON.stringify(slim)}
|
|
|
3966
4340
|
);
|
|
3967
4341
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3968
4342
|
}
|
|
3969
|
-
return
|
|
4343
|
+
return text16(lines.join("\n"));
|
|
3970
4344
|
} catch (e) {
|
|
3971
|
-
return
|
|
4345
|
+
return fail18(errMsg17(e));
|
|
3972
4346
|
}
|
|
3973
4347
|
}
|
|
3974
4348
|
);
|
|
@@ -3977,12 +4351,12 @@ ${JSON.stringify(slim)}
|
|
|
3977
4351
|
{
|
|
3978
4352
|
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
4353
|
inputSchema: {
|
|
3980
|
-
deviceId:
|
|
3981
|
-
network:
|
|
3982
|
-
blankImages:
|
|
3983
|
-
massAction:
|
|
3984
|
-
clearRegistry:
|
|
3985
|
-
clearExpoCaches:
|
|
4354
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4355
|
+
network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4356
|
+
blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
|
|
4357
|
+
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."),
|
|
4358
|
+
clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
|
|
4359
|
+
clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3986
4360
|
}
|
|
3987
4361
|
},
|
|
3988
4362
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -4009,17 +4383,17 @@ ${JSON.stringify(slim)}
|
|
|
4009
4383
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
4010
4384
|
done.push(r?.message ?? "expo caches cleared");
|
|
4011
4385
|
}
|
|
4012
|
-
if (done.length === 0) return
|
|
4013
|
-
return
|
|
4386
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4387
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
4014
4388
|
} catch (e) {
|
|
4015
|
-
return
|
|
4389
|
+
return fail18(errMsg17(e));
|
|
4016
4390
|
}
|
|
4017
4391
|
}
|
|
4018
4392
|
);
|
|
4019
4393
|
}
|
|
4020
4394
|
|
|
4021
4395
|
// src/tools/packs/assets.ts
|
|
4022
|
-
var
|
|
4396
|
+
var import_zod19 = require("zod");
|
|
4023
4397
|
|
|
4024
4398
|
// src/tools/packs/assetsGraphNote.ts
|
|
4025
4399
|
function graphUnavailableNote(ss) {
|
|
@@ -4034,8 +4408,8 @@ function graphUnavailableNote(ss) {
|
|
|
4034
4408
|
|
|
4035
4409
|
// src/tools/packs/assets.ts
|
|
4036
4410
|
var ASSETS_TOOL_ID = "assets";
|
|
4037
|
-
var
|
|
4038
|
-
var
|
|
4411
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4412
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4039
4413
|
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
4040
4414
|
function kb2(bytes) {
|
|
4041
4415
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
@@ -4124,10 +4498,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4124
4498
|
{
|
|
4125
4499
|
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
4500
|
inputSchema: {
|
|
4127
|
-
deviceId:
|
|
4128
|
-
limit:
|
|
4129
|
-
kind:
|
|
4130
|
-
unusedOnly:
|
|
4501
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4502
|
+
limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4503
|
+
kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4504
|
+
unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4131
4505
|
}
|
|
4132
4506
|
},
|
|
4133
4507
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4140,7 +4514,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4140
4514
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4141
4515
|
target
|
|
4142
4516
|
);
|
|
4143
|
-
if (!list) return
|
|
4517
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4144
4518
|
if (unusedOnly) {
|
|
4145
4519
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4146
4520
|
}
|
|
@@ -4164,13 +4538,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4164
4538
|
sizeSource: r.sizeSource
|
|
4165
4539
|
}))
|
|
4166
4540
|
};
|
|
4167
|
-
return
|
|
4541
|
+
return text17(`${md}
|
|
4168
4542
|
|
|
4169
4543
|
\`\`\`json
|
|
4170
4544
|
${JSON.stringify(slim)}
|
|
4171
4545
|
\`\`\``);
|
|
4172
4546
|
} catch (e) {
|
|
4173
|
-
return
|
|
4547
|
+
return fail19(errMsg18(e));
|
|
4174
4548
|
}
|
|
4175
4549
|
}
|
|
4176
4550
|
);
|
|
@@ -4179,9 +4553,9 @@ ${JSON.stringify(slim)}
|
|
|
4179
4553
|
{
|
|
4180
4554
|
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
4555
|
inputSchema: {
|
|
4182
|
-
deviceId:
|
|
4183
|
-
action:
|
|
4184
|
-
id:
|
|
4556
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4557
|
+
action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4558
|
+
id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4185
4559
|
}
|
|
4186
4560
|
},
|
|
4187
4561
|
async ({ deviceId, action, id }) => {
|
|
@@ -4191,19 +4565,19 @@ ${JSON.stringify(slim)}
|
|
|
4191
4565
|
case "rescan": {
|
|
4192
4566
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4193
4567
|
const ss = result?.scanStatus;
|
|
4194
|
-
if (!ss) return
|
|
4195
|
-
return
|
|
4568
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4569
|
+
return text17(
|
|
4196
4570
|
`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
4571
|
);
|
|
4198
4572
|
}
|
|
4199
4573
|
case "measure_sizes": {
|
|
4200
4574
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4201
|
-
return
|
|
4575
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4202
4576
|
}
|
|
4203
4577
|
case "detail": {
|
|
4204
|
-
if (id == null) return
|
|
4578
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
4205
4579
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4206
|
-
if (!detail) return
|
|
4580
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4207
4581
|
const lines = [
|
|
4208
4582
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4209
4583
|
"",
|
|
@@ -4221,32 +4595,32 @@ ${JSON.stringify(slim)}
|
|
|
4221
4595
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4222
4596
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4223
4597
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4224
|
-
return
|
|
4598
|
+
return text17(lines.join("\n"));
|
|
4225
4599
|
}
|
|
4226
4600
|
case "save_baseline": {
|
|
4227
4601
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4228
|
-
return
|
|
4602
|
+
return text17(
|
|
4229
4603
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4230
4604
|
);
|
|
4231
4605
|
}
|
|
4232
4606
|
case "clear_baseline": {
|
|
4233
4607
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4234
|
-
return
|
|
4608
|
+
return text17("Baseline cleared.");
|
|
4235
4609
|
}
|
|
4236
4610
|
case "clear_records": {
|
|
4237
4611
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4238
|
-
return
|
|
4612
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
4239
4613
|
}
|
|
4240
4614
|
}
|
|
4241
4615
|
} catch (e) {
|
|
4242
|
-
return
|
|
4616
|
+
return fail19(errMsg18(e));
|
|
4243
4617
|
}
|
|
4244
4618
|
}
|
|
4245
4619
|
);
|
|
4246
4620
|
}
|
|
4247
4621
|
|
|
4248
4622
|
// src/tools/packs/reload.ts
|
|
4249
|
-
var
|
|
4623
|
+
var import_zod20 = require("zod");
|
|
4250
4624
|
|
|
4251
4625
|
// src/capture/devServerReload.ts
|
|
4252
4626
|
var import_ws = require("ws");
|
|
@@ -4309,8 +4683,8 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4309
4683
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4310
4684
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4311
4685
|
var PROBE_INTERVAL_MS = 400;
|
|
4312
|
-
var
|
|
4313
|
-
var
|
|
4686
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4687
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4314
4688
|
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4315
4689
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4316
4690
|
async function ping(sync, deviceId, timeoutMs) {
|
|
@@ -4350,12 +4724,12 @@ function registerReloadTools(server, sync) {
|
|
|
4350
4724
|
{
|
|
4351
4725
|
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
4726
|
inputSchema: {
|
|
4353
|
-
strategy:
|
|
4727
|
+
strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4354
4728
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4355
4729
|
),
|
|
4356
|
-
wait:
|
|
4357
|
-
timeoutMs:
|
|
4358
|
-
deviceId:
|
|
4730
|
+
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."),
|
|
4731
|
+
timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4732
|
+
deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4359
4733
|
}
|
|
4360
4734
|
},
|
|
4361
4735
|
async (args) => {
|
|
@@ -4397,28 +4771,28 @@ function registerReloadTools(server, sync) {
|
|
|
4397
4771
|
}
|
|
4398
4772
|
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
4773
|
if (args.wait === false) {
|
|
4400
|
-
return
|
|
4774
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4401
4775
|
}
|
|
4402
4776
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4403
4777
|
if (elapsed == null) {
|
|
4404
|
-
return
|
|
4778
|
+
return fail20(
|
|
4405
4779
|
`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
4780
|
);
|
|
4407
4781
|
}
|
|
4408
|
-
return
|
|
4782
|
+
return text18(
|
|
4409
4783
|
`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
4784
|
);
|
|
4411
4785
|
} catch (e) {
|
|
4412
|
-
return
|
|
4786
|
+
return fail20(errMsg19(e));
|
|
4413
4787
|
}
|
|
4414
4788
|
}
|
|
4415
4789
|
);
|
|
4416
4790
|
}
|
|
4417
4791
|
|
|
4418
4792
|
// src/tools/packs/triage.ts
|
|
4419
|
-
var
|
|
4420
|
-
var
|
|
4421
|
-
var
|
|
4793
|
+
var import_zod21 = require("zod");
|
|
4794
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4795
|
+
var fail21 = (message) => ({
|
|
4422
4796
|
isError: true,
|
|
4423
4797
|
content: [{ type: "text", text: message }]
|
|
4424
4798
|
});
|
|
@@ -4453,9 +4827,9 @@ async function otherBackends(sync, target, opts) {
|
|
|
4453
4827
|
return notes.length ? `
|
|
4454
4828
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4455
4829
|
}
|
|
4456
|
-
async function section(title,
|
|
4830
|
+
async function section(title, run2) {
|
|
4457
4831
|
try {
|
|
4458
|
-
const body = await
|
|
4832
|
+
const body = await run2();
|
|
4459
4833
|
return body ? `${title}
|
|
4460
4834
|
${body}` : null;
|
|
4461
4835
|
} catch (e) {
|
|
@@ -4474,7 +4848,7 @@ function registerTriageTools(server, sync) {
|
|
|
4474
4848
|
{
|
|
4475
4849
|
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
4850
|
inputSchema: {
|
|
4477
|
-
deviceId:
|
|
4851
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4478
4852
|
}
|
|
4479
4853
|
},
|
|
4480
4854
|
async (args) => {
|
|
@@ -4581,7 +4955,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4581
4955
|
|
|
4582
4956
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4583
4957
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4584
|
-
return
|
|
4958
|
+
return text19(
|
|
4585
4959
|
`${head}${banner}
|
|
4586
4960
|
|
|
4587
4961
|
${body}
|
|
@@ -4589,18 +4963,18 @@ ${body}
|
|
|
4589
4963
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4590
4964
|
);
|
|
4591
4965
|
} catch (e) {
|
|
4592
|
-
return
|
|
4966
|
+
return fail21(errMsg20(e));
|
|
4593
4967
|
}
|
|
4594
4968
|
}
|
|
4595
4969
|
);
|
|
4596
4970
|
}
|
|
4597
4971
|
|
|
4598
4972
|
// src/tools/packs/flow.ts
|
|
4599
|
-
var
|
|
4973
|
+
var import_zod22 = require("zod");
|
|
4600
4974
|
var UI_TOOL_ID = "highlight-updates";
|
|
4601
4975
|
var APP_TOOL_ID2 = "app";
|
|
4602
|
-
var
|
|
4603
|
-
var
|
|
4976
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4977
|
+
var fail22 = (message) => ({
|
|
4604
4978
|
isError: true,
|
|
4605
4979
|
content: [{ type: "text", text: message }]
|
|
4606
4980
|
});
|
|
@@ -4621,19 +4995,19 @@ function formatElement2(e) {
|
|
|
4621
4995
|
const display = e.text ?? e.label;
|
|
4622
4996
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4623
4997
|
}
|
|
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:
|
|
4998
|
+
var stepSchema = import_zod22.z.object({
|
|
4999
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5000
|
+
longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
5001
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5002
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5003
|
+
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."),
|
|
5004
|
+
reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5005
|
+
waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5006
|
+
waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5007
|
+
waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5008
|
+
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."),
|
|
5009
|
+
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."),
|
|
5010
|
+
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
5011
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4638
5012
|
function registerFlowTools(server, sync) {
|
|
4639
5013
|
server.registerTool(
|
|
@@ -4641,10 +5015,10 @@ function registerFlowTools(server, sync) {
|
|
|
4641
5015
|
{
|
|
4642
5016
|
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
5017
|
inputSchema: {
|
|
4644
|
-
steps:
|
|
4645
|
-
include:
|
|
4646
|
-
continueOnFailure:
|
|
4647
|
-
deviceId:
|
|
5018
|
+
steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5019
|
+
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."),
|
|
5020
|
+
continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5021
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4648
5022
|
}
|
|
4649
5023
|
},
|
|
4650
5024
|
async (args) => {
|
|
@@ -4868,9 +5242,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4868
5242
|
}
|
|
4869
5243
|
}
|
|
4870
5244
|
}
|
|
4871
|
-
return
|
|
5245
|
+
return text20(parts.join("\n\n"));
|
|
4872
5246
|
} catch (e) {
|
|
4873
|
-
return
|
|
5247
|
+
return fail22(errMsg21(e));
|
|
4874
5248
|
}
|
|
4875
5249
|
}
|
|
4876
5250
|
);
|
|
@@ -4891,10 +5265,10 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4891
5265
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4892
5266
|
|
|
4893
5267
|
// src/tools/packs/renders.ts
|
|
4894
|
-
var
|
|
5268
|
+
var import_zod23 = require("zod");
|
|
4895
5269
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4896
|
-
var
|
|
4897
|
-
var
|
|
5270
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5271
|
+
var fail23 = (message) => ({
|
|
4898
5272
|
isError: true,
|
|
4899
5273
|
content: [{ type: "text", text: message }]
|
|
4900
5274
|
});
|
|
@@ -4904,12 +5278,12 @@ var DEFAULT_IDLE_MS = 3e3;
|
|
|
4904
5278
|
var SETTLE_MS = 400;
|
|
4905
5279
|
var DEFAULT_TOP = 12;
|
|
4906
5280
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4907
|
-
var stepSchema2 =
|
|
4908
|
-
tap:
|
|
4909
|
-
type:
|
|
4910
|
-
into:
|
|
4911
|
-
waitMs:
|
|
4912
|
-
repeat:
|
|
5281
|
+
var stepSchema2 = import_zod23.z.object({
|
|
5282
|
+
tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5283
|
+
type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5284
|
+
into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5285
|
+
waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5286
|
+
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
5287
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4914
5288
|
function selector2(needle) {
|
|
4915
5289
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -5002,14 +5376,14 @@ function registerRenderTools(server, sync) {
|
|
|
5002
5376
|
{
|
|
5003
5377
|
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
5378
|
inputSchema: {
|
|
5005
|
-
steps:
|
|
5379
|
+
steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
|
|
5006
5380
|
"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
5381
|
),
|
|
5008
|
-
idleMs:
|
|
5009
|
-
label:
|
|
5010
|
-
compareToPrevious:
|
|
5011
|
-
top:
|
|
5012
|
-
deviceId:
|
|
5382
|
+
idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5383
|
+
label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5384
|
+
compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5385
|
+
top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5386
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
5013
5387
|
}
|
|
5014
5388
|
},
|
|
5015
5389
|
async (args) => {
|
|
@@ -5018,18 +5392,18 @@ function registerRenderTools(server, sync) {
|
|
|
5018
5392
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
5019
5393
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
5020
5394
|
if (!uiTool) {
|
|
5021
|
-
return
|
|
5395
|
+
return fail23(
|
|
5022
5396
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
5023
5397
|
);
|
|
5024
5398
|
}
|
|
5025
5399
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
5026
|
-
return
|
|
5400
|
+
return fail23(
|
|
5027
5401
|
"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
5402
|
);
|
|
5029
5403
|
}
|
|
5030
5404
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
5031
5405
|
if (began && began.ok === false) {
|
|
5032
|
-
return
|
|
5406
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
5033
5407
|
}
|
|
5034
5408
|
await delay5(SETTLE_MS);
|
|
5035
5409
|
const startedAt = Date.now();
|
|
@@ -5074,7 +5448,7 @@ function registerRenderTools(server, sync) {
|
|
|
5074
5448
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5075
5449
|
const summary = ended?.summary;
|
|
5076
5450
|
if (!summary) {
|
|
5077
|
-
return
|
|
5451
|
+
return fail23(
|
|
5078
5452
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5079
5453
|
);
|
|
5080
5454
|
}
|
|
@@ -5099,17 +5473,17 @@ function registerRenderTools(server, sync) {
|
|
|
5099
5473
|
} else if (!args.compareToPrevious) {
|
|
5100
5474
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5101
5475
|
}
|
|
5102
|
-
return
|
|
5476
|
+
return text21(parts.join("\n"));
|
|
5103
5477
|
} catch (e) {
|
|
5104
|
-
return
|
|
5478
|
+
return fail23(errMsg22(e));
|
|
5105
5479
|
}
|
|
5106
5480
|
}
|
|
5107
5481
|
);
|
|
5108
5482
|
}
|
|
5109
5483
|
|
|
5110
5484
|
// src/verify/editWatcher.ts
|
|
5111
|
-
var
|
|
5112
|
-
var
|
|
5485
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
5486
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5113
5487
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5114
5488
|
".ts",
|
|
5115
5489
|
".tsx",
|
|
@@ -5152,14 +5526,14 @@ var EditWatcher = class {
|
|
|
5152
5526
|
if (mode === "never" || this.started) return;
|
|
5153
5527
|
this.started = true;
|
|
5154
5528
|
try {
|
|
5155
|
-
this.watcher =
|
|
5529
|
+
this.watcher = import_node_fs4.default.watch(
|
|
5156
5530
|
rootDir,
|
|
5157
5531
|
{ recursive: true },
|
|
5158
5532
|
(_event, filename) => {
|
|
5159
5533
|
if (!filename) return;
|
|
5160
5534
|
const rel2 = String(filename);
|
|
5161
|
-
if (rel2.split(
|
|
5162
|
-
if (!WATCHED_EXT.has(
|
|
5535
|
+
if (rel2.split(import_node_path4.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5536
|
+
if (!WATCHED_EXT.has(import_node_path4.default.extname(rel2))) return;
|
|
5163
5537
|
this.noteEdit(rel2);
|
|
5164
5538
|
}
|
|
5165
5539
|
);
|
|
@@ -5259,6 +5633,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
5259
5633
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
5260
5634
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
5261
5635
|
|
|
5636
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5637
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5638
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5639
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5640
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5641
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5642
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5643
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5644
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5645
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5646
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5647
|
+
and no Buoy integration in the app.
|
|
5648
|
+
|
|
5262
5649
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
5263
5650
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
5264
5651
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -5375,6 +5762,7 @@ function createMcpServer(sync) {
|
|
|
5375
5762
|
registerTimeMachineTools(server, sync);
|
|
5376
5763
|
registerScenariosTools(server, sync);
|
|
5377
5764
|
registerScreenshotTools(server, sync);
|
|
5765
|
+
registerCameraTools(server);
|
|
5378
5766
|
registerInteractTools(server, sync);
|
|
5379
5767
|
registerJsThreadTools(server, sync);
|
|
5380
5768
|
registerImagesTools(server, sync);
|
|
@@ -5383,16 +5771,16 @@ function createMcpServer(sync) {
|
|
|
5383
5771
|
return server;
|
|
5384
5772
|
}
|
|
5385
5773
|
var deviceIdField = {
|
|
5386
|
-
deviceId:
|
|
5774
|
+
deviceId: import_zod24.z.string().optional().describe(
|
|
5387
5775
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5388
5776
|
)
|
|
5389
5777
|
};
|
|
5390
5778
|
|
|
5391
5779
|
// src/init.ts
|
|
5392
|
-
var
|
|
5393
|
-
var
|
|
5394
|
-
var
|
|
5395
|
-
var
|
|
5780
|
+
var import_node_child_process3 = require("child_process");
|
|
5781
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5782
|
+
var import_node_os4 = __toESM(require("os"));
|
|
5783
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5396
5784
|
function defaultEnv() {
|
|
5397
5785
|
return { BUOY_VERIFY: "auto" };
|
|
5398
5786
|
}
|
|
@@ -5400,19 +5788,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5400
5788
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5401
5789
|
function runInit(opts = {}) {
|
|
5402
5790
|
const cwd = opts.cwd ?? process.cwd();
|
|
5403
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5791
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path5.default.resolve(__dirname, "../skill");
|
|
5404
5792
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5405
5793
|
const entry = resolveEntry(opts, log);
|
|
5406
5794
|
const done = [];
|
|
5407
|
-
upsertMcpConfig(
|
|
5408
|
-
|
|
5409
|
-
upsertMcpConfig(
|
|
5410
|
-
if (
|
|
5411
|
-
upsertMcpConfig(
|
|
5795
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5796
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.join(cwd, ".cursor"), { recursive: true });
|
|
5797
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5798
|
+
if (import_node_fs5.default.existsSync(import_node_path5.default.join(cwd, ".vscode"))) {
|
|
5799
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5412
5800
|
}
|
|
5413
5801
|
installSkills(skillSrcDir, cwd, done);
|
|
5414
|
-
appendDebuggingGuide(
|
|
5415
|
-
appendDebuggingGuide(
|
|
5802
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5803
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
5416
5804
|
log("Buoy MCP installed:");
|
|
5417
5805
|
for (const line of done) log(" \u2022 " + line);
|
|
5418
5806
|
log("");
|
|
@@ -5454,21 +5842,21 @@ function npxEntry() {
|
|
|
5454
5842
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5455
5843
|
}
|
|
5456
5844
|
function localEntry(opts, log) {
|
|
5457
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5845
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path5.default.join(import_node_os4.default.homedir(), ".buoy-mcp");
|
|
5458
5846
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5459
|
-
const
|
|
5460
|
-
if (
|
|
5461
|
-
return { type: "stdio", command: "node", args: [
|
|
5847
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5848
|
+
if (cliPath2) {
|
|
5849
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5462
5850
|
}
|
|
5463
5851
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5464
5852
|
return npxEntry();
|
|
5465
5853
|
}
|
|
5466
5854
|
function installLocalCopy(installDir, registry, log) {
|
|
5467
5855
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
5468
|
-
|
|
5856
|
+
import_node_fs5.default.mkdirSync(installDir, { recursive: true });
|
|
5469
5857
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5470
5858
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
5471
|
-
const res = (0,
|
|
5859
|
+
const res = (0, import_node_child_process3.spawnSync)(
|
|
5472
5860
|
npm,
|
|
5473
5861
|
[
|
|
5474
5862
|
"install",
|
|
@@ -5488,12 +5876,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5488
5876
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5489
5877
|
return null;
|
|
5490
5878
|
}
|
|
5491
|
-
const
|
|
5492
|
-
if (!
|
|
5493
|
-
log(`\u26A0 install finished but ${
|
|
5879
|
+
const cliPath2 = import_node_path5.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5880
|
+
if (!import_node_fs5.default.existsSync(cliPath2)) {
|
|
5881
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5494
5882
|
return null;
|
|
5495
5883
|
}
|
|
5496
|
-
return
|
|
5884
|
+
return cliPath2;
|
|
5497
5885
|
}
|
|
5498
5886
|
function probeRegistry() {
|
|
5499
5887
|
const registry = effectiveRegistry();
|
|
@@ -5511,14 +5899,14 @@ function effectiveRegistry() {
|
|
|
5511
5899
|
}
|
|
5512
5900
|
function npmConfigGet(key) {
|
|
5513
5901
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5514
|
-
const res = (0,
|
|
5902
|
+
const res = (0, import_node_child_process3.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
|
|
5515
5903
|
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
5516
5904
|
const val = res.stdout.trim();
|
|
5517
5905
|
if (!val || val === "undefined" || val === "null") return null;
|
|
5518
5906
|
return val;
|
|
5519
5907
|
}
|
|
5520
5908
|
function hostReachable(registry) {
|
|
5521
|
-
const done = (0,
|
|
5909
|
+
const done = (0, import_node_child_process3.spawnSync)(
|
|
5522
5910
|
process.execPath,
|
|
5523
5911
|
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS3)],
|
|
5524
5912
|
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 + 2e3 }
|
|
@@ -5541,22 +5929,22 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
5541
5929
|
`;
|
|
5542
5930
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
5543
5931
|
let json3 = {};
|
|
5544
|
-
if (
|
|
5932
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5545
5933
|
try {
|
|
5546
|
-
json3 = JSON.parse(
|
|
5934
|
+
json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
|
|
5547
5935
|
} catch {
|
|
5548
|
-
|
|
5936
|
+
import_node_fs5.default.copyFileSync(file, file + ".bak");
|
|
5549
5937
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
5550
5938
|
json3 = {};
|
|
5551
5939
|
}
|
|
5552
5940
|
} else {
|
|
5553
|
-
|
|
5941
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
|
|
5554
5942
|
}
|
|
5555
5943
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5556
5944
|
const existed = "buoy" in servers;
|
|
5557
5945
|
servers.buoy = entry;
|
|
5558
5946
|
json3[key] = servers;
|
|
5559
|
-
|
|
5947
|
+
import_node_fs5.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
5560
5948
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
5561
5949
|
}
|
|
5562
5950
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
@@ -5616,9 +6004,9 @@ mentions it.
|
|
|
5616
6004
|
`;
|
|
5617
6005
|
function appendDebuggingGuide(file, cwd, done) {
|
|
5618
6006
|
let existing = "";
|
|
5619
|
-
if (
|
|
6007
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5620
6008
|
try {
|
|
5621
|
-
existing =
|
|
6009
|
+
existing = import_node_fs5.default.readFileSync(file, "utf8");
|
|
5622
6010
|
} catch {
|
|
5623
6011
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5624
6012
|
return;
|
|
@@ -5630,40 +6018,40 @@ function appendDebuggingGuide(file, cwd, done) {
|
|
|
5630
6018
|
}
|
|
5631
6019
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
5632
6020
|
try {
|
|
5633
|
-
|
|
6021
|
+
import_node_fs5.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
5634
6022
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
5635
6023
|
} catch {
|
|
5636
6024
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5637
6025
|
}
|
|
5638
6026
|
}
|
|
5639
6027
|
function installSkills(skillSrcDir, cwd, done) {
|
|
5640
|
-
if (!
|
|
6028
|
+
if (!import_node_fs5.default.existsSync(skillSrcDir)) {
|
|
5641
6029
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5642
6030
|
return;
|
|
5643
6031
|
}
|
|
5644
|
-
const skills =
|
|
6032
|
+
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
6033
|
if (skills.length === 0) {
|
|
5646
6034
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5647
6035
|
return;
|
|
5648
6036
|
}
|
|
5649
6037
|
for (const skill of skills) {
|
|
5650
|
-
const from =
|
|
5651
|
-
const to =
|
|
6038
|
+
const from = import_node_path5.default.join(skillSrcDir, skill.name);
|
|
6039
|
+
const to = import_node_path5.default.join(cwd, ".claude", "skills", skill.name);
|
|
5652
6040
|
copyDir(from, to);
|
|
5653
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6041
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path5.default.join(to, "SKILL.md"), cwd)}`);
|
|
5654
6042
|
}
|
|
5655
6043
|
}
|
|
5656
6044
|
function copyDir(from, to) {
|
|
5657
|
-
|
|
5658
|
-
for (const entry of
|
|
5659
|
-
const s =
|
|
5660
|
-
const d =
|
|
6045
|
+
import_node_fs5.default.mkdirSync(to, { recursive: true });
|
|
6046
|
+
for (const entry of import_node_fs5.default.readdirSync(from, { withFileTypes: true })) {
|
|
6047
|
+
const s = import_node_path5.default.join(from, entry.name);
|
|
6048
|
+
const d = import_node_path5.default.join(to, entry.name);
|
|
5661
6049
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5662
|
-
else
|
|
6050
|
+
else import_node_fs5.default.copyFileSync(s, d);
|
|
5663
6051
|
}
|
|
5664
6052
|
}
|
|
5665
6053
|
function rel(file, cwd) {
|
|
5666
|
-
const r =
|
|
6054
|
+
const r = import_node_path5.default.relative(cwd, file);
|
|
5667
6055
|
return r && !r.startsWith("..") ? r : file;
|
|
5668
6056
|
}
|
|
5669
6057
|
// Annotate the CommonJS export names for ESM import in node:
|