@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.
package/dist/index.js CHANGED
@@ -342,7 +342,7 @@ var SyncClient = class {
342
342
  };
343
343
  const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
344
344
  dev2
345
- )} Upgrade to Buoy Pro in the app to enable MCP access (run \`list_devices\` to check status).`;
345
+ )} 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.`;
346
346
  if (deviceId === ALL_DEVICES) {
347
347
  if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
348
348
  return ALL_DEVICES;
@@ -675,7 +675,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
675
675
 
676
676
  // src/server.ts
677
677
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
678
- var import_zod22 = require("zod");
678
+ var import_zod23 = require("zod");
679
679
 
680
680
  // src/tools/generic.ts
681
681
  var import_zod = require("zod");
@@ -686,7 +686,7 @@ var import_node_os = __toESM(require("os"));
686
686
  var import_node_path = __toESM(require("path"));
687
687
 
688
688
  // src/version.ts
689
- var VERSION = "7.0.29" ? "7.0.29" : "0.0.0-dev";
689
+ var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
690
690
  var PACKAGE_NAME = "@buoy-gg/mcp";
691
691
 
692
692
  // src/update/checkForUpdate.ts
@@ -783,7 +783,9 @@ function registerGenericTools(server, sync) {
783
783
  return json({
784
784
  devices,
785
785
  ...anyPro ? {} : {
786
- 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."
786
+ // A headless build has no on-device UI, so "upgrade in the app"
787
+ // cannot be followed there — name the `licenseKey` prop too.
788
+ 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)."
787
789
  },
788
790
  ...updateNotice ? { updateAvailable: updateNotice } : {}
789
791
  });
@@ -2944,8 +2946,303 @@ ${lines.join("\n")}`);
2944
2946
  );
2945
2947
  }
2946
2948
 
2947
- // src/tools/packs/screenshot.ts
2949
+ // src/tools/packs/scenarios.ts
2948
2950
  var import_zod13 = require("zod");
2951
+ var SCENARIOS_TOOL_ID = "scenarios";
2952
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2953
+ var fail13 = (message) => ({
2954
+ isError: true,
2955
+ content: [{ type: "text", text: message }]
2956
+ });
2957
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2958
+ function varSpec(v) {
2959
+ const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
2960
+ const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
2961
+ return `${v.key}: ${v.type}${opts}${def}`;
2962
+ }
2963
+ function summarize(s) {
2964
+ const vars = s.vars?.length ? `
2965
+ variables: ${s.vars.map(varSpec).join("; ")}` : "";
2966
+ const steps = s.stepLines?.length ? `
2967
+ ${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
2968
+ const meta = [
2969
+ `v${s.version}`,
2970
+ s.author,
2971
+ s.usage?.runs ? `${s.usage.runs} run(s)` : null,
2972
+ s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
2973
+ s.unsavedToRepo ? "unsaved to repo" : null
2974
+ ].filter(Boolean).join(" \xB7 ");
2975
+ const outcome = s.expectedOutcome ? `
2976
+ expected: ${s.expectedOutcome}` : "";
2977
+ return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
2978
+ }
2979
+ function renderSection(title, list, folders) {
2980
+ if (!list.length) return `**${title} (0):**
2981
+ _none_`;
2982
+ if (!folders.length) {
2983
+ return `**${title} (${list.length}):**
2984
+ ${list.map(summarize).join("\n")}`;
2985
+ }
2986
+ const parts = [`**${title} (${list.length}):**`];
2987
+ for (const folder of folders) {
2988
+ const inIt = list.filter(
2989
+ (s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
2990
+ );
2991
+ if (!inIt.length) continue;
2992
+ parts.push(`_${folder} (${inIt.length})_
2993
+ ${inIt.map(summarize).join("\n")}`);
2994
+ }
2995
+ const ungrouped = list.filter((s) => !s.folder);
2996
+ if (ungrouped.length) {
2997
+ parts.push(
2998
+ `_Ungrouped (${ungrouped.length})_
2999
+ ${ungrouped.map(summarize).join("\n")}`
3000
+ );
3001
+ }
3002
+ return parts.join("\n");
3003
+ }
3004
+ function formatActive(a) {
3005
+ const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
3006
+ return [
3007
+ `**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
3008
+ vars ? `variables: ${vars}` : null,
3009
+ `effects:
3010
+ ${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
3011
+ ].filter(Boolean).join("\n");
3012
+ }
3013
+ function registerScenariosTools(server, sync) {
3014
+ server.registerTool(
3015
+ "get_scenarios",
3016
+ {
3017
+ 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.",
3018
+ inputSchema: {
3019
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3020
+ }
3021
+ },
3022
+ async (args) => {
3023
+ try {
3024
+ const target = sync.requireProDevice(args.deviceId);
3025
+ const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
3026
+ const p = payload ?? {};
3027
+ const code = p.code ?? [];
3028
+ const device = p.device ?? [];
3029
+ const drafts = p.drafts ?? [];
3030
+ const folders = p.folders ?? [];
3031
+ const parts = [];
3032
+ parts.push(
3033
+ p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
3034
+ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
3035
+ );
3036
+ if (folders.length) {
3037
+ parts.push(
3038
+ `**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.`
3039
+ );
3040
+ }
3041
+ parts.push(renderSection("Shipped with this app", code, folders));
3042
+ parts.push(renderSection("Saved on this device", device, folders));
3043
+ if (drafts.length) {
3044
+ parts.push(
3045
+ `**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
3046
+ ${drafts.map(summarize).join("\n")}`
3047
+ );
3048
+ }
3049
+ return text12(parts.join("\n\n"));
3050
+ } catch (e) {
3051
+ return fail13(errMsg13(e));
3052
+ }
3053
+ }
3054
+ );
3055
+ server.registerTool(
3056
+ "scenario_action",
3057
+ {
3058
+ 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.`,
3059
+ inputSchema: {
3060
+ action: import_zod13.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3061
+ folder: import_zod13.z.string().optional().describe(
3062
+ "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."
3063
+ ),
3064
+ id: import_zod13.z.string().optional().describe(
3065
+ "Scenario id (required for preview/run/delete; from get_scenarios)."
3066
+ ),
3067
+ params: import_zod13.z.record(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional().describe(
3068
+ "Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
3069
+ ),
3070
+ scenario: import_zod13.z.object({
3071
+ id: import_zod13.z.string().describe("Stable kebab-case id."),
3072
+ name: import_zod13.z.string().describe("Human name; may contain {{variable}} placeholders."),
3073
+ description: import_zod13.z.string().optional(),
3074
+ expectedOutcome: import_zod13.z.string().optional().describe(
3075
+ "What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
3076
+ ),
3077
+ author: import_zod13.z.string().optional(),
3078
+ authoredAgainst: import_zod13.z.string().optional().describe("App version this was written against."),
3079
+ folder: import_zod13.z.string().optional().describe(
3080
+ 'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
3081
+ ),
3082
+ tags: import_zod13.z.array(import_zod13.z.string()).optional(),
3083
+ vars: import_zod13.z.array(
3084
+ import_zod13.z.object({
3085
+ key: import_zod13.z.string(),
3086
+ label: import_zod13.z.string().optional(),
3087
+ type: import_zod13.z.enum(["string", "number", "boolean", "enum"]),
3088
+ options: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional(),
3089
+ default: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]).optional()
3090
+ })
3091
+ ).optional().describe(
3092
+ "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."
3093
+ ),
3094
+ steps: import_zod13.z.array(
3095
+ import_zod13.z.object({
3096
+ id: import_zod13.z.string().optional(),
3097
+ tool: import_zod13.z.string().describe(
3098
+ "Tool id: network | storage | impersonate | route-events | query | time-machine"
3099
+ ),
3100
+ action: import_zod13.z.string().describe(
3101
+ "Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
3102
+ ),
3103
+ params: import_zod13.z.unknown().optional(),
3104
+ label: import_zod13.z.string().optional()
3105
+ })
3106
+ ).describe("Steps, in order."),
3107
+ undoSteps: import_zod13.z.array(
3108
+ import_zod13.z.object({
3109
+ id: import_zod13.z.string().optional(),
3110
+ tool: import_zod13.z.string(),
3111
+ action: import_zod13.z.string(),
3112
+ params: import_zod13.z.unknown().optional(),
3113
+ label: import_zod13.z.string().optional()
3114
+ })
3115
+ ).optional().describe(
3116
+ "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."
3117
+ )
3118
+ }).optional().describe("The scenario to save (action 'save')."),
3119
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3120
+ }
3121
+ },
3122
+ async (args) => {
3123
+ try {
3124
+ const target = sync.requireProDevice(args.deviceId);
3125
+ const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
3126
+ switch (args.action) {
3127
+ case "preview": {
3128
+ if (!args.id) return fail13("preview requires `id` (from get_scenarios).");
3129
+ const r = await call("preview", {
3130
+ id: args.id,
3131
+ params: args.params ?? {}
3132
+ });
3133
+ const lines = [
3134
+ `**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
3135
+ `variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
3136
+ `**Would do:**
3137
+ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
3138
+ ];
3139
+ if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
3140
+ if (r.conflict) {
3141
+ lines.push(
3142
+ `\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.`
3143
+ );
3144
+ }
3145
+ if (r.warnings?.length) lines.push(`**Warnings:**
3146
+ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
3147
+ lines.push(
3148
+ r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
3149
+ ${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
3150
+ );
3151
+ return text12(lines.join("\n\n"));
3152
+ }
3153
+ case "run": {
3154
+ if (!args.id) return fail13("run requires `id` (from get_scenarios).");
3155
+ const r = await call("run", {
3156
+ id: args.id,
3157
+ params: args.params ?? {}
3158
+ });
3159
+ if (r.preflightErrors?.length) {
3160
+ return fail13(
3161
+ `Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
3162
+ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3163
+ );
3164
+ }
3165
+ const steps = (r.steps ?? []).map((s) => {
3166
+ const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
3167
+ const err = s.error ? `
3168
+ ${s.error}` : "";
3169
+ return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
3170
+ }).join("\n");
3171
+ if (!r.ok) {
3172
+ return fail13(
3173
+ `Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
3174
+ ${steps}
3175
+
3176
+ The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
3177
+ );
3178
+ }
3179
+ return text12(
3180
+ [
3181
+ `\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
3182
+ steps,
3183
+ `**Now bent:**
3184
+ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
3185
+ "The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
3186
+ ].join("\n\n")
3187
+ );
3188
+ }
3189
+ case "deactivate": {
3190
+ const r = await call("deactivate", {});
3191
+ const rep = r.report;
3192
+ const lines = [
3193
+ `Removed ${rep.removedRules} override rule(s)`,
3194
+ rep.stoppedImpersonation ? "Ended impersonation" : null,
3195
+ rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
3196
+ ].filter(Boolean);
3197
+ const warn = rep.unrestoredStorageKeys.length ? `
3198
+
3199
+ \u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
3200
+ const errs = rep.errors.length ? `
3201
+
3202
+ Errors: ${rep.errors.join("; ")}` : "";
3203
+ return text12(`**Deactivated.**
3204
+ ${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
3205
+ }
3206
+ case "save": {
3207
+ if (!args.scenario) return fail13("save requires `scenario`.");
3208
+ const r = await call("save", { scenario: args.scenario });
3209
+ if (!r.ok) {
3210
+ return fail13(
3211
+ `Save rejected:
3212
+ ${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
3213
+ );
3214
+ }
3215
+ return text12(
3216
+ `Saved \`${r.id}\` **as a draft**.
3217
+
3218
+ 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.`
3219
+ );
3220
+ }
3221
+ case "setFolder": {
3222
+ if (!args.id) return fail13("setFolder requires `id` (from get_scenarios).");
3223
+ const r = await call("setFolder", {
3224
+ id: args.id,
3225
+ folder: args.folder
3226
+ });
3227
+ return text12(
3228
+ r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
3229
+ );
3230
+ }
3231
+ case "delete": {
3232
+ if (!args.id) return fail13("delete requires `id`.");
3233
+ await call("delete", { id: args.id });
3234
+ return text12(`Deleted \`${args.id}\` from the device library.`);
3235
+ }
3236
+ }
3237
+ } catch (e) {
3238
+ return fail13(errMsg13(e));
3239
+ }
3240
+ }
3241
+ );
3242
+ }
3243
+
3244
+ // src/tools/packs/screenshot.ts
3245
+ var import_zod14 = require("zod");
2949
3246
 
2950
3247
  // src/capture/simctl.ts
2951
3248
  var import_node_child_process = require("child_process");
@@ -3059,8 +3356,8 @@ function frameToRect(frame, screen) {
3059
3356
  var TOOL_ID = "highlight-updates";
3060
3357
  var MARGIN_PT = 12;
3061
3358
  var CAPTURE_PADDING_PT = 6;
3062
- var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3063
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
3359
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3360
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3064
3361
  function reasonMessage(res, query) {
3065
3362
  switch (res.reason) {
3066
3363
  case "no-renders":
@@ -3090,10 +3387,10 @@ function registerScreenshotTools(server, sync) {
3090
3387
  {
3091
3388
  description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. If highlight tracking hasn't seen a render yet, it falls back to a live fiber walk (same as describe_screen). iOS Simulator only, and the MCP server must run on the same Mac as the booted Simulator. Requires Buoy Pro.",
3092
3389
  inputSchema: {
3093
- query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3094
- nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3095
- scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3096
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3390
+ query: import_zod14.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3391
+ nativeTag: import_zod14.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3392
+ scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3393
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3097
3394
  }
3098
3395
  },
3099
3396
  async (args) => {
@@ -3135,11 +3432,11 @@ function registerScreenshotTools(server, sync) {
3135
3432
  }
3136
3433
  }
3137
3434
  if (!located.matched || !located.rect || !located.screen) {
3138
- return fail13(reasonMessage(located, args.query) + candidatesText(located.candidates));
3435
+ return fail14(reasonMessage(located, args.query) + candidatesText(located.candidates));
3139
3436
  }
3140
3437
  const shot = await captureIosFramebuffer();
3141
3438
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
3142
- return fail13(shot.error ?? "Screenshot capture failed.");
3439
+ return fail14(shot.error ?? "Screenshot capture failed.");
3143
3440
  }
3144
3441
  const pxScale = shot.width / located.screen.width;
3145
3442
  const r = located.rect;
@@ -3155,7 +3452,7 @@ function registerScreenshotTools(server, sync) {
3155
3452
  }
3156
3453
  );
3157
3454
  if (!crop.ok || !crop.buffer) {
3158
- return fail13(crop.error ?? "Crop failed.");
3455
+ return fail14(crop.error ?? "Crop failed.");
3159
3456
  }
3160
3457
  const label = located.componentName || "component";
3161
3458
  const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
@@ -3168,21 +3465,21 @@ function registerScreenshotTools(server, sync) {
3168
3465
  ]
3169
3466
  };
3170
3467
  } catch (e) {
3171
- return fail13(errMsg13(e));
3468
+ return fail14(errMsg14(e));
3172
3469
  }
3173
3470
  }
3174
3471
  );
3175
3472
  }
3176
3473
 
3177
3474
  // src/tools/packs/interact.ts
3178
- var import_zod14 = require("zod");
3475
+ var import_zod15 = require("zod");
3179
3476
  var TOOL_ID2 = "highlight-updates";
3180
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
3181
- var fail14 = (message) => ({
3477
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3478
+ var fail15 = (message) => ({
3182
3479
  isError: true,
3183
3480
  content: [{ type: "text", text: message }]
3184
3481
  });
3185
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3482
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3186
3483
  function formatElement(e) {
3187
3484
  let line = e.interactive ? "\u2022 " : " ";
3188
3485
  line += e.name;
@@ -3205,8 +3502,8 @@ function registerInteractTools(server, sync) {
3205
3502
  {
3206
3503
  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.",
3207
3504
  inputSchema: {
3208
- interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3209
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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).")
3210
3507
  }
3211
3508
  },
3212
3509
  async (args) => {
@@ -3216,16 +3513,16 @@ function registerInteractTools(server, sync) {
3216
3513
  const all = res?.elements ?? [];
3217
3514
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3218
3515
  if (elements.length === 0) {
3219
- return text12(
3516
+ return text13(
3220
3517
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
3221
3518
  );
3222
3519
  }
3223
3520
  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 }).`;
3224
- return text12(`${header}
3521
+ return text13(`${header}
3225
3522
 
3226
3523
  ${elements.map(formatElement).join("\n")}`);
3227
3524
  } catch (e) {
3228
- return fail14(errMsg14(e));
3525
+ return fail15(errMsg15(e));
3229
3526
  }
3230
3527
  }
3231
3528
  );
@@ -3234,20 +3531,20 @@ ${elements.map(formatElement).join("\n")}`);
3234
3531
  {
3235
3532
  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.",
3236
3533
  inputSchema: {
3237
- nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3238
- testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
3239
- query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3240
- value: import_zod14.z.union([import_zod14.z.number(), import_zod14.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)."),
3241
- text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3242
- longPress: import_zod14.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3243
- scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3244
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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).")
3245
3542
  }
3246
3543
  },
3247
3544
  async (args) => {
3248
3545
  try {
3249
3546
  if (args.nativeTag == null && !args.testID && !args.query) {
3250
- return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
3547
+ return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
3251
3548
  }
3252
3549
  const target = sync.requireProDevice(args.deviceId);
3253
3550
  const res = await sync.callAction(
@@ -3272,26 +3569,26 @@ ${elements.map(formatElement).join("\n")}`);
3272
3569
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3273
3570
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3274
3571
  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}`;
3275
- return text12(`${verb}${scrolledNote}.${flowTip}`);
3572
+ return text13(`${verb}${scrolledNote}.${flowTip}`);
3276
3573
  }
3277
3574
  let msg = res?.reason ?? "Tap failed.";
3278
3575
  if (res?.candidates?.length) {
3279
3576
  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");
3280
3577
  }
3281
- return fail14(msg);
3578
+ return fail15(msg);
3282
3579
  } catch (e) {
3283
- return fail14(errMsg14(e));
3580
+ return fail15(errMsg15(e));
3284
3581
  }
3285
3582
  }
3286
3583
  );
3287
3584
  }
3288
3585
 
3289
3586
  // src/tools/packs/jsThread.ts
3290
- var import_zod15 = require("zod");
3587
+ var import_zod16 = require("zod");
3291
3588
  var JS_TOP_TOOL_ID = "js-top";
3292
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3293
- var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3294
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3589
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3590
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3591
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3295
3592
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3296
3593
  function sparkline(values) {
3297
3594
  const max = Math.max(1, ...values);
@@ -3362,9 +3659,9 @@ function registerJsThreadTools(server, sync) {
3362
3659
  {
3363
3660
  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.',
3364
3661
  inputSchema: {
3365
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3366
- durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3367
- clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
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.")
3368
3665
  }
3369
3666
  },
3370
3667
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -3377,7 +3674,7 @@ function registerJsThreadTools(server, sync) {
3377
3674
  { durationMs: duration, clearFirst: clearFirst === true },
3378
3675
  target
3379
3676
  );
3380
- if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3677
+ if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3381
3678
  const md = formatTopMarkdown(snap, duration);
3382
3679
  const slim = {
3383
3680
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -3396,13 +3693,13 @@ function registerJsThreadTools(server, sync) {
3396
3693
  worstFreezeMs: r.worstFreezeMs
3397
3694
  }))
3398
3695
  };
3399
- return text13(`${md}
3696
+ return text14(`${md}
3400
3697
 
3401
3698
  \`\`\`json
3402
3699
  ${JSON.stringify(slim)}
3403
3700
  \`\`\``);
3404
3701
  } catch (e) {
3405
- return fail15(errMsg15(e));
3702
+ return fail16(errMsg16(e));
3406
3703
  }
3407
3704
  }
3408
3705
  );
@@ -3411,8 +3708,8 @@ ${JSON.stringify(slim)}
3411
3708
  {
3412
3709
  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).',
3413
3710
  inputSchema: {
3414
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3415
- key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
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.")
3416
3713
  }
3417
3714
  },
3418
3715
  async ({ deviceId, key }) => {
@@ -3425,18 +3722,18 @@ ${JSON.stringify(slim)}
3425
3722
  target
3426
3723
  );
3427
3724
  if (!detail) {
3428
- return fail15(
3725
+ return fail16(
3429
3726
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
3430
3727
  );
3431
3728
  }
3432
3729
  const md = formatDetailMarkdown(detail);
3433
- return text13(`${md}
3730
+ return text14(`${md}
3434
3731
 
3435
3732
  \`\`\`json
3436
3733
  ${JSON.stringify(detail)}
3437
3734
  \`\`\``);
3438
3735
  } catch (e) {
3439
- return fail15(errMsg15(e));
3736
+ return fail16(errMsg16(e));
3440
3737
  }
3441
3738
  }
3442
3739
  );
@@ -3445,27 +3742,27 @@ ${JSON.stringify(detail)}
3445
3742
  {
3446
3743
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
3447
3744
  inputSchema: {
3448
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3745
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3449
3746
  }
3450
3747
  },
3451
3748
  async ({ deviceId }) => {
3452
3749
  try {
3453
3750
  const target = sync.requireProDevice(deviceId);
3454
3751
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
3455
- return text13("Cleared js-top stats.");
3752
+ return text14("Cleared js-top stats.");
3456
3753
  } catch (e) {
3457
- return fail15(errMsg15(e));
3754
+ return fail16(errMsg16(e));
3458
3755
  }
3459
3756
  }
3460
3757
  );
3461
3758
  }
3462
3759
 
3463
3760
  // src/tools/packs/images.ts
3464
- var import_zod16 = require("zod");
3761
+ var import_zod17 = require("zod");
3465
3762
  var IMAGES_TOOL_ID = "images";
3466
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3467
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3468
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3763
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3764
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3765
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3469
3766
  function uriTail(uri) {
3470
3767
  if (!uri) return "(no source)";
3471
3768
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -3553,9 +3850,9 @@ function registerImagesTools(server, sync) {
3553
3850
  {
3554
3851
  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.",
3555
3852
  inputSchema: {
3556
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3557
- limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3558
- status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
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).")
3559
3856
  }
3560
3857
  },
3561
3858
  async ({ deviceId, limit, status }) => {
@@ -3568,7 +3865,7 @@ function registerImagesTools(server, sync) {
3568
3865
  { limit: capped, status },
3569
3866
  target
3570
3867
  );
3571
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3868
+ if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3572
3869
  const md = formatListMarkdown(list, capped);
3573
3870
  const slim = {
3574
3871
  stats: list.stats,
@@ -3595,13 +3892,13 @@ function registerImagesTools(server, sync) {
3595
3892
  layoutShifts: r.layoutShifts
3596
3893
  }))
3597
3894
  };
3598
- return text14(`${md}
3895
+ return text15(`${md}
3599
3896
 
3600
3897
  \`\`\`json
3601
3898
  ${JSON.stringify(slim)}
3602
3899
  \`\`\``);
3603
3900
  } catch (e) {
3604
- return fail16(errMsg16(e));
3901
+ return fail17(errMsg17(e));
3605
3902
  }
3606
3903
  }
3607
3904
  );
@@ -3610,9 +3907,9 @@ ${JSON.stringify(slim)}
3610
3907
  {
3611
3908
  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.",
3612
3909
  inputSchema: {
3613
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3614
- id: import_zod16.z.number().describe("Record id from get_images."),
3615
- action: import_zod16.z.enum([
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([
3616
3913
  "hard_reload",
3617
3914
  "retry",
3618
3915
  "evict_disk",
@@ -3624,7 +3921,7 @@ ${JSON.stringify(slim)}
3624
3921
  "swap_url",
3625
3922
  "restore"
3626
3923
  ]).describe("What to do."),
3627
- url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
3924
+ url: import_zod17.z.string().optional().describe("Replacement URL (swap_url only).")
3628
3925
  }
3629
3926
  },
3630
3927
  async ({ deviceId, id, action, url }) => {
@@ -3632,8 +3929,8 @@ ${JSON.stringify(slim)}
3632
3929
  const target = sync.requireProDevice(deviceId);
3633
3930
  if (action === "prove_savings") {
3634
3931
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
3635
- if (!savings) return fail16("Device returned nothing for proveSavings.");
3636
- if (!savings.ok) return fail16(savings.message);
3932
+ if (!savings) return fail17("Device returned nothing for proveSavings.");
3933
+ if (!savings.ok) return fail17(savings.message);
3637
3934
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
3638
3935
  if (savings.originalBytes != null)
3639
3936
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -3645,11 +3942,11 @@ ${JSON.stringify(slim)}
3645
3942
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
3646
3943
  if (savings.optimizedUri)
3647
3944
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
3648
- return text14(parts.join("\n"));
3945
+ return text15(parts.join("\n"));
3649
3946
  }
3650
3947
  const entry = ACTION_MAP[action];
3651
- if (!entry) return fail16(`Unknown action: ${action}`);
3652
- if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
3948
+ if (!entry) return fail17(`Unknown action: ${action}`);
3949
+ if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
3653
3950
  const result = await sync.callAction(
3654
3951
  IMAGES_TOOL_ID,
3655
3952
  entry.action,
@@ -3669,9 +3966,9 @@ ${JSON.stringify(slim)}
3669
3966
  );
3670
3967
  if (detail.error) lines.push(`Error: ${detail.error}`);
3671
3968
  }
3672
- return text14(lines.join("\n"));
3969
+ return text15(lines.join("\n"));
3673
3970
  } catch (e) {
3674
- return fail16(errMsg16(e));
3971
+ return fail17(errMsg17(e));
3675
3972
  }
3676
3973
  }
3677
3974
  );
@@ -3680,12 +3977,12 @@ ${JSON.stringify(slim)}
3680
3977
  {
3681
3978
  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.",
3682
3979
  inputSchema: {
3683
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3684
- network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3685
- blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
3686
- massAction: import_zod16.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."),
3687
- clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
3688
- clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
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.")
3689
3986
  }
3690
3987
  },
3691
3988
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3712,17 +4009,17 @@ ${JSON.stringify(slim)}
3712
4009
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3713
4010
  done.push(r?.message ?? "expo caches cleared");
3714
4011
  }
3715
- if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3716
- return text14(`Applied: ${done.join(" \xB7 ")}`);
4012
+ if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4013
+ return text15(`Applied: ${done.join(" \xB7 ")}`);
3717
4014
  } catch (e) {
3718
- return fail16(errMsg16(e));
4015
+ return fail17(errMsg17(e));
3719
4016
  }
3720
4017
  }
3721
4018
  );
3722
4019
  }
3723
4020
 
3724
4021
  // src/tools/packs/assets.ts
3725
- var import_zod17 = require("zod");
4022
+ var import_zod18 = require("zod");
3726
4023
 
3727
4024
  // src/tools/packs/assetsGraphNote.ts
3728
4025
  function graphUnavailableNote(ss) {
@@ -3737,9 +4034,9 @@ function graphUnavailableNote(ss) {
3737
4034
 
3738
4035
  // src/tools/packs/assets.ts
3739
4036
  var ASSETS_TOOL_ID = "assets";
3740
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3741
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3742
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
4037
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4038
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4039
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3743
4040
  function kb2(bytes) {
3744
4041
  if (bytes == null || bytes <= 0) return "\u2013";
3745
4042
  if (bytes < 1024) return `${bytes}B`;
@@ -3827,10 +4124,10 @@ function registerAssetsTools(server, sync) {
3827
4124
  {
3828
4125
  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.",
3829
4126
  inputSchema: {
3830
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3831
- limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3832
- kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3833
- unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
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.")
3834
4131
  }
3835
4132
  },
3836
4133
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3843,7 +4140,7 @@ function registerAssetsTools(server, sync) {
3843
4140
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3844
4141
  target
3845
4142
  );
3846
- if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4143
+ if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3847
4144
  if (unusedOnly) {
3848
4145
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3849
4146
  }
@@ -3867,13 +4164,13 @@ function registerAssetsTools(server, sync) {
3867
4164
  sizeSource: r.sizeSource
3868
4165
  }))
3869
4166
  };
3870
- return text15(`${md}
4167
+ return text16(`${md}
3871
4168
 
3872
4169
  \`\`\`json
3873
4170
  ${JSON.stringify(slim)}
3874
4171
  \`\`\``);
3875
4172
  } catch (e) {
3876
- return fail17(errMsg17(e));
4173
+ return fail18(errMsg18(e));
3877
4174
  }
3878
4175
  }
3879
4176
  );
@@ -3882,9 +4179,9 @@ ${JSON.stringify(slim)}
3882
4179
  {
3883
4180
  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).",
3884
4181
  inputSchema: {
3885
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3886
- action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3887
- id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
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).")
3888
4185
  }
3889
4186
  },
3890
4187
  async ({ deviceId, action, id }) => {
@@ -3894,19 +4191,19 @@ ${JSON.stringify(slim)}
3894
4191
  case "rescan": {
3895
4192
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3896
4193
  const ss = result?.scanStatus;
3897
- if (!ss) return fail17("Device returned nothing for rescan.");
3898
- return text15(
4194
+ if (!ss) return fail18("Device returned nothing for rescan.");
4195
+ return text16(
3899
4196
  `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" : "")
3900
4197
  );
3901
4198
  }
3902
4199
  case "measure_sizes": {
3903
4200
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3904
- return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4201
+ return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3905
4202
  }
3906
4203
  case "detail": {
3907
- if (id == null) return fail17("detail requires the `id` param (from get_assets).");
4204
+ if (id == null) return fail18("detail requires the `id` param (from get_assets).");
3908
4205
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3909
- if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4206
+ if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3910
4207
  const lines = [
3911
4208
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3912
4209
  "",
@@ -3924,32 +4221,32 @@ ${JSON.stringify(slim)}
3924
4221
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3925
4222
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3926
4223
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3927
- return text15(lines.join("\n"));
4224
+ return text16(lines.join("\n"));
3928
4225
  }
3929
4226
  case "save_baseline": {
3930
4227
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3931
- return text15(
4228
+ return text16(
3932
4229
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3933
4230
  );
3934
4231
  }
3935
4232
  case "clear_baseline": {
3936
4233
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3937
- return text15("Baseline cleared.");
4234
+ return text16("Baseline cleared.");
3938
4235
  }
3939
4236
  case "clear_records": {
3940
4237
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3941
- return text15(result?.message ?? "Inventory cleared.");
4238
+ return text16(result?.message ?? "Inventory cleared.");
3942
4239
  }
3943
4240
  }
3944
4241
  } catch (e) {
3945
- return fail17(errMsg17(e));
4242
+ return fail18(errMsg18(e));
3946
4243
  }
3947
4244
  }
3948
4245
  );
3949
4246
  }
3950
4247
 
3951
4248
  // src/tools/packs/reload.ts
3952
- var import_zod18 = require("zod");
4249
+ var import_zod19 = require("zod");
3953
4250
 
3954
4251
  // src/capture/devServerReload.ts
3955
4252
  var import_ws = require("ws");
@@ -4012,9 +4309,9 @@ var DEFAULT_WAIT_MS = 3e4;
4012
4309
  var TEARDOWN_GRACE_MS = 1200;
4013
4310
  var PROBE_TIMEOUT_MS = 1500;
4014
4311
  var PROBE_INTERVAL_MS = 400;
4015
- var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4016
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4017
- var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
4312
+ var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4313
+ var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4314
+ var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4018
4315
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
4019
4316
  async function ping(sync, deviceId, timeoutMs) {
4020
4317
  try {
@@ -4053,12 +4350,12 @@ function registerReloadTools(server, sync) {
4053
4350
  {
4054
4351
  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.",
4055
4352
  inputSchema: {
4056
- strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4353
+ strategy: import_zod19.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4057
4354
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
4058
4355
  ),
4059
- wait: import_zod18.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4060
- timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4061
- deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
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.')
4062
4359
  }
4063
4360
  },
4064
4361
  async (args) => {
@@ -4100,32 +4397,32 @@ function registerReloadTools(server, sync) {
4100
4397
  }
4101
4398
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
4102
4399
  if (args.wait === false) {
4103
- return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4400
+ return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4104
4401
  }
4105
4402
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4106
4403
  if (elapsed == null) {
4107
- return fail18(
4404
+ return fail19(
4108
4405
  `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.`
4109
4406
  );
4110
4407
  }
4111
- return text16(
4408
+ return text17(
4112
4409
  `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.`
4113
4410
  );
4114
4411
  } catch (e) {
4115
- return fail18(errMsg18(e));
4412
+ return fail19(errMsg19(e));
4116
4413
  }
4117
4414
  }
4118
4415
  );
4119
4416
  }
4120
4417
 
4121
4418
  // src/tools/packs/triage.ts
4122
- var import_zod19 = require("zod");
4123
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4124
- var fail19 = (message) => ({
4419
+ var import_zod20 = require("zod");
4420
+ var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4421
+ var fail20 = (message) => ({
4125
4422
  isError: true,
4126
4423
  content: [{ type: "text", text: message }]
4127
4424
  });
4128
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4425
+ var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4129
4426
  var PROBE_TIMEOUT_MS2 = 5e3;
4130
4427
  var MAX_STORAGE_KEYS = 20;
4131
4428
  var MAX_STORAGE_VALUE = 400;
@@ -4162,7 +4459,7 @@ async function section(title, run) {
4162
4459
  return body ? `${title}
4163
4460
  ${body}` : null;
4164
4461
  } catch (e) {
4165
- const msg = errMsg19(e);
4462
+ const msg = errMsg20(e);
4166
4463
  if (/not responding|timed out|got no answer/i.test(msg)) {
4167
4464
  return `${title}
4168
4465
  (no answer \u2014 see the note above)`;
@@ -4177,7 +4474,7 @@ function registerTriageTools(server, sync) {
4177
4474
  {
4178
4475
  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.",
4179
4476
  inputSchema: {
4180
- deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4477
+ deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4181
4478
  }
4182
4479
  },
4183
4480
  async (args) => {
@@ -4284,7 +4581,7 @@ _Values: get_storage includeValues:true_`;
4284
4581
 
4285
4582
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4286
4583
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4287
- return text17(
4584
+ return text18(
4288
4585
  `${head}${banner}
4289
4586
 
4290
4587
  ${body}
@@ -4292,22 +4589,22 @@ ${body}
4292
4589
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4293
4590
  );
4294
4591
  } catch (e) {
4295
- return fail19(errMsg19(e));
4592
+ return fail20(errMsg20(e));
4296
4593
  }
4297
4594
  }
4298
4595
  );
4299
4596
  }
4300
4597
 
4301
4598
  // src/tools/packs/flow.ts
4302
- var import_zod20 = require("zod");
4599
+ var import_zod21 = require("zod");
4303
4600
  var UI_TOOL_ID = "highlight-updates";
4304
4601
  var APP_TOOL_ID2 = "app";
4305
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4306
- var fail20 = (message) => ({
4602
+ var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4603
+ var fail21 = (message) => ({
4307
4604
  isError: true,
4308
4605
  content: [{ type: "text", text: message }]
4309
4606
  });
4310
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4607
+ var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4311
4608
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4312
4609
  var POLL_MS = 250;
4313
4610
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4324,19 +4621,19 @@ function formatElement2(e) {
4324
4621
  const display = e.text ?? e.label;
4325
4622
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4326
4623
  }
4327
- var stepSchema = import_zod20.z.object({
4328
- tap: import_zod20.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4329
- longPress: import_zod20.z.boolean().optional().describe("Make `tap` a long-press instead."),
4330
- type: import_zod20.z.string().optional().describe("Text to type (needs `into`)."),
4331
- into: import_zod20.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4332
- set: import_zod20.z.union([import_zod20.z.number(), import_zod20.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4333
- reload: import_zod20.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4334
- waitMs: import_zod20.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4335
- waitFor: import_zod20.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4336
- waitGone: import_zod20.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4337
- expect: import_zod20.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4338
- expectGone: import_zod20.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4339
- timeoutMs: import_zod20.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
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).")
4340
4637
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4341
4638
  function registerFlowTools(server, sync) {
4342
4639
  server.registerTool(
@@ -4344,10 +4641,10 @@ function registerFlowTools(server, sync) {
4344
4641
  {
4345
4642
  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.",
4346
4643
  inputSchema: {
4347
- steps: import_zod20.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4348
- include: import_zod20.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."),
4349
- continueOnFailure: import_zod20.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4350
- deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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).")
4351
4648
  }
4352
4649
  },
4353
4650
  async (args) => {
@@ -4519,7 +4816,7 @@ function registerFlowTools(server, sync) {
4519
4816
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
4520
4817
  }
4521
4818
  } catch (e) {
4522
- stepErrors.push(errMsg20(e));
4819
+ stepErrors.push(errMsg21(e));
4523
4820
  }
4524
4821
  if (stepErrors.length) {
4525
4822
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -4542,7 +4839,7 @@ function registerFlowTools(server, sync) {
4542
4839
  ${shown.map(formatElement2).join("\n")}`
4543
4840
  );
4544
4841
  } catch (e) {
4545
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg20(e))}`);
4842
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
4546
4843
  }
4547
4844
  }
4548
4845
  if (include === "all") {
@@ -4571,9 +4868,9 @@ ${shown.map(formatElement2).join("\n")}`
4571
4868
  }
4572
4869
  }
4573
4870
  }
4574
- return text18(parts.join("\n\n"));
4871
+ return text19(parts.join("\n\n"));
4575
4872
  } catch (e) {
4576
- return fail20(errMsg20(e));
4873
+ return fail21(errMsg21(e));
4577
4874
  }
4578
4875
  }
4579
4876
  );
@@ -4594,25 +4891,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4594
4891
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
4595
4892
 
4596
4893
  // src/tools/packs/renders.ts
4597
- var import_zod21 = require("zod");
4894
+ var import_zod22 = require("zod");
4598
4895
  var UI_TOOL_ID2 = "highlight-updates";
4599
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4600
- var fail21 = (message) => ({
4896
+ var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4897
+ var fail22 = (message) => ({
4601
4898
  isError: true,
4602
4899
  content: [{ type: "text", text: message }]
4603
4900
  });
4604
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4901
+ var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
4605
4902
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
4606
4903
  var DEFAULT_IDLE_MS = 3e3;
4607
4904
  var SETTLE_MS = 400;
4608
4905
  var DEFAULT_TOP = 12;
4609
4906
  var lastCapture = /* @__PURE__ */ new Map();
4610
- var stepSchema2 = import_zod21.z.object({
4611
- tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4612
- type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
4613
- into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4614
- waitMs: import_zod21.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4615
- repeat: import_zod21.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
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'.")
4616
4913
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
4617
4914
  function selector2(needle) {
4618
4915
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -4705,14 +5002,14 @@ function registerRenderTools(server, sync) {
4705
5002
  {
4706
5003
  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.",
4707
5004
  inputSchema: {
4708
- steps: import_zod21.z.array(stepSchema2).max(20).optional().describe(
5005
+ steps: import_zod22.z.array(stepSchema2).max(20).optional().describe(
4709
5006
  "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."
4710
5007
  ),
4711
- idleMs: import_zod21.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
4712
- label: import_zod21.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
4713
- compareToPrevious: import_zod21.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
4714
- top: import_zod21.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
4715
- deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
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).")
4716
5013
  }
4717
5014
  },
4718
5015
  async (args) => {
@@ -4721,18 +5018,18 @@ function registerRenderTools(server, sync) {
4721
5018
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
4722
5019
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
4723
5020
  if (!uiTool) {
4724
- return fail21(
5021
+ return fail22(
4725
5022
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
4726
5023
  );
4727
5024
  }
4728
5025
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
4729
- return fail21(
5026
+ return fail22(
4730
5027
  "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."
4731
5028
  );
4732
5029
  }
4733
5030
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
4734
5031
  if (began && began.ok === false) {
4735
- return fail21(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5032
+ return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4736
5033
  }
4737
5034
  await delay5(SETTLE_MS);
4738
5035
  const startedAt = Date.now();
@@ -4777,7 +5074,7 @@ function registerRenderTools(server, sync) {
4777
5074
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
4778
5075
  const summary = ended?.summary;
4779
5076
  if (!summary) {
4780
- return fail21(
5077
+ return fail22(
4781
5078
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
4782
5079
  );
4783
5080
  }
@@ -4802,9 +5099,9 @@ function registerRenderTools(server, sync) {
4802
5099
  } else if (!args.compareToPrevious) {
4803
5100
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
4804
5101
  }
4805
- return text19(parts.join("\n"));
5102
+ return text20(parts.join("\n"));
4806
5103
  } catch (e) {
4807
- return fail21(errMsg21(e));
5104
+ return fail22(errMsg22(e));
4808
5105
  }
4809
5106
  }
4810
5107
  );
@@ -5025,6 +5322,7 @@ require the connected app to have an active Buoy Pro license. If they error with
5025
5322
  required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
5026
5323
  var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
5027
5324
  "measure_renders",
5325
+ "scenario_action",
5028
5326
  "run_flow",
5029
5327
  "run_benchmark_batch",
5030
5328
  "get_triage",
@@ -5075,6 +5373,7 @@ function createMcpServer(sync) {
5075
5373
  registerRoutesTools(server, sync);
5076
5374
  registerConsoleTools(server, sync);
5077
5375
  registerTimeMachineTools(server, sync);
5376
+ registerScenariosTools(server, sync);
5078
5377
  registerScreenshotTools(server, sync);
5079
5378
  registerInteractTools(server, sync);
5080
5379
  registerJsThreadTools(server, sync);
@@ -5084,7 +5383,7 @@ function createMcpServer(sync) {
5084
5383
  return server;
5085
5384
  }
5086
5385
  var deviceIdField = {
5087
- deviceId: import_zod22.z.string().optional().describe(
5386
+ deviceId: import_zod23.z.string().optional().describe(
5088
5387
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5089
5388
  )
5090
5389
  };