@buoy-gg/mcp 7.0.29 → 7.0.31

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.
@@ -281,7 +281,7 @@ var SyncClient = class {
281
281
  };
282
282
  const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
283
283
  dev2
284
- )} Upgrade to Buoy Pro in the app to enable MCP access (run \`list_devices\` to check status).`;
284
+ )} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;
285
285
  if (deviceId === ALL_DEVICES) {
286
286
  if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
287
287
  return ALL_DEVICES;
@@ -613,7 +613,7 @@ async function waitForBroker(url, attempts = 20) {
613
613
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
614
614
 
615
615
  // src/version.ts
616
- var VERSION = "7.0.29" ? "7.0.29" : "0.0.0-dev";
616
+ var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
617
617
  var PACKAGE_NAME = "@buoy-gg/mcp";
618
618
 
619
619
  // src/update/checkForUpdate.ts
@@ -714,7 +714,9 @@ function registerGenericTools(server, sync) {
714
714
  return json({
715
715
  devices,
716
716
  ...anyPro ? {} : {
717
- hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
717
+ // A headless build has no on-device UI, so "upgrade in the app"
718
+ // cannot be followed there — name the `licenseKey` prop too.
719
+ hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Enter a license in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless (no on-device UI)."
718
720
  },
719
721
  ...updateNotice ? { updateAvailable: updateNotice } : {}
720
722
  });
@@ -2460,7 +2462,7 @@ function registerScreenshotTools(server, sync) {
2460
2462
 
2461
2463
  // src/server.ts
2462
2464
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2463
- import { z as z22 } from "zod";
2465
+ import { z as z23 } from "zod";
2464
2466
 
2465
2467
  // src/tools/packs/network.ts
2466
2468
  import { z as z12 } from "zod";
@@ -3109,15 +3111,310 @@ ${lines.join("\n")}`);
3109
3111
  );
3110
3112
  }
3111
3113
 
3112
- // src/tools/packs/interact.ts
3114
+ // src/tools/packs/scenarios.ts
3113
3115
  import { z as z14 } from "zod";
3114
- var TOOL_ID2 = "highlight-updates";
3116
+ var SCENARIOS_TOOL_ID = "scenarios";
3115
3117
  var text12 = (s) => ({ content: [{ type: "text", text: s }] });
3116
3118
  var fail14 = (message) => ({
3117
3119
  isError: true,
3118
3120
  content: [{ type: "text", text: message }]
3119
3121
  });
3120
3122
  var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3123
+ function varSpec(v) {
3124
+ const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
3125
+ const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
3126
+ return `${v.key}: ${v.type}${opts}${def}`;
3127
+ }
3128
+ function summarize(s) {
3129
+ const vars = s.vars?.length ? `
3130
+ variables: ${s.vars.map(varSpec).join("; ")}` : "";
3131
+ const steps = s.stepLines?.length ? `
3132
+ ${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
3133
+ const meta = [
3134
+ `v${s.version}`,
3135
+ s.author,
3136
+ s.usage?.runs ? `${s.usage.runs} run(s)` : null,
3137
+ s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
3138
+ s.unsavedToRepo ? "unsaved to repo" : null
3139
+ ].filter(Boolean).join(" \xB7 ");
3140
+ const outcome = s.expectedOutcome ? `
3141
+ expected: ${s.expectedOutcome}` : "";
3142
+ return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
3143
+ }
3144
+ function renderSection(title, list, folders) {
3145
+ if (!list.length) return `**${title} (0):**
3146
+ _none_`;
3147
+ if (!folders.length) {
3148
+ return `**${title} (${list.length}):**
3149
+ ${list.map(summarize).join("\n")}`;
3150
+ }
3151
+ const parts = [`**${title} (${list.length}):**`];
3152
+ for (const folder of folders) {
3153
+ const inIt = list.filter(
3154
+ (s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
3155
+ );
3156
+ if (!inIt.length) continue;
3157
+ parts.push(`_${folder} (${inIt.length})_
3158
+ ${inIt.map(summarize).join("\n")}`);
3159
+ }
3160
+ const ungrouped = list.filter((s) => !s.folder);
3161
+ if (ungrouped.length) {
3162
+ parts.push(
3163
+ `_Ungrouped (${ungrouped.length})_
3164
+ ${ungrouped.map(summarize).join("\n")}`
3165
+ );
3166
+ }
3167
+ return parts.join("\n");
3168
+ }
3169
+ function formatActive(a) {
3170
+ const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
3171
+ return [
3172
+ `**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
3173
+ vars ? `variables: ${vars}` : null,
3174
+ `effects:
3175
+ ${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
3176
+ ].filter(Boolean).join("\n");
3177
+ }
3178
+ function registerScenariosTools(server, sync) {
3179
+ server.registerTool(
3180
+ "get_scenarios",
3181
+ {
3182
+ description: "List the app's saved Scenarios \u2014 named, parameterized app states that can be applied with one call (override a response, write storage, impersonate a user, navigate). Shows what each one WILL do in plain English with a durability label per step, plus which scenario is currently active and what it changed. Use scenario_action to run one as a deterministic fixture BEFORE driving a flow, so the flow starts from a known state instead of whatever the app happened to be in. If a scenario is active, whatever you observe on the device is partly simulated \u2014 check here first when the app's data looks wrong.",
3183
+ inputSchema: {
3184
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3185
+ }
3186
+ },
3187
+ async (args) => {
3188
+ try {
3189
+ const target = sync.requireProDevice(args.deviceId);
3190
+ const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
3191
+ const p = payload ?? {};
3192
+ const code = p.code ?? [];
3193
+ const device = p.device ?? [];
3194
+ const drafts = p.drafts ?? [];
3195
+ const folders = p.folders ?? [];
3196
+ const parts = [];
3197
+ parts.push(
3198
+ p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
3199
+ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
3200
+ );
3201
+ if (folders.length) {
3202
+ parts.push(
3203
+ `**Folders:** ${folders.join(" \xB7 ")} \u2014 the flows this team organizes by. Scenarios below are grouped under them; file new ones into an existing folder with \`scenario_action setFolder\` rather than inventing a name.`
3204
+ );
3205
+ }
3206
+ parts.push(renderSection("Shipped with this app", code, folders));
3207
+ parts.push(renderSection("Saved on this device", device, folders));
3208
+ if (drafts.length) {
3209
+ parts.push(
3210
+ `**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
3211
+ ${drafts.map(summarize).join("\n")}`
3212
+ );
3213
+ }
3214
+ return text12(parts.join("\n\n"));
3215
+ } catch (e) {
3216
+ return fail14(errMsg14(e));
3217
+ }
3218
+ }
3219
+ );
3220
+ server.registerTool(
3221
+ "scenario_action",
3222
+ {
3223
+ description: `Drive Buoy Scenarios: 'preview' dry-runs one and reports exactly what it WOULD change (plus any conflict with the active scenario) without touching the app \u2014 do this before running an unfamiliar scenario; 'run' applies it and resolves only once every step has landed, which makes it a deterministic fixture to call before run_flow; 'deactivate' reverses it and reports what it could NOT restore (storage writes have no automatic inverse); 'save' writes a new scenario onto the device \u2014 it lands as a DRAFT that a human must accept on the device before it can run, so tell the user to go accept it; 'setFolder' files a saved scenario under a flow ("Checkout", "Login") or clears its folder, which is how a QA library stays navigable \u2014 check get_scenarios for the folders already in use and reuse one instead of inventing a near-duplicate; 'delete' removes a device scenario. Prefer a network override step over a query-cache poke: the override survives refetch and restart, the poke dies on the next fetch.`,
3224
+ inputSchema: {
3225
+ action: z14.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3226
+ folder: z14.string().optional().describe(
3227
+ "Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."
3228
+ ),
3229
+ id: z14.string().optional().describe(
3230
+ "Scenario id (required for preview/run/delete; from get_scenarios)."
3231
+ ),
3232
+ params: z14.record(z14.union([z14.string(), z14.number(), z14.boolean()])).optional().describe(
3233
+ "Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
3234
+ ),
3235
+ scenario: z14.object({
3236
+ id: z14.string().describe("Stable kebab-case id."),
3237
+ name: z14.string().describe("Human name; may contain {{variable}} placeholders."),
3238
+ description: z14.string().optional(),
3239
+ expectedOutcome: z14.string().optional().describe(
3240
+ "What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
3241
+ ),
3242
+ author: z14.string().optional(),
3243
+ authoredAgainst: z14.string().optional().describe("App version this was written against."),
3244
+ folder: z14.string().optional().describe(
3245
+ 'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
3246
+ ),
3247
+ tags: z14.array(z14.string()).optional(),
3248
+ vars: z14.array(
3249
+ z14.object({
3250
+ key: z14.string(),
3251
+ label: z14.string().optional(),
3252
+ type: z14.enum(["string", "number", "boolean", "enum"]),
3253
+ options: z14.array(z14.union([z14.string(), z14.number(), z14.boolean()])).optional(),
3254
+ default: z14.union([z14.string(), z14.number(), z14.boolean()]).optional()
3255
+ })
3256
+ ).optional().describe(
3257
+ "Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."
3258
+ ),
3259
+ steps: z14.array(
3260
+ z14.object({
3261
+ id: z14.string().optional(),
3262
+ tool: z14.string().describe(
3263
+ "Tool id: network | storage | impersonate | route-events | query | time-machine"
3264
+ ),
3265
+ action: z14.string().describe(
3266
+ "Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
3267
+ ),
3268
+ params: z14.unknown().optional(),
3269
+ label: z14.string().optional()
3270
+ })
3271
+ ).describe("Steps, in order."),
3272
+ undoSteps: z14.array(
3273
+ z14.object({
3274
+ id: z14.string().optional(),
3275
+ tool: z14.string(),
3276
+ action: z14.string(),
3277
+ params: z14.unknown().optional(),
3278
+ label: z14.string().optional()
3279
+ })
3280
+ ).optional().describe(
3281
+ "How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored."
3282
+ )
3283
+ }).optional().describe("The scenario to save (action 'save')."),
3284
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3285
+ }
3286
+ },
3287
+ async (args) => {
3288
+ try {
3289
+ const target = sync.requireProDevice(args.deviceId);
3290
+ const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
3291
+ switch (args.action) {
3292
+ case "preview": {
3293
+ if (!args.id) return fail14("preview requires `id` (from get_scenarios).");
3294
+ const r = await call("preview", {
3295
+ id: args.id,
3296
+ params: args.params ?? {}
3297
+ });
3298
+ const lines = [
3299
+ `**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
3300
+ `variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
3301
+ `**Would do:**
3302
+ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
3303
+ ];
3304
+ if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
3305
+ if (r.conflict) {
3306
+ lines.push(
3307
+ `\u26A0\uFE0F **Conflict:** "${r.conflict.activeName}" is active and touches the same target(s): ${r.conflict.overlaps.join(", ")}. Running will SWAP \u2014 it deactivates first.`
3308
+ );
3309
+ }
3310
+ if (r.warnings?.length) lines.push(`**Warnings:**
3311
+ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
3312
+ lines.push(
3313
+ r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
3314
+ ${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
3315
+ );
3316
+ return text12(lines.join("\n\n"));
3317
+ }
3318
+ case "run": {
3319
+ if (!args.id) return fail14("run requires `id` (from get_scenarios).");
3320
+ const r = await call("run", {
3321
+ id: args.id,
3322
+ params: args.params ?? {}
3323
+ });
3324
+ if (r.preflightErrors?.length) {
3325
+ return fail14(
3326
+ `Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
3327
+ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3328
+ );
3329
+ }
3330
+ const steps = (r.steps ?? []).map((s) => {
3331
+ const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
3332
+ const err = s.error ? `
3333
+ ${s.error}` : "";
3334
+ return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
3335
+ }).join("\n");
3336
+ if (!r.ok) {
3337
+ return fail14(
3338
+ `Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
3339
+ ${steps}
3340
+
3341
+ The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
3342
+ );
3343
+ }
3344
+ return text12(
3345
+ [
3346
+ `\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
3347
+ steps,
3348
+ `**Now bent:**
3349
+ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
3350
+ "The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
3351
+ ].join("\n\n")
3352
+ );
3353
+ }
3354
+ case "deactivate": {
3355
+ const r = await call("deactivate", {});
3356
+ const rep = r.report;
3357
+ const lines = [
3358
+ `Removed ${rep.removedRules} override rule(s)`,
3359
+ rep.stoppedImpersonation ? "Ended impersonation" : null,
3360
+ rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
3361
+ ].filter(Boolean);
3362
+ const warn = rep.unrestoredStorageKeys.length ? `
3363
+
3364
+ \u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
3365
+ const errs = rep.errors.length ? `
3366
+
3367
+ Errors: ${rep.errors.join("; ")}` : "";
3368
+ return text12(`**Deactivated.**
3369
+ ${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
3370
+ }
3371
+ case "save": {
3372
+ if (!args.scenario) return fail14("save requires `scenario`.");
3373
+ const r = await call("save", { scenario: args.scenario });
3374
+ if (!r.ok) {
3375
+ return fail14(
3376
+ `Save rejected:
3377
+ ${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
3378
+ );
3379
+ }
3380
+ return text12(
3381
+ `Saved \`${r.id}\` **as a draft**.
3382
+
3383
+ It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`
3384
+ );
3385
+ }
3386
+ case "setFolder": {
3387
+ if (!args.id) return fail14("setFolder requires `id` (from get_scenarios).");
3388
+ const r = await call("setFolder", {
3389
+ id: args.id,
3390
+ folder: args.folder
3391
+ });
3392
+ return text12(
3393
+ r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
3394
+ );
3395
+ }
3396
+ case "delete": {
3397
+ if (!args.id) return fail14("delete requires `id`.");
3398
+ await call("delete", { id: args.id });
3399
+ return text12(`Deleted \`${args.id}\` from the device library.`);
3400
+ }
3401
+ }
3402
+ } catch (e) {
3403
+ return fail14(errMsg14(e));
3404
+ }
3405
+ }
3406
+ );
3407
+ }
3408
+
3409
+ // src/tools/packs/interact.ts
3410
+ import { z as z15 } from "zod";
3411
+ var TOOL_ID2 = "highlight-updates";
3412
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3413
+ var fail15 = (message) => ({
3414
+ isError: true,
3415
+ content: [{ type: "text", text: message }]
3416
+ });
3417
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3121
3418
  function formatElement(e) {
3122
3419
  let line = e.interactive ? "\u2022 " : " ";
3123
3420
  line += e.name;
@@ -3140,8 +3437,8 @@ function registerInteractTools(server, sync) {
3140
3437
  {
3141
3438
  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.",
3142
3439
  inputSchema: {
3143
- interactiveOnly: z14.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3144
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3440
+ interactiveOnly: z15.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3441
+ deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
3145
3442
  }
3146
3443
  },
3147
3444
  async (args) => {
@@ -3151,16 +3448,16 @@ function registerInteractTools(server, sync) {
3151
3448
  const all = res?.elements ?? [];
3152
3449
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3153
3450
  if (elements.length === 0) {
3154
- return text12(
3451
+ return text13(
3155
3452
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
3156
3453
  );
3157
3454
  }
3158
3455
  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 }).`;
3159
- return text12(`${header}
3456
+ return text13(`${header}
3160
3457
 
3161
3458
  ${elements.map(formatElement).join("\n")}`);
3162
3459
  } catch (e) {
3163
- return fail14(errMsg14(e));
3460
+ return fail15(errMsg15(e));
3164
3461
  }
3165
3462
  }
3166
3463
  );
@@ -3169,20 +3466,20 @@ ${elements.map(formatElement).join("\n")}`);
3169
3466
  {
3170
3467
  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.",
3171
3468
  inputSchema: {
3172
- nativeTag: z14.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3173
- testID: z14.string().optional().describe("Exact testID of the element."),
3174
- query: z14.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3175
- value: z14.union([z14.number(), z14.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
3176
- text: z14.string().optional().describe("For a text input: the string to set via onChangeText."),
3177
- longPress: z14.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3178
- scrollIntoView: z14.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3179
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3469
+ nativeTag: z15.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3470
+ testID: z15.string().optional().describe("Exact testID of the element."),
3471
+ query: z15.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3472
+ value: z15.union([z15.number(), z15.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
3473
+ text: z15.string().optional().describe("For a text input: the string to set via onChangeText."),
3474
+ longPress: z15.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3475
+ scrollIntoView: z15.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3476
+ deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
3180
3477
  }
3181
3478
  },
3182
3479
  async (args) => {
3183
3480
  try {
3184
3481
  if (args.nativeTag == null && !args.testID && !args.query) {
3185
- return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
3482
+ return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
3186
3483
  }
3187
3484
  const target = sync.requireProDevice(args.deviceId);
3188
3485
  const res = await sync.callAction(
@@ -3207,26 +3504,26 @@ ${elements.map(formatElement).join("\n")}`);
3207
3504
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3208
3505
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3209
3506
  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}`;
3210
- return text12(`${verb}${scrolledNote}.${flowTip}`);
3507
+ return text13(`${verb}${scrolledNote}.${flowTip}`);
3211
3508
  }
3212
3509
  let msg = res?.reason ?? "Tap failed.";
3213
3510
  if (res?.candidates?.length) {
3214
3511
  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");
3215
3512
  }
3216
- return fail14(msg);
3513
+ return fail15(msg);
3217
3514
  } catch (e) {
3218
- return fail14(errMsg14(e));
3515
+ return fail15(errMsg15(e));
3219
3516
  }
3220
3517
  }
3221
3518
  );
3222
3519
  }
3223
3520
 
3224
3521
  // src/tools/packs/jsThread.ts
3225
- import { z as z15 } from "zod";
3522
+ import { z as z16 } from "zod";
3226
3523
  var JS_TOP_TOOL_ID = "js-top";
3227
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3228
- var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3229
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3524
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3525
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3526
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3230
3527
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3231
3528
  function sparkline(values) {
3232
3529
  const max = Math.max(1, ...values);
@@ -3297,9 +3594,9 @@ function registerJsThreadTools(server, sync) {
3297
3594
  {
3298
3595
  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.',
3299
3596
  inputSchema: {
3300
- deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
3301
- durationMs: z15.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3302
- clearFirst: z15.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3597
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3598
+ durationMs: z16.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3599
+ clearFirst: z16.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3303
3600
  }
3304
3601
  },
3305
3602
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -3312,7 +3609,7 @@ function registerJsThreadTools(server, sync) {
3312
3609
  { durationMs: duration, clearFirst: clearFirst === true },
3313
3610
  target
3314
3611
  );
3315
- if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3612
+ if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3316
3613
  const md = formatTopMarkdown(snap, duration);
3317
3614
  const slim = {
3318
3615
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -3331,13 +3628,13 @@ function registerJsThreadTools(server, sync) {
3331
3628
  worstFreezeMs: r.worstFreezeMs
3332
3629
  }))
3333
3630
  };
3334
- return text13(`${md}
3631
+ return text14(`${md}
3335
3632
 
3336
3633
  \`\`\`json
3337
3634
  ${JSON.stringify(slim)}
3338
3635
  \`\`\``);
3339
3636
  } catch (e) {
3340
- return fail15(errMsg15(e));
3637
+ return fail16(errMsg16(e));
3341
3638
  }
3342
3639
  }
3343
3640
  );
@@ -3346,8 +3643,8 @@ ${JSON.stringify(slim)}
3346
3643
  {
3347
3644
  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).',
3348
3645
  inputSchema: {
3349
- deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
3350
- key: z15.string().describe("Origin key from get_js_thread_top's JSON rows.")
3646
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3647
+ key: z16.string().describe("Origin key from get_js_thread_top's JSON rows.")
3351
3648
  }
3352
3649
  },
3353
3650
  async ({ deviceId, key }) => {
@@ -3360,18 +3657,18 @@ ${JSON.stringify(slim)}
3360
3657
  target
3361
3658
  );
3362
3659
  if (!detail) {
3363
- return fail15(
3660
+ return fail16(
3364
3661
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
3365
3662
  );
3366
3663
  }
3367
3664
  const md = formatDetailMarkdown(detail);
3368
- return text13(`${md}
3665
+ return text14(`${md}
3369
3666
 
3370
3667
  \`\`\`json
3371
3668
  ${JSON.stringify(detail)}
3372
3669
  \`\`\``);
3373
3670
  } catch (e) {
3374
- return fail15(errMsg15(e));
3671
+ return fail16(errMsg16(e));
3375
3672
  }
3376
3673
  }
3377
3674
  );
@@ -3380,27 +3677,27 @@ ${JSON.stringify(detail)}
3380
3677
  {
3381
3678
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
3382
3679
  inputSchema: {
3383
- deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
3680
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
3384
3681
  }
3385
3682
  },
3386
3683
  async ({ deviceId }) => {
3387
3684
  try {
3388
3685
  const target = sync.requireProDevice(deviceId);
3389
3686
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
3390
- return text13("Cleared js-top stats.");
3687
+ return text14("Cleared js-top stats.");
3391
3688
  } catch (e) {
3392
- return fail15(errMsg15(e));
3689
+ return fail16(errMsg16(e));
3393
3690
  }
3394
3691
  }
3395
3692
  );
3396
3693
  }
3397
3694
 
3398
3695
  // src/tools/packs/images.ts
3399
- import { z as z16 } from "zod";
3696
+ import { z as z17 } from "zod";
3400
3697
  var IMAGES_TOOL_ID = "images";
3401
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3402
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3403
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3698
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3699
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3700
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3404
3701
  function uriTail(uri) {
3405
3702
  if (!uri) return "(no source)";
3406
3703
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -3488,9 +3785,9 @@ function registerImagesTools(server, sync) {
3488
3785
  {
3489
3786
  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.",
3490
3787
  inputSchema: {
3491
- deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3492
- limit: z16.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3493
- status: z16.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3788
+ deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3789
+ limit: z17.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3790
+ status: z17.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3494
3791
  }
3495
3792
  },
3496
3793
  async ({ deviceId, limit, status }) => {
@@ -3503,7 +3800,7 @@ function registerImagesTools(server, sync) {
3503
3800
  { limit: capped, status },
3504
3801
  target
3505
3802
  );
3506
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3803
+ if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3507
3804
  const md = formatListMarkdown(list, capped);
3508
3805
  const slim = {
3509
3806
  stats: list.stats,
@@ -3530,13 +3827,13 @@ function registerImagesTools(server, sync) {
3530
3827
  layoutShifts: r.layoutShifts
3531
3828
  }))
3532
3829
  };
3533
- return text14(`${md}
3830
+ return text15(`${md}
3534
3831
 
3535
3832
  \`\`\`json
3536
3833
  ${JSON.stringify(slim)}
3537
3834
  \`\`\``);
3538
3835
  } catch (e) {
3539
- return fail16(errMsg16(e));
3836
+ return fail17(errMsg17(e));
3540
3837
  }
3541
3838
  }
3542
3839
  );
@@ -3545,9 +3842,9 @@ ${JSON.stringify(slim)}
3545
3842
  {
3546
3843
  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.",
3547
3844
  inputSchema: {
3548
- deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3549
- id: z16.number().describe("Record id from get_images."),
3550
- action: z16.enum([
3845
+ deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3846
+ id: z17.number().describe("Record id from get_images."),
3847
+ action: z17.enum([
3551
3848
  "hard_reload",
3552
3849
  "retry",
3553
3850
  "evict_disk",
@@ -3559,7 +3856,7 @@ ${JSON.stringify(slim)}
3559
3856
  "swap_url",
3560
3857
  "restore"
3561
3858
  ]).describe("What to do."),
3562
- url: z16.string().optional().describe("Replacement URL (swap_url only).")
3859
+ url: z17.string().optional().describe("Replacement URL (swap_url only).")
3563
3860
  }
3564
3861
  },
3565
3862
  async ({ deviceId, id, action, url }) => {
@@ -3567,8 +3864,8 @@ ${JSON.stringify(slim)}
3567
3864
  const target = sync.requireProDevice(deviceId);
3568
3865
  if (action === "prove_savings") {
3569
3866
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
3570
- if (!savings) return fail16("Device returned nothing for proveSavings.");
3571
- if (!savings.ok) return fail16(savings.message);
3867
+ if (!savings) return fail17("Device returned nothing for proveSavings.");
3868
+ if (!savings.ok) return fail17(savings.message);
3572
3869
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
3573
3870
  if (savings.originalBytes != null)
3574
3871
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -3580,11 +3877,11 @@ ${JSON.stringify(slim)}
3580
3877
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
3581
3878
  if (savings.optimizedUri)
3582
3879
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
3583
- return text14(parts.join("\n"));
3880
+ return text15(parts.join("\n"));
3584
3881
  }
3585
3882
  const entry = ACTION_MAP[action];
3586
- if (!entry) return fail16(`Unknown action: ${action}`);
3587
- if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
3883
+ if (!entry) return fail17(`Unknown action: ${action}`);
3884
+ if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
3588
3885
  const result = await sync.callAction(
3589
3886
  IMAGES_TOOL_ID,
3590
3887
  entry.action,
@@ -3604,9 +3901,9 @@ ${JSON.stringify(slim)}
3604
3901
  );
3605
3902
  if (detail.error) lines.push(`Error: ${detail.error}`);
3606
3903
  }
3607
- return text14(lines.join("\n"));
3904
+ return text15(lines.join("\n"));
3608
3905
  } catch (e) {
3609
- return fail16(errMsg16(e));
3906
+ return fail17(errMsg17(e));
3610
3907
  }
3611
3908
  }
3612
3909
  );
@@ -3615,12 +3912,12 @@ ${JSON.stringify(slim)}
3615
3912
  {
3616
3913
  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.",
3617
3914
  inputSchema: {
3618
- deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3619
- network: z16.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3620
- blankImages: z16.boolean().optional().describe("Render all images without a source."),
3621
- massAction: z16.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."),
3622
- clearRegistry: z16.boolean().optional().describe("Clear the captured image log."),
3623
- clearExpoCaches: z16.boolean().optional().describe("Clear expo-image memory + disk caches.")
3915
+ deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3916
+ network: z17.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3917
+ blankImages: z17.boolean().optional().describe("Render all images without a source."),
3918
+ massAction: z17.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."),
3919
+ clearRegistry: z17.boolean().optional().describe("Clear the captured image log."),
3920
+ clearExpoCaches: z17.boolean().optional().describe("Clear expo-image memory + disk caches.")
3624
3921
  }
3625
3922
  },
3626
3923
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3647,17 +3944,17 @@ ${JSON.stringify(slim)}
3647
3944
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3648
3945
  done.push(r?.message ?? "expo caches cleared");
3649
3946
  }
3650
- if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3651
- return text14(`Applied: ${done.join(" \xB7 ")}`);
3947
+ if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3948
+ return text15(`Applied: ${done.join(" \xB7 ")}`);
3652
3949
  } catch (e) {
3653
- return fail16(errMsg16(e));
3950
+ return fail17(errMsg17(e));
3654
3951
  }
3655
3952
  }
3656
3953
  );
3657
3954
  }
3658
3955
 
3659
3956
  // src/tools/packs/assets.ts
3660
- import { z as z17 } from "zod";
3957
+ import { z as z18 } from "zod";
3661
3958
 
3662
3959
  // src/tools/packs/assetsGraphNote.ts
3663
3960
  function graphUnavailableNote(ss) {
@@ -3672,9 +3969,9 @@ function graphUnavailableNote(ss) {
3672
3969
 
3673
3970
  // src/tools/packs/assets.ts
3674
3971
  var ASSETS_TOOL_ID = "assets";
3675
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3676
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3677
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3972
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
3973
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3974
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3678
3975
  function kb2(bytes) {
3679
3976
  if (bytes == null || bytes <= 0) return "\u2013";
3680
3977
  if (bytes < 1024) return `${bytes}B`;
@@ -3762,10 +4059,10 @@ function registerAssetsTools(server, sync) {
3762
4059
  {
3763
4060
  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.",
3764
4061
  inputSchema: {
3765
- deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3766
- limit: z17.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3767
- kind: z17.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3768
- unusedOnly: z17.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4062
+ deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
4063
+ limit: z18.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4064
+ kind: z18.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4065
+ unusedOnly: z18.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3769
4066
  }
3770
4067
  },
3771
4068
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3778,7 +4075,7 @@ function registerAssetsTools(server, sync) {
3778
4075
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3779
4076
  target
3780
4077
  );
3781
- if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4078
+ if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3782
4079
  if (unusedOnly) {
3783
4080
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3784
4081
  }
@@ -3802,13 +4099,13 @@ function registerAssetsTools(server, sync) {
3802
4099
  sizeSource: r.sizeSource
3803
4100
  }))
3804
4101
  };
3805
- return text15(`${md}
4102
+ return text16(`${md}
3806
4103
 
3807
4104
  \`\`\`json
3808
4105
  ${JSON.stringify(slim)}
3809
4106
  \`\`\``);
3810
4107
  } catch (e) {
3811
- return fail17(errMsg17(e));
4108
+ return fail18(errMsg18(e));
3812
4109
  }
3813
4110
  }
3814
4111
  );
@@ -3817,9 +4114,9 @@ ${JSON.stringify(slim)}
3817
4114
  {
3818
4115
  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).",
3819
4116
  inputSchema: {
3820
- deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3821
- action: z17.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3822
- id: z17.number().optional().describe("Asset id from get_assets (detail only).")
4117
+ deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
4118
+ action: z18.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4119
+ id: z18.number().optional().describe("Asset id from get_assets (detail only).")
3823
4120
  }
3824
4121
  },
3825
4122
  async ({ deviceId, action, id }) => {
@@ -3829,19 +4126,19 @@ ${JSON.stringify(slim)}
3829
4126
  case "rescan": {
3830
4127
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3831
4128
  const ss = result?.scanStatus;
3832
- if (!ss) return fail17("Device returned nothing for rescan.");
3833
- return text15(
4129
+ if (!ss) return fail18("Device returned nothing for rescan.");
4130
+ return text16(
3834
4131
  `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" : "")
3835
4132
  );
3836
4133
  }
3837
4134
  case "measure_sizes": {
3838
4135
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3839
- return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4136
+ return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3840
4137
  }
3841
4138
  case "detail": {
3842
- if (id == null) return fail17("detail requires the `id` param (from get_assets).");
4139
+ if (id == null) return fail18("detail requires the `id` param (from get_assets).");
3843
4140
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3844
- if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4141
+ if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3845
4142
  const lines = [
3846
4143
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3847
4144
  "",
@@ -3859,32 +4156,32 @@ ${JSON.stringify(slim)}
3859
4156
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3860
4157
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3861
4158
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3862
- return text15(lines.join("\n"));
4159
+ return text16(lines.join("\n"));
3863
4160
  }
3864
4161
  case "save_baseline": {
3865
4162
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3866
- return text15(
4163
+ return text16(
3867
4164
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3868
4165
  );
3869
4166
  }
3870
4167
  case "clear_baseline": {
3871
4168
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3872
- return text15("Baseline cleared.");
4169
+ return text16("Baseline cleared.");
3873
4170
  }
3874
4171
  case "clear_records": {
3875
4172
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3876
- return text15(result?.message ?? "Inventory cleared.");
4173
+ return text16(result?.message ?? "Inventory cleared.");
3877
4174
  }
3878
4175
  }
3879
4176
  } catch (e) {
3880
- return fail17(errMsg17(e));
4177
+ return fail18(errMsg18(e));
3881
4178
  }
3882
4179
  }
3883
4180
  );
3884
4181
  }
3885
4182
 
3886
4183
  // src/tools/packs/reload.ts
3887
- import { z as z18 } from "zod";
4184
+ import { z as z19 } from "zod";
3888
4185
 
3889
4186
  // src/capture/devServerReload.ts
3890
4187
  import { WebSocket } from "ws";
@@ -3947,9 +4244,9 @@ var DEFAULT_WAIT_MS = 3e4;
3947
4244
  var TEARDOWN_GRACE_MS = 1200;
3948
4245
  var PROBE_TIMEOUT_MS = 1500;
3949
4246
  var PROBE_INTERVAL_MS = 400;
3950
- var text16 = (s) => ({ content: [{ type: "text", text: s }] });
3951
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3952
- var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
4247
+ var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4248
+ var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4249
+ var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
3953
4250
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3954
4251
  async function ping(sync, deviceId, timeoutMs) {
3955
4252
  try {
@@ -3988,12 +4285,12 @@ function registerReloadTools(server, sync) {
3988
4285
  {
3989
4286
  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.",
3990
4287
  inputSchema: {
3991
- strategy: z18.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4288
+ strategy: z19.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3992
4289
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3993
4290
  ),
3994
- wait: z18.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3995
- timeoutMs: z18.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3996
- deviceId: z18.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4291
+ wait: z19.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4292
+ timeoutMs: z19.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4293
+ deviceId: z19.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3997
4294
  }
3998
4295
  },
3999
4296
  async (args) => {
@@ -4035,32 +4332,32 @@ function registerReloadTools(server, sync) {
4035
4332
  }
4036
4333
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
4037
4334
  if (args.wait === false) {
4038
- return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4335
+ return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4039
4336
  }
4040
4337
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4041
4338
  if (elapsed == null) {
4042
- return fail18(
4339
+ return fail19(
4043
4340
  `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.`
4044
4341
  );
4045
4342
  }
4046
- return text16(
4343
+ return text17(
4047
4344
  `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.`
4048
4345
  );
4049
4346
  } catch (e) {
4050
- return fail18(errMsg18(e));
4347
+ return fail19(errMsg19(e));
4051
4348
  }
4052
4349
  }
4053
4350
  );
4054
4351
  }
4055
4352
 
4056
4353
  // src/tools/packs/triage.ts
4057
- import { z as z19 } from "zod";
4058
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4059
- var fail19 = (message) => ({
4354
+ import { z as z20 } from "zod";
4355
+ var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4356
+ var fail20 = (message) => ({
4060
4357
  isError: true,
4061
4358
  content: [{ type: "text", text: message }]
4062
4359
  });
4063
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4360
+ var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4064
4361
  var PROBE_TIMEOUT_MS2 = 5e3;
4065
4362
  var MAX_STORAGE_KEYS = 20;
4066
4363
  var MAX_STORAGE_VALUE = 400;
@@ -4097,7 +4394,7 @@ async function section(title, run) {
4097
4394
  return body ? `${title}
4098
4395
  ${body}` : null;
4099
4396
  } catch (e) {
4100
- const msg = errMsg19(e);
4397
+ const msg = errMsg20(e);
4101
4398
  if (/not responding|timed out|got no answer/i.test(msg)) {
4102
4399
  return `${title}
4103
4400
  (no answer \u2014 see the note above)`;
@@ -4112,7 +4409,7 @@ function registerTriageTools(server, sync) {
4112
4409
  {
4113
4410
  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.",
4114
4411
  inputSchema: {
4115
- deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected).")
4412
+ deviceId: z20.string().optional().describe("Target device id (auto-selected if one is connected).")
4116
4413
  }
4117
4414
  },
4118
4415
  async (args) => {
@@ -4219,7 +4516,7 @@ _Values: get_storage includeValues:true_`;
4219
4516
 
4220
4517
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4221
4518
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4222
- return text17(
4519
+ return text18(
4223
4520
  `${head}${banner}
4224
4521
 
4225
4522
  ${body}
@@ -4227,22 +4524,22 @@ ${body}
4227
4524
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4228
4525
  );
4229
4526
  } catch (e) {
4230
- return fail19(errMsg19(e));
4527
+ return fail20(errMsg20(e));
4231
4528
  }
4232
4529
  }
4233
4530
  );
4234
4531
  }
4235
4532
 
4236
4533
  // src/tools/packs/flow.ts
4237
- import { z as z20 } from "zod";
4534
+ import { z as z21 } from "zod";
4238
4535
  var UI_TOOL_ID = "highlight-updates";
4239
4536
  var APP_TOOL_ID2 = "app";
4240
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4241
- var fail20 = (message) => ({
4537
+ var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4538
+ var fail21 = (message) => ({
4242
4539
  isError: true,
4243
4540
  content: [{ type: "text", text: message }]
4244
4541
  });
4245
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4542
+ var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4246
4543
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4247
4544
  var POLL_MS = 250;
4248
4545
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4259,19 +4556,19 @@ function formatElement2(e) {
4259
4556
  const display = e.text ?? e.label;
4260
4557
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4261
4558
  }
4262
- var stepSchema = z20.object({
4263
- tap: z20.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4264
- longPress: z20.boolean().optional().describe("Make `tap` a long-press instead."),
4265
- type: z20.string().optional().describe("Text to type (needs `into`)."),
4266
- into: z20.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4267
- set: z20.union([z20.number(), z20.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4268
- reload: z20.boolean().optional().describe("Reload the JS bundle before this step's action."),
4269
- waitMs: z20.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4270
- waitFor: z20.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4271
- waitGone: z20.string().optional().describe("Wait until no element matches this (default 5s)."),
4272
- expect: z20.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4273
- expectGone: z20.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4274
- timeoutMs: z20.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
4559
+ var stepSchema = z21.object({
4560
+ tap: z21.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4561
+ longPress: z21.boolean().optional().describe("Make `tap` a long-press instead."),
4562
+ type: z21.string().optional().describe("Text to type (needs `into`)."),
4563
+ into: z21.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4564
+ set: z21.union([z21.number(), z21.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4565
+ reload: z21.boolean().optional().describe("Reload the JS bundle before this step's action."),
4566
+ waitMs: z21.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4567
+ waitFor: z21.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4568
+ waitGone: z21.string().optional().describe("Wait until no element matches this (default 5s)."),
4569
+ expect: z21.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4570
+ expectGone: z21.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4571
+ timeoutMs: z21.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
4275
4572
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4276
4573
  function registerFlowTools(server, sync) {
4277
4574
  server.registerTool(
@@ -4279,10 +4576,10 @@ function registerFlowTools(server, sync) {
4279
4576
  {
4280
4577
  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.",
4281
4578
  inputSchema: {
4282
- steps: z20.array(stepSchema).min(1).describe("Steps to run, in order."),
4283
- include: z20.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."),
4284
- continueOnFailure: z20.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4285
- deviceId: z20.string().optional().describe("Target device id (auto-selected if one is connected).")
4579
+ steps: z21.array(stepSchema).min(1).describe("Steps to run, in order."),
4580
+ include: z21.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."),
4581
+ continueOnFailure: z21.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4582
+ deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
4286
4583
  }
4287
4584
  },
4288
4585
  async (args) => {
@@ -4454,7 +4751,7 @@ function registerFlowTools(server, sync) {
4454
4751
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
4455
4752
  }
4456
4753
  } catch (e) {
4457
- stepErrors.push(errMsg20(e));
4754
+ stepErrors.push(errMsg21(e));
4458
4755
  }
4459
4756
  if (stepErrors.length) {
4460
4757
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -4477,7 +4774,7 @@ function registerFlowTools(server, sync) {
4477
4774
  ${shown.map(formatElement2).join("\n")}`
4478
4775
  );
4479
4776
  } catch (e) {
4480
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg20(e))}`);
4777
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
4481
4778
  }
4482
4779
  }
4483
4780
  if (include === "all") {
@@ -4506,9 +4803,9 @@ ${shown.map(formatElement2).join("\n")}`
4506
4803
  }
4507
4804
  }
4508
4805
  }
4509
- return text18(parts.join("\n\n"));
4806
+ return text19(parts.join("\n\n"));
4510
4807
  } catch (e) {
4511
- return fail20(errMsg20(e));
4808
+ return fail21(errMsg21(e));
4512
4809
  }
4513
4810
  }
4514
4811
  );
@@ -4529,25 +4826,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4529
4826
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
4530
4827
 
4531
4828
  // src/tools/packs/renders.ts
4532
- import { z as z21 } from "zod";
4829
+ import { z as z22 } from "zod";
4533
4830
  var UI_TOOL_ID2 = "highlight-updates";
4534
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4535
- var fail21 = (message) => ({
4831
+ var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4832
+ var fail22 = (message) => ({
4536
4833
  isError: true,
4537
4834
  content: [{ type: "text", text: message }]
4538
4835
  });
4539
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4836
+ var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
4540
4837
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
4541
4838
  var DEFAULT_IDLE_MS = 3e3;
4542
4839
  var SETTLE_MS = 400;
4543
4840
  var DEFAULT_TOP = 12;
4544
4841
  var lastCapture = /* @__PURE__ */ new Map();
4545
- var stepSchema2 = z21.object({
4546
- tap: z21.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4547
- type: z21.string().optional().describe("Text to type (needs `into`)."),
4548
- into: z21.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4549
- waitMs: z21.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4550
- repeat: z21.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
4842
+ var stepSchema2 = z22.object({
4843
+ tap: z22.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4844
+ type: z22.string().optional().describe("Text to type (needs `into`)."),
4845
+ into: z22.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4846
+ waitMs: z22.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4847
+ repeat: z22.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
4551
4848
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
4552
4849
  function selector2(needle) {
4553
4850
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -4640,14 +4937,14 @@ function registerRenderTools(server, sync) {
4640
4937
  {
4641
4938
  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.",
4642
4939
  inputSchema: {
4643
- steps: z21.array(stepSchema2).max(20).optional().describe(
4940
+ steps: z22.array(stepSchema2).max(20).optional().describe(
4644
4941
  "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."
4645
4942
  ),
4646
- idleMs: z21.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
4647
- label: z21.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
4648
- compareToPrevious: z21.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
4649
- top: z21.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
4650
- deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
4943
+ idleMs: z22.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
4944
+ label: z22.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
4945
+ compareToPrevious: z22.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
4946
+ top: z22.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
4947
+ deviceId: z22.string().optional().describe("Target device id (auto-selected if one is connected).")
4651
4948
  }
4652
4949
  },
4653
4950
  async (args) => {
@@ -4656,18 +4953,18 @@ function registerRenderTools(server, sync) {
4656
4953
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
4657
4954
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
4658
4955
  if (!uiTool) {
4659
- return fail21(
4956
+ return fail22(
4660
4957
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
4661
4958
  );
4662
4959
  }
4663
4960
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
4664
- return fail21(
4961
+ return fail22(
4665
4962
  "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."
4666
4963
  );
4667
4964
  }
4668
4965
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
4669
4966
  if (began && began.ok === false) {
4670
- return fail21(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4967
+ return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4671
4968
  }
4672
4969
  await delay5(SETTLE_MS);
4673
4970
  const startedAt = Date.now();
@@ -4712,7 +5009,7 @@ function registerRenderTools(server, sync) {
4712
5009
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
4713
5010
  const summary = ended?.summary;
4714
5011
  if (!summary) {
4715
- return fail21(
5012
+ return fail22(
4716
5013
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
4717
5014
  );
4718
5015
  }
@@ -4737,9 +5034,9 @@ function registerRenderTools(server, sync) {
4737
5034
  } else if (!args.compareToPrevious) {
4738
5035
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
4739
5036
  }
4740
- return text19(parts.join("\n"));
5037
+ return text20(parts.join("\n"));
4741
5038
  } catch (e) {
4742
- return fail21(errMsg21(e));
5039
+ return fail22(errMsg22(e));
4743
5040
  }
4744
5041
  }
4745
5042
  );
@@ -4960,6 +5257,7 @@ require the connected app to have an active Buoy Pro license. If they error with
4960
5257
  required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
4961
5258
  var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
4962
5259
  "measure_renders",
5260
+ "scenario_action",
4963
5261
  "run_flow",
4964
5262
  "run_benchmark_batch",
4965
5263
  "get_triage",
@@ -5010,6 +5308,7 @@ function createMcpServer(sync) {
5010
5308
  registerRoutesTools(server, sync);
5011
5309
  registerConsoleTools(server, sync);
5012
5310
  registerTimeMachineTools(server, sync);
5311
+ registerScenariosTools(server, sync);
5013
5312
  registerScreenshotTools(server, sync);
5014
5313
  registerInteractTools(server, sync);
5015
5314
  registerJsThreadTools(server, sync);
@@ -5019,7 +5318,7 @@ function createMcpServer(sync) {
5019
5318
  return server;
5020
5319
  }
5021
5320
  var deviceIdField = {
5022
- deviceId: z22.string().optional().describe(
5321
+ deviceId: z23.string().optional().describe(
5023
5322
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5024
5323
  )
5025
5324
  };