@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/cli.js
CHANGED
|
@@ -252,6 +252,7 @@ var SyncClient = class {
|
|
|
252
252
|
isConnected: d.isConnected !== false,
|
|
253
253
|
isPro: d.isPro === true,
|
|
254
254
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
255
|
+
collision: d.collision === true,
|
|
255
256
|
tools: caps?.tools ?? []
|
|
256
257
|
};
|
|
257
258
|
});
|
|
@@ -640,7 +641,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
640
641
|
|
|
641
642
|
// src/server.ts
|
|
642
643
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
643
|
-
var
|
|
644
|
+
var import_zod24 = require("zod");
|
|
644
645
|
|
|
645
646
|
// src/tools/generic.ts
|
|
646
647
|
var import_zod = require("zod");
|
|
@@ -651,7 +652,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
651
652
|
var import_node_path = __toESM(require("path"));
|
|
652
653
|
|
|
653
654
|
// src/version.ts
|
|
654
|
-
var VERSION = "7.0.
|
|
655
|
+
var VERSION = "7.0.33" ? "7.0.33" : "0.0.0-dev";
|
|
655
656
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
656
657
|
|
|
657
658
|
// src/update/checkForUpdate.ts
|
|
@@ -745,8 +746,12 @@ function registerGenericTools(server, sync) {
|
|
|
745
746
|
const connected = devices.filter((d) => d.isConnected);
|
|
746
747
|
const anyPro = connected.some((d) => d.isPro);
|
|
747
748
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
749
|
+
const collided = connected.filter((d) => d.collision);
|
|
748
750
|
return json({
|
|
749
751
|
devices,
|
|
752
|
+
...collided.length > 0 ? {
|
|
753
|
+
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.`
|
|
754
|
+
} : {},
|
|
750
755
|
...anyPro ? {} : {
|
|
751
756
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
752
757
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -1384,7 +1389,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1384
1389
|
let sawActive = false;
|
|
1385
1390
|
let idleStreak = 0;
|
|
1386
1391
|
let lastCount = -1;
|
|
1387
|
-
let
|
|
1392
|
+
let resolved2 = null;
|
|
1388
1393
|
while (Date.now() - start < cap3) {
|
|
1389
1394
|
let snap2;
|
|
1390
1395
|
try {
|
|
@@ -1396,7 +1401,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1396
1401
|
const completed = snap2.live?.automationCompleted;
|
|
1397
1402
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1398
1403
|
if (completed?.phase === "done") {
|
|
1399
|
-
|
|
1404
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1400
1405
|
break;
|
|
1401
1406
|
}
|
|
1402
1407
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1406,11 +1411,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1406
1411
|
const count = runCountFor(snap2.index, newest);
|
|
1407
1412
|
if (count === lastCount) {
|
|
1408
1413
|
if (sawActive && ++idleStreak >= 3) {
|
|
1409
|
-
|
|
1414
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1410
1415
|
break;
|
|
1411
1416
|
}
|
|
1412
1417
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1413
|
-
|
|
1418
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1414
1419
|
break;
|
|
1415
1420
|
}
|
|
1416
1421
|
} else {
|
|
@@ -1422,11 +1427,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1422
1427
|
}
|
|
1423
1428
|
await delay2(POLL_INTERVAL_MS);
|
|
1424
1429
|
}
|
|
1425
|
-
if (!
|
|
1430
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1426
1431
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1427
1432
|
});
|
|
1428
1433
|
const snap = await readPerfSnapshot(sync, target);
|
|
1429
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1434
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1430
1435
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1431
1436
|
});
|
|
1432
1437
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -1753,8 +1758,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
1753
1758
|
}
|
|
1754
1759
|
function bodyLines(e, fetched) {
|
|
1755
1760
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
1756
|
-
const label = (inline,
|
|
1757
|
-
const value = inline ??
|
|
1761
|
+
const label = (inline, resolved2, omitted) => {
|
|
1762
|
+
const value = inline ?? resolved2;
|
|
1758
1763
|
if (value !== void 0 && value !== null) return truncate(value);
|
|
1759
1764
|
if (!omitted) return "(none)";
|
|
1760
1765
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -3335,9 +3340,9 @@ function reasonMessage(res, query) {
|
|
|
3335
3340
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
3336
3341
|
}
|
|
3337
3342
|
}
|
|
3338
|
-
function candidatesText(
|
|
3339
|
-
if (!
|
|
3340
|
-
const lines =
|
|
3343
|
+
function candidatesText(candidates2) {
|
|
3344
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
3345
|
+
const lines = candidates2.map((c) => {
|
|
3341
3346
|
const id = c.testID || c.nativeID;
|
|
3342
3347
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
3343
3348
|
});
|
|
@@ -3436,14 +3441,383 @@ function registerScreenshotTools(server, sync) {
|
|
|
3436
3441
|
);
|
|
3437
3442
|
}
|
|
3438
3443
|
|
|
3439
|
-
// src/tools/packs/
|
|
3444
|
+
// src/tools/packs/camera.ts
|
|
3440
3445
|
var import_zod15 = require("zod");
|
|
3441
|
-
|
|
3446
|
+
|
|
3447
|
+
// src/capture/camera.ts
|
|
3448
|
+
var import_node_child_process2 = require("child_process");
|
|
3449
|
+
var import_node_util2 = require("util");
|
|
3450
|
+
var import_node_fs3 = require("fs");
|
|
3451
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3452
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3453
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3454
|
+
function candidates() {
|
|
3455
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3456
|
+
const list = [
|
|
3457
|
+
...fromEnv ? [import_node_path3.default.join(fromEnv, "buoycam")] : [],
|
|
3458
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3459
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3460
|
+
];
|
|
3461
|
+
let dir = __dirname;
|
|
3462
|
+
for (let i = 0; i < 8; i++) {
|
|
3463
|
+
list.push(import_node_path3.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3464
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
3465
|
+
if (parent === dir) break;
|
|
3466
|
+
dir = parent;
|
|
3467
|
+
}
|
|
3468
|
+
return list;
|
|
3469
|
+
}
|
|
3470
|
+
var resolved = null;
|
|
3471
|
+
function cliPath() {
|
|
3472
|
+
if (resolved) return resolved;
|
|
3473
|
+
resolved = candidates().find((c) => (0, import_node_fs3.existsSync)(c)) ?? null;
|
|
3474
|
+
return resolved;
|
|
3475
|
+
}
|
|
3476
|
+
async function run(args) {
|
|
3477
|
+
const cli = cliPath();
|
|
3478
|
+
if (!cli) {
|
|
3479
|
+
return {
|
|
3480
|
+
ok: false,
|
|
3481
|
+
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`."
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
try {
|
|
3485
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3486
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
const stdout = err.stdout ?? "";
|
|
3489
|
+
try {
|
|
3490
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3491
|
+
} catch {
|
|
3492
|
+
return {
|
|
3493
|
+
ok: false,
|
|
3494
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
function describeDiagnosis(checks) {
|
|
3500
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3501
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3502
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3503
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3504
|
+
}
|
|
3505
|
+
const lines = [];
|
|
3506
|
+
for (const c of [...failed, ...warned]) {
|
|
3507
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3508
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3509
|
+
}
|
|
3510
|
+
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.`;
|
|
3511
|
+
return [headline, "", ...lines].join("\n");
|
|
3512
|
+
}
|
|
3513
|
+
function describeDevices(devices, apps) {
|
|
3514
|
+
if (devices.length === 0) {
|
|
3515
|
+
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._";
|
|
3516
|
+
}
|
|
3517
|
+
const lines = [
|
|
3518
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3519
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3520
|
+
];
|
|
3521
|
+
if (devices.length > 1 && apps === null) {
|
|
3522
|
+
lines.push(
|
|
3523
|
+
"",
|
|
3524
|
+
"_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._"
|
|
3525
|
+
);
|
|
3526
|
+
return lines.join("\n");
|
|
3527
|
+
}
|
|
3528
|
+
if (apps === null) {
|
|
3529
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3530
|
+
return lines.join("\n");
|
|
3531
|
+
}
|
|
3532
|
+
if (apps.length === 0) {
|
|
3533
|
+
lines.push("", "No apps installed on it.");
|
|
3534
|
+
return lines.join("\n");
|
|
3535
|
+
}
|
|
3536
|
+
lines.push(
|
|
3537
|
+
"",
|
|
3538
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3539
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3540
|
+
"",
|
|
3541
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3542
|
+
);
|
|
3543
|
+
return lines.join("\n");
|
|
3544
|
+
}
|
|
3545
|
+
function describeInputs(cameras, windows) {
|
|
3546
|
+
const lines = [];
|
|
3547
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3548
|
+
if (cameras.length) {
|
|
3549
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3550
|
+
} else {
|
|
3551
|
+
lines.push(
|
|
3552
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
lines.push("");
|
|
3556
|
+
lines.push(
|
|
3557
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3558
|
+
);
|
|
3559
|
+
if (windows.length) {
|
|
3560
|
+
lines.push(
|
|
3561
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3562
|
+
);
|
|
3563
|
+
} else {
|
|
3564
|
+
lines.push(
|
|
3565
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
return lines.join("\n");
|
|
3569
|
+
}
|
|
3570
|
+
function entitlementPaths() {
|
|
3571
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3572
|
+
if (fromEnv) return [fromEnv];
|
|
3573
|
+
return [
|
|
3574
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3575
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3576
|
+
];
|
|
3577
|
+
}
|
|
3578
|
+
function readEntitlement() {
|
|
3579
|
+
for (const p of entitlementPaths()) {
|
|
3580
|
+
try {
|
|
3581
|
+
if (!(0, import_node_fs3.existsSync)(p)) continue;
|
|
3582
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf8"));
|
|
3583
|
+
return {
|
|
3584
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3585
|
+
isPro: parsed.isPro === true
|
|
3586
|
+
};
|
|
3587
|
+
} catch {
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
return { tier: "anonymous", isPro: false };
|
|
3591
|
+
}
|
|
3592
|
+
function proRequired(tool) {
|
|
3593
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3594
|
+
|
|
3595
|
+
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.
|
|
3596
|
+
|
|
3597
|
+
_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._`;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// src/tools/packs/camera.ts
|
|
3442
3601
|
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3602
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3443
3603
|
var fail15 = (message) => ({
|
|
3444
3604
|
isError: true,
|
|
3445
3605
|
content: [{ type: "text", text: message }]
|
|
3446
3606
|
});
|
|
3607
|
+
function registerCameraTools(server) {
|
|
3608
|
+
server.registerTool(
|
|
3609
|
+
"camera_source",
|
|
3610
|
+
{
|
|
3611
|
+
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.",
|
|
3612
|
+
inputSchema: {
|
|
3613
|
+
kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3614
|
+
value: import_zod15.z.string().optional().describe(
|
|
3615
|
+
"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`."
|
|
3616
|
+
),
|
|
3617
|
+
format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3618
|
+
}
|
|
3619
|
+
},
|
|
3620
|
+
async (args) => {
|
|
3621
|
+
const blocked = gate("camera_source");
|
|
3622
|
+
if (blocked) return blocked;
|
|
3623
|
+
const cmd = ["source", args.kind];
|
|
3624
|
+
if (args.value) cmd.push(args.value);
|
|
3625
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3626
|
+
const result = await run(cmd);
|
|
3627
|
+
if (!result.ok) {
|
|
3628
|
+
return fail15(
|
|
3629
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
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;
|
|
3633
|
+
return text13(
|
|
3634
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3635
|
+
|
|
3636
|
+
_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._`
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
);
|
|
3640
|
+
server.registerTool(
|
|
3641
|
+
"camera_status",
|
|
3642
|
+
{
|
|
3643
|
+
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.",
|
|
3644
|
+
inputSchema: {}
|
|
3645
|
+
},
|
|
3646
|
+
async () => {
|
|
3647
|
+
const blocked = gate("camera_status");
|
|
3648
|
+
if (blocked) return blocked;
|
|
3649
|
+
const result = await run(["status"]);
|
|
3650
|
+
if (!result.ok || !result.data) {
|
|
3651
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3652
|
+
}
|
|
3653
|
+
const { publishing, fps, codes } = result.data;
|
|
3654
|
+
if (!publishing) {
|
|
3655
|
+
return text13(
|
|
3656
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3657
|
+
);
|
|
3658
|
+
}
|
|
3659
|
+
return text13(
|
|
3660
|
+
[
|
|
3661
|
+
`Publishing at ${fps} fps.`,
|
|
3662
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3663
|
+
"",
|
|
3664
|
+
"_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._"
|
|
3665
|
+
].join("\n")
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
);
|
|
3669
|
+
server.registerTool(
|
|
3670
|
+
"camera_launch",
|
|
3671
|
+
{
|
|
3672
|
+
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.",
|
|
3673
|
+
inputSchema: {
|
|
3674
|
+
bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
|
|
3675
|
+
udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3678
|
+
async (args) => {
|
|
3679
|
+
const blocked = gate("camera_launch");
|
|
3680
|
+
if (blocked) return blocked;
|
|
3681
|
+
const cmd = ["launch", args.bundleId];
|
|
3682
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3683
|
+
const result = await run(cmd);
|
|
3684
|
+
if (!result.ok) {
|
|
3685
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3686
|
+
}
|
|
3687
|
+
return text13(
|
|
3688
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
);
|
|
3692
|
+
server.registerTool(
|
|
3693
|
+
"camera_diagnose",
|
|
3694
|
+
{
|
|
3695
|
+
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.",
|
|
3696
|
+
inputSchema: {
|
|
3697
|
+
bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3698
|
+
}
|
|
3699
|
+
},
|
|
3700
|
+
async (args) => {
|
|
3701
|
+
const cmd = ["diagnose"];
|
|
3702
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3703
|
+
const result = await run(cmd);
|
|
3704
|
+
if (!result.data?.checks) {
|
|
3705
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3706
|
+
}
|
|
3707
|
+
const ent = readEntitlement();
|
|
3708
|
+
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._";
|
|
3709
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3710
|
+
}
|
|
3711
|
+
);
|
|
3712
|
+
server.registerTool(
|
|
3713
|
+
"camera_stop",
|
|
3714
|
+
{
|
|
3715
|
+
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.",
|
|
3716
|
+
inputSchema: {}
|
|
3717
|
+
},
|
|
3718
|
+
async () => {
|
|
3719
|
+
const blocked = gate("camera_stop");
|
|
3720
|
+
if (blocked) return blocked;
|
|
3721
|
+
const result = await run(["stop"]);
|
|
3722
|
+
if (!result.ok) {
|
|
3723
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3724
|
+
}
|
|
3725
|
+
return text13("The simulator camera is off.");
|
|
3726
|
+
}
|
|
3727
|
+
);
|
|
3728
|
+
server.registerTool(
|
|
3729
|
+
"camera_devices",
|
|
3730
|
+
{
|
|
3731
|
+
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.",
|
|
3732
|
+
inputSchema: {
|
|
3733
|
+
udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3734
|
+
}
|
|
3735
|
+
},
|
|
3736
|
+
async (args) => {
|
|
3737
|
+
const blocked = gate("camera_devices");
|
|
3738
|
+
if (blocked) return blocked;
|
|
3739
|
+
const devicesResult = await run(["devices"]);
|
|
3740
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3741
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3742
|
+
}
|
|
3743
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3744
|
+
let apps = null;
|
|
3745
|
+
if (devices.length === 1 || args.udid) {
|
|
3746
|
+
const appsResult = await run([
|
|
3747
|
+
"apps",
|
|
3748
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3749
|
+
]);
|
|
3750
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3751
|
+
}
|
|
3752
|
+
return text13(describeDevices(devices, apps));
|
|
3753
|
+
}
|
|
3754
|
+
);
|
|
3755
|
+
server.registerTool(
|
|
3756
|
+
"camera_inputs",
|
|
3757
|
+
{
|
|
3758
|
+
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.",
|
|
3759
|
+
inputSchema: {}
|
|
3760
|
+
},
|
|
3761
|
+
async () => {
|
|
3762
|
+
const blocked = gate("camera_inputs");
|
|
3763
|
+
if (blocked) return blocked;
|
|
3764
|
+
const [cams, wins] = await Promise.all([
|
|
3765
|
+
run(["cameras"]),
|
|
3766
|
+
run(["windows"])
|
|
3767
|
+
]);
|
|
3768
|
+
if (!cams.ok && !wins.ok) {
|
|
3769
|
+
return fail15(
|
|
3770
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
return text13(
|
|
3774
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
);
|
|
3778
|
+
server.registerTool(
|
|
3779
|
+
"camera_zero_setup",
|
|
3780
|
+
{
|
|
3781
|
+
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.",
|
|
3782
|
+
inputSchema: {
|
|
3783
|
+
action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3784
|
+
udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3785
|
+
}
|
|
3786
|
+
},
|
|
3787
|
+
async (args) => {
|
|
3788
|
+
const blocked = gate("camera_zero_setup");
|
|
3789
|
+
if (blocked) return blocked;
|
|
3790
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3791
|
+
if (!result.ok) {
|
|
3792
|
+
return fail15(
|
|
3793
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3794
|
+
);
|
|
3795
|
+
}
|
|
3796
|
+
const d = result.data ?? {};
|
|
3797
|
+
if (args.action === "status") {
|
|
3798
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3799
|
+
return text13(
|
|
3800
|
+
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."
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
if (args.action === "on") {
|
|
3804
|
+
return text13(
|
|
3805
|
+
"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._"
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
return text13("Zero-setup mode is **off**.");
|
|
3809
|
+
}
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// src/tools/packs/interact.ts
|
|
3814
|
+
var import_zod16 = require("zod");
|
|
3815
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3816
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3817
|
+
var fail16 = (message) => ({
|
|
3818
|
+
isError: true,
|
|
3819
|
+
content: [{ type: "text", text: message }]
|
|
3820
|
+
});
|
|
3447
3821
|
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3448
3822
|
function formatElement(e) {
|
|
3449
3823
|
let line = e.interactive ? "\u2022 " : " ";
|
|
@@ -3467,8 +3841,8 @@ function registerInteractTools(server, sync) {
|
|
|
3467
3841
|
{
|
|
3468
3842
|
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.",
|
|
3469
3843
|
inputSchema: {
|
|
3470
|
-
interactiveOnly:
|
|
3471
|
-
deviceId:
|
|
3844
|
+
interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3845
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3472
3846
|
}
|
|
3473
3847
|
},
|
|
3474
3848
|
async (args) => {
|
|
@@ -3478,16 +3852,16 @@ function registerInteractTools(server, sync) {
|
|
|
3478
3852
|
const all = res?.elements ?? [];
|
|
3479
3853
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3480
3854
|
if (elements.length === 0) {
|
|
3481
|
-
return
|
|
3855
|
+
return text14(
|
|
3482
3856
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3483
3857
|
);
|
|
3484
3858
|
}
|
|
3485
3859
|
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 }).`;
|
|
3486
|
-
return
|
|
3860
|
+
return text14(`${header}
|
|
3487
3861
|
|
|
3488
3862
|
${elements.map(formatElement).join("\n")}`);
|
|
3489
3863
|
} catch (e) {
|
|
3490
|
-
return
|
|
3864
|
+
return fail16(errMsg15(e));
|
|
3491
3865
|
}
|
|
3492
3866
|
}
|
|
3493
3867
|
);
|
|
@@ -3496,20 +3870,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3496
3870
|
{
|
|
3497
3871
|
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.",
|
|
3498
3872
|
inputSchema: {
|
|
3499
|
-
nativeTag:
|
|
3500
|
-
testID:
|
|
3501
|
-
query:
|
|
3502
|
-
value:
|
|
3503
|
-
text:
|
|
3504
|
-
longPress:
|
|
3505
|
-
scrollIntoView:
|
|
3506
|
-
deviceId:
|
|
3873
|
+
nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3874
|
+
testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
|
|
3875
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3876
|
+
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)."),
|
|
3877
|
+
text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3878
|
+
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."),
|
|
3879
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
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) => {
|
|
3510
3884
|
try {
|
|
3511
3885
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3512
|
-
return
|
|
3886
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3513
3887
|
}
|
|
3514
3888
|
const target = sync.requireProDevice(args.deviceId);
|
|
3515
3889
|
const res = await sync.callAction(
|
|
@@ -3534,25 +3908,25 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3534
3908
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3535
3909
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3536
3910
|
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}`;
|
|
3537
|
-
return
|
|
3911
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3538
3912
|
}
|
|
3539
3913
|
let msg = res?.reason ?? "Tap failed.";
|
|
3540
3914
|
if (res?.candidates?.length) {
|
|
3541
3915
|
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");
|
|
3542
3916
|
}
|
|
3543
|
-
return
|
|
3917
|
+
return fail16(msg);
|
|
3544
3918
|
} catch (e) {
|
|
3545
|
-
return
|
|
3919
|
+
return fail16(errMsg15(e));
|
|
3546
3920
|
}
|
|
3547
3921
|
}
|
|
3548
3922
|
);
|
|
3549
3923
|
}
|
|
3550
3924
|
|
|
3551
3925
|
// src/tools/packs/jsThread.ts
|
|
3552
|
-
var
|
|
3926
|
+
var import_zod17 = require("zod");
|
|
3553
3927
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3554
|
-
var
|
|
3555
|
-
var
|
|
3928
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3929
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3556
3930
|
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3557
3931
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3558
3932
|
function sparkline(values) {
|
|
@@ -3624,9 +3998,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3624
3998
|
{
|
|
3625
3999
|
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.',
|
|
3626
4000
|
inputSchema: {
|
|
3627
|
-
deviceId:
|
|
3628
|
-
durationMs:
|
|
3629
|
-
clearFirst:
|
|
4001
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4002
|
+
durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4003
|
+
clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3630
4004
|
}
|
|
3631
4005
|
},
|
|
3632
4006
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3639,7 +4013,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3639
4013
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3640
4014
|
target
|
|
3641
4015
|
);
|
|
3642
|
-
if (!snap) return
|
|
4016
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3643
4017
|
const md = formatTopMarkdown(snap, duration);
|
|
3644
4018
|
const slim = {
|
|
3645
4019
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3658,13 +4032,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3658
4032
|
worstFreezeMs: r.worstFreezeMs
|
|
3659
4033
|
}))
|
|
3660
4034
|
};
|
|
3661
|
-
return
|
|
4035
|
+
return text15(`${md}
|
|
3662
4036
|
|
|
3663
4037
|
\`\`\`json
|
|
3664
4038
|
${JSON.stringify(slim)}
|
|
3665
4039
|
\`\`\``);
|
|
3666
4040
|
} catch (e) {
|
|
3667
|
-
return
|
|
4041
|
+
return fail17(errMsg16(e));
|
|
3668
4042
|
}
|
|
3669
4043
|
}
|
|
3670
4044
|
);
|
|
@@ -3673,8 +4047,8 @@ ${JSON.stringify(slim)}
|
|
|
3673
4047
|
{
|
|
3674
4048
|
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).',
|
|
3675
4049
|
inputSchema: {
|
|
3676
|
-
deviceId:
|
|
3677
|
-
key:
|
|
4050
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4051
|
+
key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3678
4052
|
}
|
|
3679
4053
|
},
|
|
3680
4054
|
async ({ deviceId, key }) => {
|
|
@@ -3687,18 +4061,18 @@ ${JSON.stringify(slim)}
|
|
|
3687
4061
|
target
|
|
3688
4062
|
);
|
|
3689
4063
|
if (!detail) {
|
|
3690
|
-
return
|
|
4064
|
+
return fail17(
|
|
3691
4065
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3692
4066
|
);
|
|
3693
4067
|
}
|
|
3694
4068
|
const md = formatDetailMarkdown(detail);
|
|
3695
|
-
return
|
|
4069
|
+
return text15(`${md}
|
|
3696
4070
|
|
|
3697
4071
|
\`\`\`json
|
|
3698
4072
|
${JSON.stringify(detail)}
|
|
3699
4073
|
\`\`\``);
|
|
3700
4074
|
} catch (e) {
|
|
3701
|
-
return
|
|
4075
|
+
return fail17(errMsg16(e));
|
|
3702
4076
|
}
|
|
3703
4077
|
}
|
|
3704
4078
|
);
|
|
@@ -3707,26 +4081,26 @@ ${JSON.stringify(detail)}
|
|
|
3707
4081
|
{
|
|
3708
4082
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3709
4083
|
inputSchema: {
|
|
3710
|
-
deviceId:
|
|
4084
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3711
4085
|
}
|
|
3712
4086
|
},
|
|
3713
4087
|
async ({ deviceId }) => {
|
|
3714
4088
|
try {
|
|
3715
4089
|
const target = sync.requireProDevice(deviceId);
|
|
3716
4090
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3717
|
-
return
|
|
4091
|
+
return text15("Cleared js-top stats.");
|
|
3718
4092
|
} catch (e) {
|
|
3719
|
-
return
|
|
4093
|
+
return fail17(errMsg16(e));
|
|
3720
4094
|
}
|
|
3721
4095
|
}
|
|
3722
4096
|
);
|
|
3723
4097
|
}
|
|
3724
4098
|
|
|
3725
4099
|
// src/tools/packs/images.ts
|
|
3726
|
-
var
|
|
4100
|
+
var import_zod18 = require("zod");
|
|
3727
4101
|
var IMAGES_TOOL_ID = "images";
|
|
3728
|
-
var
|
|
3729
|
-
var
|
|
4102
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4103
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3730
4104
|
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3731
4105
|
function uriTail(uri) {
|
|
3732
4106
|
if (!uri) return "(no source)";
|
|
@@ -3815,9 +4189,9 @@ function registerImagesTools(server, sync) {
|
|
|
3815
4189
|
{
|
|
3816
4190
|
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.",
|
|
3817
4191
|
inputSchema: {
|
|
3818
|
-
deviceId:
|
|
3819
|
-
limit:
|
|
3820
|
-
status:
|
|
4192
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4193
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4194
|
+
status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3821
4195
|
}
|
|
3822
4196
|
},
|
|
3823
4197
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3830,7 +4204,7 @@ function registerImagesTools(server, sync) {
|
|
|
3830
4204
|
{ limit: capped, status },
|
|
3831
4205
|
target
|
|
3832
4206
|
);
|
|
3833
|
-
if (!list) return
|
|
4207
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3834
4208
|
const md = formatListMarkdown(list, capped);
|
|
3835
4209
|
const slim = {
|
|
3836
4210
|
stats: list.stats,
|
|
@@ -3857,13 +4231,13 @@ function registerImagesTools(server, sync) {
|
|
|
3857
4231
|
layoutShifts: r.layoutShifts
|
|
3858
4232
|
}))
|
|
3859
4233
|
};
|
|
3860
|
-
return
|
|
4234
|
+
return text16(`${md}
|
|
3861
4235
|
|
|
3862
4236
|
\`\`\`json
|
|
3863
4237
|
${JSON.stringify(slim)}
|
|
3864
4238
|
\`\`\``);
|
|
3865
4239
|
} catch (e) {
|
|
3866
|
-
return
|
|
4240
|
+
return fail18(errMsg17(e));
|
|
3867
4241
|
}
|
|
3868
4242
|
}
|
|
3869
4243
|
);
|
|
@@ -3872,9 +4246,9 @@ ${JSON.stringify(slim)}
|
|
|
3872
4246
|
{
|
|
3873
4247
|
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.",
|
|
3874
4248
|
inputSchema: {
|
|
3875
|
-
deviceId:
|
|
3876
|
-
id:
|
|
3877
|
-
action:
|
|
4249
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4250
|
+
id: import_zod18.z.number().describe("Record id from get_images."),
|
|
4251
|
+
action: import_zod18.z.enum([
|
|
3878
4252
|
"hard_reload",
|
|
3879
4253
|
"retry",
|
|
3880
4254
|
"evict_disk",
|
|
@@ -3886,7 +4260,7 @@ ${JSON.stringify(slim)}
|
|
|
3886
4260
|
"swap_url",
|
|
3887
4261
|
"restore"
|
|
3888
4262
|
]).describe("What to do."),
|
|
3889
|
-
url:
|
|
4263
|
+
url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3890
4264
|
}
|
|
3891
4265
|
},
|
|
3892
4266
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3894,8 +4268,8 @@ ${JSON.stringify(slim)}
|
|
|
3894
4268
|
const target = sync.requireProDevice(deviceId);
|
|
3895
4269
|
if (action === "prove_savings") {
|
|
3896
4270
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3897
|
-
if (!savings) return
|
|
3898
|
-
if (!savings.ok) return
|
|
4271
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4272
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3899
4273
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3900
4274
|
if (savings.originalBytes != null)
|
|
3901
4275
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3907,11 +4281,11 @@ ${JSON.stringify(slim)}
|
|
|
3907
4281
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3908
4282
|
if (savings.optimizedUri)
|
|
3909
4283
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3910
|
-
return
|
|
4284
|
+
return text16(parts.join("\n"));
|
|
3911
4285
|
}
|
|
3912
4286
|
const entry = ACTION_MAP[action];
|
|
3913
|
-
if (!entry) return
|
|
3914
|
-
if (action === "swap_url" && !url) return
|
|
4287
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4288
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3915
4289
|
const result = await sync.callAction(
|
|
3916
4290
|
IMAGES_TOOL_ID,
|
|
3917
4291
|
entry.action,
|
|
@@ -3931,9 +4305,9 @@ ${JSON.stringify(slim)}
|
|
|
3931
4305
|
);
|
|
3932
4306
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3933
4307
|
}
|
|
3934
|
-
return
|
|
4308
|
+
return text16(lines.join("\n"));
|
|
3935
4309
|
} catch (e) {
|
|
3936
|
-
return
|
|
4310
|
+
return fail18(errMsg17(e));
|
|
3937
4311
|
}
|
|
3938
4312
|
}
|
|
3939
4313
|
);
|
|
@@ -3942,12 +4316,12 @@ ${JSON.stringify(slim)}
|
|
|
3942
4316
|
{
|
|
3943
4317
|
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.",
|
|
3944
4318
|
inputSchema: {
|
|
3945
|
-
deviceId:
|
|
3946
|
-
network:
|
|
3947
|
-
blankImages:
|
|
3948
|
-
massAction:
|
|
3949
|
-
clearRegistry:
|
|
3950
|
-
clearExpoCaches:
|
|
4319
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4320
|
+
network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4321
|
+
blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
|
|
4322
|
+
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."),
|
|
4323
|
+
clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
|
|
4324
|
+
clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3951
4325
|
}
|
|
3952
4326
|
},
|
|
3953
4327
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3974,17 +4348,17 @@ ${JSON.stringify(slim)}
|
|
|
3974
4348
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3975
4349
|
done.push(r?.message ?? "expo caches cleared");
|
|
3976
4350
|
}
|
|
3977
|
-
if (done.length === 0) return
|
|
3978
|
-
return
|
|
4351
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4352
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
3979
4353
|
} catch (e) {
|
|
3980
|
-
return
|
|
4354
|
+
return fail18(errMsg17(e));
|
|
3981
4355
|
}
|
|
3982
4356
|
}
|
|
3983
4357
|
);
|
|
3984
4358
|
}
|
|
3985
4359
|
|
|
3986
4360
|
// src/tools/packs/assets.ts
|
|
3987
|
-
var
|
|
4361
|
+
var import_zod19 = require("zod");
|
|
3988
4362
|
|
|
3989
4363
|
// src/tools/packs/assetsGraphNote.ts
|
|
3990
4364
|
function graphUnavailableNote(ss) {
|
|
@@ -3999,8 +4373,8 @@ function graphUnavailableNote(ss) {
|
|
|
3999
4373
|
|
|
4000
4374
|
// src/tools/packs/assets.ts
|
|
4001
4375
|
var ASSETS_TOOL_ID = "assets";
|
|
4002
|
-
var
|
|
4003
|
-
var
|
|
4376
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4377
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4004
4378
|
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
4005
4379
|
function kb2(bytes) {
|
|
4006
4380
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
@@ -4089,10 +4463,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4089
4463
|
{
|
|
4090
4464
|
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.",
|
|
4091
4465
|
inputSchema: {
|
|
4092
|
-
deviceId:
|
|
4093
|
-
limit:
|
|
4094
|
-
kind:
|
|
4095
|
-
unusedOnly:
|
|
4466
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4467
|
+
limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4468
|
+
kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4469
|
+
unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4096
4470
|
}
|
|
4097
4471
|
},
|
|
4098
4472
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4105,7 +4479,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4105
4479
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4106
4480
|
target
|
|
4107
4481
|
);
|
|
4108
|
-
if (!list) return
|
|
4482
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4109
4483
|
if (unusedOnly) {
|
|
4110
4484
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4111
4485
|
}
|
|
@@ -4129,13 +4503,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4129
4503
|
sizeSource: r.sizeSource
|
|
4130
4504
|
}))
|
|
4131
4505
|
};
|
|
4132
|
-
return
|
|
4506
|
+
return text17(`${md}
|
|
4133
4507
|
|
|
4134
4508
|
\`\`\`json
|
|
4135
4509
|
${JSON.stringify(slim)}
|
|
4136
4510
|
\`\`\``);
|
|
4137
4511
|
} catch (e) {
|
|
4138
|
-
return
|
|
4512
|
+
return fail19(errMsg18(e));
|
|
4139
4513
|
}
|
|
4140
4514
|
}
|
|
4141
4515
|
);
|
|
@@ -4144,9 +4518,9 @@ ${JSON.stringify(slim)}
|
|
|
4144
4518
|
{
|
|
4145
4519
|
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).",
|
|
4146
4520
|
inputSchema: {
|
|
4147
|
-
deviceId:
|
|
4148
|
-
action:
|
|
4149
|
-
id:
|
|
4521
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4522
|
+
action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4523
|
+
id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4150
4524
|
}
|
|
4151
4525
|
},
|
|
4152
4526
|
async ({ deviceId, action, id }) => {
|
|
@@ -4156,19 +4530,19 @@ ${JSON.stringify(slim)}
|
|
|
4156
4530
|
case "rescan": {
|
|
4157
4531
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4158
4532
|
const ss = result?.scanStatus;
|
|
4159
|
-
if (!ss) return
|
|
4160
|
-
return
|
|
4533
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4534
|
+
return text17(
|
|
4161
4535
|
`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" : "")
|
|
4162
4536
|
);
|
|
4163
4537
|
}
|
|
4164
4538
|
case "measure_sizes": {
|
|
4165
4539
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4166
|
-
return
|
|
4540
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4167
4541
|
}
|
|
4168
4542
|
case "detail": {
|
|
4169
|
-
if (id == null) return
|
|
4543
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
4170
4544
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4171
|
-
if (!detail) return
|
|
4545
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4172
4546
|
const lines = [
|
|
4173
4547
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4174
4548
|
"",
|
|
@@ -4186,32 +4560,32 @@ ${JSON.stringify(slim)}
|
|
|
4186
4560
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4187
4561
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4188
4562
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4189
|
-
return
|
|
4563
|
+
return text17(lines.join("\n"));
|
|
4190
4564
|
}
|
|
4191
4565
|
case "save_baseline": {
|
|
4192
4566
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4193
|
-
return
|
|
4567
|
+
return text17(
|
|
4194
4568
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4195
4569
|
);
|
|
4196
4570
|
}
|
|
4197
4571
|
case "clear_baseline": {
|
|
4198
4572
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4199
|
-
return
|
|
4573
|
+
return text17("Baseline cleared.");
|
|
4200
4574
|
}
|
|
4201
4575
|
case "clear_records": {
|
|
4202
4576
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4203
|
-
return
|
|
4577
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
4204
4578
|
}
|
|
4205
4579
|
}
|
|
4206
4580
|
} catch (e) {
|
|
4207
|
-
return
|
|
4581
|
+
return fail19(errMsg18(e));
|
|
4208
4582
|
}
|
|
4209
4583
|
}
|
|
4210
4584
|
);
|
|
4211
4585
|
}
|
|
4212
4586
|
|
|
4213
4587
|
// src/tools/packs/reload.ts
|
|
4214
|
-
var
|
|
4588
|
+
var import_zod20 = require("zod");
|
|
4215
4589
|
|
|
4216
4590
|
// src/capture/devServerReload.ts
|
|
4217
4591
|
var import_ws = require("ws");
|
|
@@ -4274,8 +4648,8 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4274
4648
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4275
4649
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4276
4650
|
var PROBE_INTERVAL_MS = 400;
|
|
4277
|
-
var
|
|
4278
|
-
var
|
|
4651
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4652
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4279
4653
|
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4280
4654
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4281
4655
|
async function ping(sync, deviceId, timeoutMs) {
|
|
@@ -4315,12 +4689,12 @@ function registerReloadTools(server, sync) {
|
|
|
4315
4689
|
{
|
|
4316
4690
|
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.",
|
|
4317
4691
|
inputSchema: {
|
|
4318
|
-
strategy:
|
|
4692
|
+
strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4319
4693
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4320
4694
|
),
|
|
4321
|
-
wait:
|
|
4322
|
-
timeoutMs:
|
|
4323
|
-
deviceId:
|
|
4695
|
+
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."),
|
|
4696
|
+
timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4697
|
+
deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4324
4698
|
}
|
|
4325
4699
|
},
|
|
4326
4700
|
async (args) => {
|
|
@@ -4362,28 +4736,28 @@ function registerReloadTools(server, sync) {
|
|
|
4362
4736
|
}
|
|
4363
4737
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4364
4738
|
if (args.wait === false) {
|
|
4365
|
-
return
|
|
4739
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4366
4740
|
}
|
|
4367
4741
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4368
4742
|
if (elapsed == null) {
|
|
4369
|
-
return
|
|
4743
|
+
return fail20(
|
|
4370
4744
|
`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.`
|
|
4371
4745
|
);
|
|
4372
4746
|
}
|
|
4373
|
-
return
|
|
4747
|
+
return text18(
|
|
4374
4748
|
`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.`
|
|
4375
4749
|
);
|
|
4376
4750
|
} catch (e) {
|
|
4377
|
-
return
|
|
4751
|
+
return fail20(errMsg19(e));
|
|
4378
4752
|
}
|
|
4379
4753
|
}
|
|
4380
4754
|
);
|
|
4381
4755
|
}
|
|
4382
4756
|
|
|
4383
4757
|
// src/tools/packs/triage.ts
|
|
4384
|
-
var
|
|
4385
|
-
var
|
|
4386
|
-
var
|
|
4758
|
+
var import_zod21 = require("zod");
|
|
4759
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4760
|
+
var fail21 = (message) => ({
|
|
4387
4761
|
isError: true,
|
|
4388
4762
|
content: [{ type: "text", text: message }]
|
|
4389
4763
|
});
|
|
@@ -4418,9 +4792,9 @@ async function otherBackends(sync, target, opts) {
|
|
|
4418
4792
|
return notes.length ? `
|
|
4419
4793
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4420
4794
|
}
|
|
4421
|
-
async function section(title,
|
|
4795
|
+
async function section(title, run2) {
|
|
4422
4796
|
try {
|
|
4423
|
-
const body = await
|
|
4797
|
+
const body = await run2();
|
|
4424
4798
|
return body ? `${title}
|
|
4425
4799
|
${body}` : null;
|
|
4426
4800
|
} catch (e) {
|
|
@@ -4439,7 +4813,7 @@ function registerTriageTools(server, sync) {
|
|
|
4439
4813
|
{
|
|
4440
4814
|
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.",
|
|
4441
4815
|
inputSchema: {
|
|
4442
|
-
deviceId:
|
|
4816
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4443
4817
|
}
|
|
4444
4818
|
},
|
|
4445
4819
|
async (args) => {
|
|
@@ -4546,7 +4920,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4546
4920
|
|
|
4547
4921
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4548
4922
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4549
|
-
return
|
|
4923
|
+
return text19(
|
|
4550
4924
|
`${head}${banner}
|
|
4551
4925
|
|
|
4552
4926
|
${body}
|
|
@@ -4554,18 +4928,18 @@ ${body}
|
|
|
4554
4928
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4555
4929
|
);
|
|
4556
4930
|
} catch (e) {
|
|
4557
|
-
return
|
|
4931
|
+
return fail21(errMsg20(e));
|
|
4558
4932
|
}
|
|
4559
4933
|
}
|
|
4560
4934
|
);
|
|
4561
4935
|
}
|
|
4562
4936
|
|
|
4563
4937
|
// src/tools/packs/flow.ts
|
|
4564
|
-
var
|
|
4938
|
+
var import_zod22 = require("zod");
|
|
4565
4939
|
var UI_TOOL_ID = "highlight-updates";
|
|
4566
4940
|
var APP_TOOL_ID2 = "app";
|
|
4567
|
-
var
|
|
4568
|
-
var
|
|
4941
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4942
|
+
var fail22 = (message) => ({
|
|
4569
4943
|
isError: true,
|
|
4570
4944
|
content: [{ type: "text", text: message }]
|
|
4571
4945
|
});
|
|
@@ -4586,19 +4960,19 @@ function formatElement2(e) {
|
|
|
4586
4960
|
const display = e.text ?? e.label;
|
|
4587
4961
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4588
4962
|
}
|
|
4589
|
-
var stepSchema =
|
|
4590
|
-
tap:
|
|
4591
|
-
longPress:
|
|
4592
|
-
type:
|
|
4593
|
-
into:
|
|
4594
|
-
set:
|
|
4595
|
-
reload:
|
|
4596
|
-
waitMs:
|
|
4597
|
-
waitFor:
|
|
4598
|
-
waitGone:
|
|
4599
|
-
expect:
|
|
4600
|
-
expectGone:
|
|
4601
|
-
timeoutMs:
|
|
4963
|
+
var stepSchema = import_zod22.z.object({
|
|
4964
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4965
|
+
longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4966
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4967
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4968
|
+
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."),
|
|
4969
|
+
reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4970
|
+
waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4971
|
+
waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4972
|
+
waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4973
|
+
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."),
|
|
4974
|
+
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."),
|
|
4975
|
+
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).")
|
|
4602
4976
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4603
4977
|
function registerFlowTools(server, sync) {
|
|
4604
4978
|
server.registerTool(
|
|
@@ -4606,10 +4980,10 @@ function registerFlowTools(server, sync) {
|
|
|
4606
4980
|
{
|
|
4607
4981
|
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.",
|
|
4608
4982
|
inputSchema: {
|
|
4609
|
-
steps:
|
|
4610
|
-
include:
|
|
4611
|
-
continueOnFailure:
|
|
4612
|
-
deviceId:
|
|
4983
|
+
steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4984
|
+
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."),
|
|
4985
|
+
continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4986
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4613
4987
|
}
|
|
4614
4988
|
},
|
|
4615
4989
|
async (args) => {
|
|
@@ -4833,9 +5207,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4833
5207
|
}
|
|
4834
5208
|
}
|
|
4835
5209
|
}
|
|
4836
|
-
return
|
|
5210
|
+
return text20(parts.join("\n\n"));
|
|
4837
5211
|
} catch (e) {
|
|
4838
|
-
return
|
|
5212
|
+
return fail22(errMsg21(e));
|
|
4839
5213
|
}
|
|
4840
5214
|
}
|
|
4841
5215
|
);
|
|
@@ -4856,10 +5230,10 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4856
5230
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4857
5231
|
|
|
4858
5232
|
// src/tools/packs/renders.ts
|
|
4859
|
-
var
|
|
5233
|
+
var import_zod23 = require("zod");
|
|
4860
5234
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4861
|
-
var
|
|
4862
|
-
var
|
|
5235
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5236
|
+
var fail23 = (message) => ({
|
|
4863
5237
|
isError: true,
|
|
4864
5238
|
content: [{ type: "text", text: message }]
|
|
4865
5239
|
});
|
|
@@ -4869,12 +5243,12 @@ var DEFAULT_IDLE_MS = 3e3;
|
|
|
4869
5243
|
var SETTLE_MS = 400;
|
|
4870
5244
|
var DEFAULT_TOP = 12;
|
|
4871
5245
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4872
|
-
var stepSchema2 =
|
|
4873
|
-
tap:
|
|
4874
|
-
type:
|
|
4875
|
-
into:
|
|
4876
|
-
waitMs:
|
|
4877
|
-
repeat:
|
|
5246
|
+
var stepSchema2 = import_zod23.z.object({
|
|
5247
|
+
tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5248
|
+
type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5249
|
+
into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5250
|
+
waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5251
|
+
repeat: import_zod23.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4878
5252
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4879
5253
|
function selector2(needle) {
|
|
4880
5254
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4967,14 +5341,14 @@ function registerRenderTools(server, sync) {
|
|
|
4967
5341
|
{
|
|
4968
5342
|
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.",
|
|
4969
5343
|
inputSchema: {
|
|
4970
|
-
steps:
|
|
5344
|
+
steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
|
|
4971
5345
|
"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."
|
|
4972
5346
|
),
|
|
4973
|
-
idleMs:
|
|
4974
|
-
label:
|
|
4975
|
-
compareToPrevious:
|
|
4976
|
-
top:
|
|
4977
|
-
deviceId:
|
|
5347
|
+
idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5348
|
+
label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5349
|
+
compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5350
|
+
top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5351
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4978
5352
|
}
|
|
4979
5353
|
},
|
|
4980
5354
|
async (args) => {
|
|
@@ -4983,18 +5357,18 @@ function registerRenderTools(server, sync) {
|
|
|
4983
5357
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4984
5358
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4985
5359
|
if (!uiTool) {
|
|
4986
|
-
return
|
|
5360
|
+
return fail23(
|
|
4987
5361
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4988
5362
|
);
|
|
4989
5363
|
}
|
|
4990
5364
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4991
|
-
return
|
|
5365
|
+
return fail23(
|
|
4992
5366
|
"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."
|
|
4993
5367
|
);
|
|
4994
5368
|
}
|
|
4995
5369
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4996
5370
|
if (began && began.ok === false) {
|
|
4997
|
-
return
|
|
5371
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4998
5372
|
}
|
|
4999
5373
|
await delay5(SETTLE_MS);
|
|
5000
5374
|
const startedAt = Date.now();
|
|
@@ -5039,7 +5413,7 @@ function registerRenderTools(server, sync) {
|
|
|
5039
5413
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5040
5414
|
const summary = ended?.summary;
|
|
5041
5415
|
if (!summary) {
|
|
5042
|
-
return
|
|
5416
|
+
return fail23(
|
|
5043
5417
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5044
5418
|
);
|
|
5045
5419
|
}
|
|
@@ -5064,17 +5438,17 @@ function registerRenderTools(server, sync) {
|
|
|
5064
5438
|
} else if (!args.compareToPrevious) {
|
|
5065
5439
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5066
5440
|
}
|
|
5067
|
-
return
|
|
5441
|
+
return text21(parts.join("\n"));
|
|
5068
5442
|
} catch (e) {
|
|
5069
|
-
return
|
|
5443
|
+
return fail23(errMsg22(e));
|
|
5070
5444
|
}
|
|
5071
5445
|
}
|
|
5072
5446
|
);
|
|
5073
5447
|
}
|
|
5074
5448
|
|
|
5075
5449
|
// src/verify/editWatcher.ts
|
|
5076
|
-
var
|
|
5077
|
-
var
|
|
5450
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
5451
|
+
var import_node_path4 = __toESM(require("path"));
|
|
5078
5452
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5079
5453
|
".ts",
|
|
5080
5454
|
".tsx",
|
|
@@ -5117,14 +5491,14 @@ var EditWatcher = class {
|
|
|
5117
5491
|
if (mode === "never" || this.started) return;
|
|
5118
5492
|
this.started = true;
|
|
5119
5493
|
try {
|
|
5120
|
-
this.watcher =
|
|
5494
|
+
this.watcher = import_node_fs4.default.watch(
|
|
5121
5495
|
rootDir,
|
|
5122
5496
|
{ recursive: true },
|
|
5123
5497
|
(_event, filename) => {
|
|
5124
5498
|
if (!filename) return;
|
|
5125
5499
|
const rel2 = String(filename);
|
|
5126
|
-
if (rel2.split(
|
|
5127
|
-
if (!WATCHED_EXT.has(
|
|
5500
|
+
if (rel2.split(import_node_path4.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5501
|
+
if (!WATCHED_EXT.has(import_node_path4.default.extname(rel2))) return;
|
|
5128
5502
|
this.noteEdit(rel2);
|
|
5129
5503
|
}
|
|
5130
5504
|
);
|
|
@@ -5224,6 +5598,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
5224
5598
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
5225
5599
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
5226
5600
|
|
|
5601
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5602
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5603
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5604
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5605
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5606
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5607
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5608
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5609
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5610
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5611
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5612
|
+
and no Buoy integration in the app.
|
|
5613
|
+
|
|
5227
5614
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
5228
5615
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
5229
5616
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -5340,6 +5727,7 @@ function createMcpServer(sync) {
|
|
|
5340
5727
|
registerTimeMachineTools(server, sync);
|
|
5341
5728
|
registerScenariosTools(server, sync);
|
|
5342
5729
|
registerScreenshotTools(server, sync);
|
|
5730
|
+
registerCameraTools(server);
|
|
5343
5731
|
registerInteractTools(server, sync);
|
|
5344
5732
|
registerJsThreadTools(server, sync);
|
|
5345
5733
|
registerImagesTools(server, sync);
|
|
@@ -5348,16 +5736,16 @@ function createMcpServer(sync) {
|
|
|
5348
5736
|
return server;
|
|
5349
5737
|
}
|
|
5350
5738
|
var deviceIdField = {
|
|
5351
|
-
deviceId:
|
|
5739
|
+
deviceId: import_zod24.z.string().optional().describe(
|
|
5352
5740
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5353
5741
|
)
|
|
5354
5742
|
};
|
|
5355
5743
|
|
|
5356
5744
|
// src/init.ts
|
|
5357
|
-
var
|
|
5358
|
-
var
|
|
5359
|
-
var
|
|
5360
|
-
var
|
|
5745
|
+
var import_node_child_process3 = require("child_process");
|
|
5746
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5747
|
+
var import_node_os4 = __toESM(require("os"));
|
|
5748
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5361
5749
|
function defaultEnv() {
|
|
5362
5750
|
return { BUOY_VERIFY: "auto" };
|
|
5363
5751
|
}
|
|
@@ -5365,19 +5753,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5365
5753
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5366
5754
|
function runInit(opts = {}) {
|
|
5367
5755
|
const cwd = opts.cwd ?? process.cwd();
|
|
5368
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5756
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path5.default.resolve(__dirname, "../skill");
|
|
5369
5757
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5370
5758
|
const entry = resolveEntry(opts, log);
|
|
5371
5759
|
const done = [];
|
|
5372
|
-
upsertMcpConfig(
|
|
5373
|
-
|
|
5374
|
-
upsertMcpConfig(
|
|
5375
|
-
if (
|
|
5376
|
-
upsertMcpConfig(
|
|
5760
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5761
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.join(cwd, ".cursor"), { recursive: true });
|
|
5762
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5763
|
+
if (import_node_fs5.default.existsSync(import_node_path5.default.join(cwd, ".vscode"))) {
|
|
5764
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5377
5765
|
}
|
|
5378
5766
|
installSkills(skillSrcDir, cwd, done);
|
|
5379
|
-
appendDebuggingGuide(
|
|
5380
|
-
appendDebuggingGuide(
|
|
5767
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5768
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
5381
5769
|
log("Buoy MCP installed:");
|
|
5382
5770
|
for (const line of done) log(" \u2022 " + line);
|
|
5383
5771
|
log("");
|
|
@@ -5419,21 +5807,21 @@ function npxEntry() {
|
|
|
5419
5807
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5420
5808
|
}
|
|
5421
5809
|
function localEntry(opts, log) {
|
|
5422
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5810
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path5.default.join(import_node_os4.default.homedir(), ".buoy-mcp");
|
|
5423
5811
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5424
|
-
const
|
|
5425
|
-
if (
|
|
5426
|
-
return { type: "stdio", command: "node", args: [
|
|
5812
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5813
|
+
if (cliPath2) {
|
|
5814
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5427
5815
|
}
|
|
5428
5816
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5429
5817
|
return npxEntry();
|
|
5430
5818
|
}
|
|
5431
5819
|
function installLocalCopy(installDir, registry, log) {
|
|
5432
5820
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
5433
|
-
|
|
5821
|
+
import_node_fs5.default.mkdirSync(installDir, { recursive: true });
|
|
5434
5822
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5435
5823
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
5436
|
-
const res = (0,
|
|
5824
|
+
const res = (0, import_node_child_process3.spawnSync)(
|
|
5437
5825
|
npm,
|
|
5438
5826
|
[
|
|
5439
5827
|
"install",
|
|
@@ -5453,12 +5841,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5453
5841
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5454
5842
|
return null;
|
|
5455
5843
|
}
|
|
5456
|
-
const
|
|
5457
|
-
if (!
|
|
5458
|
-
log(`\u26A0 install finished but ${
|
|
5844
|
+
const cliPath2 = import_node_path5.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5845
|
+
if (!import_node_fs5.default.existsSync(cliPath2)) {
|
|
5846
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5459
5847
|
return null;
|
|
5460
5848
|
}
|
|
5461
|
-
return
|
|
5849
|
+
return cliPath2;
|
|
5462
5850
|
}
|
|
5463
5851
|
function probeRegistry() {
|
|
5464
5852
|
const registry = effectiveRegistry();
|
|
@@ -5476,14 +5864,14 @@ function effectiveRegistry() {
|
|
|
5476
5864
|
}
|
|
5477
5865
|
function npmConfigGet(key) {
|
|
5478
5866
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5479
|
-
const res = (0,
|
|
5867
|
+
const res = (0, import_node_child_process3.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
|
|
5480
5868
|
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
5481
5869
|
const val = res.stdout.trim();
|
|
5482
5870
|
if (!val || val === "undefined" || val === "null") return null;
|
|
5483
5871
|
return val;
|
|
5484
5872
|
}
|
|
5485
5873
|
function hostReachable(registry) {
|
|
5486
|
-
const done = (0,
|
|
5874
|
+
const done = (0, import_node_child_process3.spawnSync)(
|
|
5487
5875
|
process.execPath,
|
|
5488
5876
|
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS3)],
|
|
5489
5877
|
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 + 2e3 }
|
|
@@ -5506,22 +5894,22 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
5506
5894
|
`;
|
|
5507
5895
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
5508
5896
|
let json3 = {};
|
|
5509
|
-
if (
|
|
5897
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5510
5898
|
try {
|
|
5511
|
-
json3 = JSON.parse(
|
|
5899
|
+
json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
|
|
5512
5900
|
} catch {
|
|
5513
|
-
|
|
5901
|
+
import_node_fs5.default.copyFileSync(file, file + ".bak");
|
|
5514
5902
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
5515
5903
|
json3 = {};
|
|
5516
5904
|
}
|
|
5517
5905
|
} else {
|
|
5518
|
-
|
|
5906
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
|
|
5519
5907
|
}
|
|
5520
5908
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5521
5909
|
const existed = "buoy" in servers;
|
|
5522
5910
|
servers.buoy = entry;
|
|
5523
5911
|
json3[key] = servers;
|
|
5524
|
-
|
|
5912
|
+
import_node_fs5.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
5525
5913
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
5526
5914
|
}
|
|
5527
5915
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
@@ -5581,9 +5969,9 @@ mentions it.
|
|
|
5581
5969
|
`;
|
|
5582
5970
|
function appendDebuggingGuide(file, cwd, done) {
|
|
5583
5971
|
let existing = "";
|
|
5584
|
-
if (
|
|
5972
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5585
5973
|
try {
|
|
5586
|
-
existing =
|
|
5974
|
+
existing = import_node_fs5.default.readFileSync(file, "utf8");
|
|
5587
5975
|
} catch {
|
|
5588
5976
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5589
5977
|
return;
|
|
@@ -5595,40 +5983,40 @@ function appendDebuggingGuide(file, cwd, done) {
|
|
|
5595
5983
|
}
|
|
5596
5984
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
5597
5985
|
try {
|
|
5598
|
-
|
|
5986
|
+
import_node_fs5.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
5599
5987
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
5600
5988
|
} catch {
|
|
5601
5989
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5602
5990
|
}
|
|
5603
5991
|
}
|
|
5604
5992
|
function installSkills(skillSrcDir, cwd, done) {
|
|
5605
|
-
if (!
|
|
5993
|
+
if (!import_node_fs5.default.existsSync(skillSrcDir)) {
|
|
5606
5994
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5607
5995
|
return;
|
|
5608
5996
|
}
|
|
5609
|
-
const skills =
|
|
5997
|
+
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")));
|
|
5610
5998
|
if (skills.length === 0) {
|
|
5611
5999
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5612
6000
|
return;
|
|
5613
6001
|
}
|
|
5614
6002
|
for (const skill of skills) {
|
|
5615
|
-
const from =
|
|
5616
|
-
const to =
|
|
6003
|
+
const from = import_node_path5.default.join(skillSrcDir, skill.name);
|
|
6004
|
+
const to = import_node_path5.default.join(cwd, ".claude", "skills", skill.name);
|
|
5617
6005
|
copyDir(from, to);
|
|
5618
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6006
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path5.default.join(to, "SKILL.md"), cwd)}`);
|
|
5619
6007
|
}
|
|
5620
6008
|
}
|
|
5621
6009
|
function copyDir(from, to) {
|
|
5622
|
-
|
|
5623
|
-
for (const entry of
|
|
5624
|
-
const s =
|
|
5625
|
-
const d =
|
|
6010
|
+
import_node_fs5.default.mkdirSync(to, { recursive: true });
|
|
6011
|
+
for (const entry of import_node_fs5.default.readdirSync(from, { withFileTypes: true })) {
|
|
6012
|
+
const s = import_node_path5.default.join(from, entry.name);
|
|
6013
|
+
const d = import_node_path5.default.join(to, entry.name);
|
|
5626
6014
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5627
|
-
else
|
|
6015
|
+
else import_node_fs5.default.copyFileSync(s, d);
|
|
5628
6016
|
}
|
|
5629
6017
|
}
|
|
5630
6018
|
function rel(file, cwd) {
|
|
5631
|
-
const r =
|
|
6019
|
+
const r = import_node_path5.default.relative(cwd, file);
|
|
5632
6020
|
return r && !r.startsWith("..") ? r : file;
|
|
5633
6021
|
}
|
|
5634
6022
|
|