@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/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 import_zod23 = require("zod");
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.31" ? "7.0.31" : "0.0.0-dev";
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 resolved = null;
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
- resolved = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
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
- resolved = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1449
+ resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1445
1450
  break;
1446
1451
  }
1447
1452
  if (!sawActive && Date.now() - start > 4e3) {
1448
- resolved = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
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 (!resolved) return fail2("No automation batch completed within the wait window.");
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 ?? [], resolved.batchId, void 0, resolved.failures);
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, resolved, omitted) => {
1792
- const value = inline ?? resolved;
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(candidates) {
3374
- if (!candidates || candidates.length === 0) return "";
3375
- const lines = candidates.map((c) => {
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/interact.ts
3479
+ // src/tools/packs/camera.ts
3475
3480
  var import_zod15 = require("zod");
3476
- var TOOL_ID2 = "highlight-updates";
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: import_zod15.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3506
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 text13(
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 text13(`${header}
3895
+ return text14(`${header}
3522
3896
 
3523
3897
  ${elements.map(formatElement).join("\n")}`);
3524
3898
  } catch (e) {
3525
- return fail15(errMsg15(e));
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: import_zod15.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3535
- testID: import_zod15.z.string().optional().describe("Exact testID of the element."),
3536
- query: import_zod15.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3537
- value: import_zod15.z.union([import_zod15.z.number(), import_zod15.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)."),
3538
- text: import_zod15.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3539
- longPress: import_zod15.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3540
- scrollIntoView: import_zod15.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3541
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
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 text13(`${verb}${scrolledNote}.${flowTip}`);
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 fail15(msg);
3952
+ return fail16(msg);
3579
3953
  } catch (e) {
3580
- return fail15(errMsg15(e));
3954
+ return fail16(errMsg15(e));
3581
3955
  }
3582
3956
  }
3583
3957
  );
3584
3958
  }
3585
3959
 
3586
3960
  // src/tools/packs/jsThread.ts
3587
- var import_zod16 = require("zod");
3961
+ var import_zod17 = require("zod");
3588
3962
  var JS_TOP_TOOL_ID = "js-top";
3589
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3590
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
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: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3663
- durationMs: import_zod16.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3664
- clearFirst: import_zod16.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
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 fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
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 text14(`${md}
4070
+ return text15(`${md}
3697
4071
 
3698
4072
  \`\`\`json
3699
4073
  ${JSON.stringify(slim)}
3700
4074
  \`\`\``);
3701
4075
  } catch (e) {
3702
- return fail16(errMsg16(e));
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: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3712
- key: import_zod16.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
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 fail16(
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 text14(`${md}
4104
+ return text15(`${md}
3731
4105
 
3732
4106
  \`\`\`json
3733
4107
  ${JSON.stringify(detail)}
3734
4108
  \`\`\``);
3735
4109
  } catch (e) {
3736
- return fail16(errMsg16(e));
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: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 text14("Cleared js-top stats.");
4126
+ return text15("Cleared js-top stats.");
3753
4127
  } catch (e) {
3754
- return fail16(errMsg16(e));
4128
+ return fail17(errMsg16(e));
3755
4129
  }
3756
4130
  }
3757
4131
  );
3758
4132
  }
3759
4133
 
3760
4134
  // src/tools/packs/images.ts
3761
- var import_zod17 = require("zod");
4135
+ var import_zod18 = require("zod");
3762
4136
  var IMAGES_TOOL_ID = "images";
3763
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3764
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
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: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3854
- limit: import_zod17.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3855
- status: import_zod17.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
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 fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
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 text15(`${md}
4269
+ return text16(`${md}
3896
4270
 
3897
4271
  \`\`\`json
3898
4272
  ${JSON.stringify(slim)}
3899
4273
  \`\`\``);
3900
4274
  } catch (e) {
3901
- return fail17(errMsg17(e));
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: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3911
- id: import_zod17.z.number().describe("Record id from get_images."),
3912
- action: import_zod17.z.enum([
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: import_zod17.z.string().optional().describe("Replacement URL (swap_url only).")
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 fail17("Device returned nothing for proveSavings.");
3933
- if (!savings.ok) return fail17(savings.message);
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 text15(parts.join("\n"));
4319
+ return text16(parts.join("\n"));
3946
4320
  }
3947
4321
  const entry = ACTION_MAP[action];
3948
- if (!entry) return fail17(`Unknown action: ${action}`);
3949
- if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
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 text15(lines.join("\n"));
4343
+ return text16(lines.join("\n"));
3970
4344
  } catch (e) {
3971
- return fail17(errMsg17(e));
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: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3981
- network: import_zod17.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3982
- blankImages: import_zod17.z.boolean().optional().describe("Render all images without a source."),
3983
- massAction: import_zod17.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."),
3984
- clearRegistry: import_zod17.z.boolean().optional().describe("Clear the captured image log."),
3985
- clearExpoCaches: import_zod17.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
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 fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4013
- return text15(`Applied: ${done.join(" \xB7 ")}`);
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 fail17(errMsg17(e));
4389
+ return fail18(errMsg17(e));
4016
4390
  }
4017
4391
  }
4018
4392
  );
4019
4393
  }
4020
4394
 
4021
4395
  // src/tools/packs/assets.ts
4022
- var import_zod18 = require("zod");
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 text16 = (s) => ({ content: [{ type: "text", text: s }] });
4038
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
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: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4128
- limit: import_zod18.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4129
- kind: import_zod18.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4130
- unusedOnly: import_zod18.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
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 fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
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 text16(`${md}
4541
+ return text17(`${md}
4168
4542
 
4169
4543
  \`\`\`json
4170
4544
  ${JSON.stringify(slim)}
4171
4545
  \`\`\``);
4172
4546
  } catch (e) {
4173
- return fail18(errMsg18(e));
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: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4183
- action: import_zod18.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4184
- id: import_zod18.z.number().optional().describe("Asset id from get_assets (detail only).")
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 fail18("Device returned nothing for rescan.");
4195
- return text16(
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 text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4575
+ return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4202
4576
  }
4203
4577
  case "detail": {
4204
- if (id == null) return fail18("detail requires the `id` param (from get_assets).");
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 fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
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 text16(lines.join("\n"));
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 text16(
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 text16("Baseline cleared.");
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 text16(result?.message ?? "Inventory cleared.");
4612
+ return text17(result?.message ?? "Inventory cleared.");
4239
4613
  }
4240
4614
  }
4241
4615
  } catch (e) {
4242
- return fail18(errMsg18(e));
4616
+ return fail19(errMsg18(e));
4243
4617
  }
4244
4618
  }
4245
4619
  );
4246
4620
  }
4247
4621
 
4248
4622
  // src/tools/packs/reload.ts
4249
- var import_zod19 = require("zod");
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 text17 = (s) => ({ content: [{ type: "text", text: s }] });
4313
- var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
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: import_zod19.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
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: import_zod19.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4357
- timeoutMs: import_zod19.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4358
- deviceId: import_zod19.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
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 text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
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 fail19(
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 text17(
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 fail19(errMsg19(e));
4786
+ return fail20(errMsg19(e));
4413
4787
  }
4414
4788
  }
4415
4789
  );
4416
4790
  }
4417
4791
 
4418
4792
  // src/tools/packs/triage.ts
4419
- var import_zod20 = require("zod");
4420
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4421
- var fail20 = (message) => ({
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, run) {
4830
+ async function section(title, run2) {
4457
4831
  try {
4458
- const body = await run();
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: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 text18(
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 fail20(errMsg20(e));
4966
+ return fail21(errMsg20(e));
4593
4967
  }
4594
4968
  }
4595
4969
  );
4596
4970
  }
4597
4971
 
4598
4972
  // src/tools/packs/flow.ts
4599
- var import_zod21 = require("zod");
4973
+ var import_zod22 = require("zod");
4600
4974
  var UI_TOOL_ID = "highlight-updates";
4601
4975
  var APP_TOOL_ID2 = "app";
4602
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4603
- var fail21 = (message) => ({
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 = import_zod21.z.object({
4625
- tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4626
- longPress: import_zod21.z.boolean().optional().describe("Make `tap` a long-press instead."),
4627
- type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
4628
- into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4629
- set: import_zod21.z.union([import_zod21.z.number(), import_zod21.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4630
- reload: import_zod21.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4631
- waitMs: import_zod21.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4632
- waitFor: import_zod21.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4633
- waitGone: import_zod21.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4634
- expect: import_zod21.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4635
- expectGone: import_zod21.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4636
- timeoutMs: import_zod21.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
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: import_zod21.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4645
- include: import_zod21.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."),
4646
- continueOnFailure: import_zod21.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4647
- deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 text19(parts.join("\n\n"));
5245
+ return text20(parts.join("\n\n"));
4872
5246
  } catch (e) {
4873
- return fail21(errMsg21(e));
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 import_zod22 = require("zod");
5268
+ var import_zod23 = require("zod");
4895
5269
  var UI_TOOL_ID2 = "highlight-updates";
4896
- var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4897
- var fail22 = (message) => ({
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 = import_zod22.z.object({
4908
- tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4909
- type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
4910
- into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4911
- waitMs: import_zod22.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4912
- repeat: import_zod22.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
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: import_zod22.z.array(stepSchema2).max(20).optional().describe(
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: import_zod22.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5009
- label: import_zod22.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5010
- compareToPrevious: import_zod22.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5011
- top: import_zod22.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5012
- deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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 fail22(
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 fail22(
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 fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
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 fail22(
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 text20(parts.join("\n"));
5476
+ return text21(parts.join("\n"));
5103
5477
  } catch (e) {
5104
- return fail22(errMsg22(e));
5478
+ return fail23(errMsg22(e));
5105
5479
  }
5106
5480
  }
5107
5481
  );
5108
5482
  }
5109
5483
 
5110
5484
  // src/verify/editWatcher.ts
5111
- var import_node_fs3 = __toESM(require("fs"));
5112
- var import_node_path3 = __toESM(require("path"));
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 = import_node_fs3.default.watch(
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(import_node_path3.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
5162
- if (!WATCHED_EXT.has(import_node_path3.default.extname(rel2))) return;
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: import_zod23.z.string().optional().describe(
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 import_node_child_process2 = require("child_process");
5393
- var import_node_fs4 = __toESM(require("fs"));
5394
- var import_node_os3 = __toESM(require("os"));
5395
- var import_node_path4 = __toESM(require("path"));
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 ?? import_node_path4.default.resolve(__dirname, "../skill");
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(import_node_path4.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
5408
- import_node_fs4.default.mkdirSync(import_node_path4.default.join(cwd, ".cursor"), { recursive: true });
5409
- upsertMcpConfig(import_node_path4.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
5410
- if (import_node_fs4.default.existsSync(import_node_path4.default.join(cwd, ".vscode"))) {
5411
- upsertMcpConfig(import_node_path4.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
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(import_node_path4.default.join(cwd, "CLAUDE.md"), cwd, done);
5415
- appendDebuggingGuide(import_node_path4.default.join(cwd, "AGENTS.md"), cwd, done);
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 ?? import_node_path4.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
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 cliPath = installLocalCopy(installDir, registry, log);
5460
- if (cliPath) {
5461
- return { type: "stdio", command: "node", args: [cliPath], env: defaultEnv() };
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
- import_node_fs4.default.mkdirSync(installDir, { recursive: true });
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, import_node_child_process2.spawnSync)(
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 cliPath = import_node_path4.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
5492
- if (!import_node_fs4.default.existsSync(cliPath)) {
5493
- log(`\u26A0 install finished but ${cliPath} is missing.`);
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 cliPath;
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, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
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, import_node_child_process2.spawnSync)(
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 (import_node_fs4.default.existsSync(file)) {
5932
+ if (import_node_fs5.default.existsSync(file)) {
5545
5933
  try {
5546
- json3 = JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
5934
+ json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
5547
5935
  } catch {
5548
- import_node_fs4.default.copyFileSync(file, file + ".bak");
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
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
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
- import_node_fs4.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
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 (import_node_fs4.default.existsSync(file)) {
6007
+ if (import_node_fs5.default.existsSync(file)) {
5620
6008
  try {
5621
- existing = import_node_fs4.default.readFileSync(file, "utf8");
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
- import_node_fs4.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
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 (!import_node_fs4.default.existsSync(skillSrcDir)) {
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 = import_node_fs4.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs4.default.existsSync(import_node_path4.default.join(skillSrcDir, e.name, "SKILL.md")));
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 = import_node_path4.default.join(skillSrcDir, skill.name);
5651
- const to = import_node_path4.default.join(cwd, ".claude", "skills", skill.name);
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(import_node_path4.default.join(to, "SKILL.md"), cwd)}`);
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
- import_node_fs4.default.mkdirSync(to, { recursive: true });
5658
- for (const entry of import_node_fs4.default.readdirSync(from, { withFileTypes: true })) {
5659
- const s = import_node_path4.default.join(from, entry.name);
5660
- const d = import_node_path4.default.join(to, entry.name);
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 import_node_fs4.default.copyFileSync(s, d);
6050
+ else import_node_fs5.default.copyFileSync(s, d);
5663
6051
  }
5664
6052
  }
5665
6053
  function rel(file, cwd) {
5666
- const r = import_node_path4.default.relative(cwd, file);
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: