@buoy-gg/mcp 7.0.30 → 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/cli.js CHANGED
@@ -640,7 +640,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
640
640
 
641
641
  // src/server.ts
642
642
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
643
- var import_zod22 = require("zod");
643
+ var import_zod23 = require("zod");
644
644
 
645
645
  // src/tools/generic.ts
646
646
  var import_zod = require("zod");
@@ -651,7 +651,7 @@ var import_node_os = __toESM(require("os"));
651
651
  var import_node_path = __toESM(require("path"));
652
652
 
653
653
  // src/version.ts
654
- var VERSION = "7.0.30" ? "7.0.30" : "0.0.0-dev";
654
+ var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
655
655
  var PACKAGE_NAME = "@buoy-gg/mcp";
656
656
 
657
657
  // src/update/checkForUpdate.ts
@@ -2911,8 +2911,303 @@ ${lines.join("\n")}`);
2911
2911
  );
2912
2912
  }
2913
2913
 
2914
- // src/tools/packs/screenshot.ts
2914
+ // src/tools/packs/scenarios.ts
2915
2915
  var import_zod13 = require("zod");
2916
+ var SCENARIOS_TOOL_ID = "scenarios";
2917
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2918
+ var fail13 = (message) => ({
2919
+ isError: true,
2920
+ content: [{ type: "text", text: message }]
2921
+ });
2922
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2923
+ function varSpec(v) {
2924
+ const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
2925
+ const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
2926
+ return `${v.key}: ${v.type}${opts}${def}`;
2927
+ }
2928
+ function summarize(s) {
2929
+ const vars = s.vars?.length ? `
2930
+ variables: ${s.vars.map(varSpec).join("; ")}` : "";
2931
+ const steps = s.stepLines?.length ? `
2932
+ ${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
2933
+ const meta = [
2934
+ `v${s.version}`,
2935
+ s.author,
2936
+ s.usage?.runs ? `${s.usage.runs} run(s)` : null,
2937
+ s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
2938
+ s.unsavedToRepo ? "unsaved to repo" : null
2939
+ ].filter(Boolean).join(" \xB7 ");
2940
+ const outcome = s.expectedOutcome ? `
2941
+ expected: ${s.expectedOutcome}` : "";
2942
+ return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
2943
+ }
2944
+ function renderSection(title, list, folders) {
2945
+ if (!list.length) return `**${title} (0):**
2946
+ _none_`;
2947
+ if (!folders.length) {
2948
+ return `**${title} (${list.length}):**
2949
+ ${list.map(summarize).join("\n")}`;
2950
+ }
2951
+ const parts = [`**${title} (${list.length}):**`];
2952
+ for (const folder of folders) {
2953
+ const inIt = list.filter(
2954
+ (s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
2955
+ );
2956
+ if (!inIt.length) continue;
2957
+ parts.push(`_${folder} (${inIt.length})_
2958
+ ${inIt.map(summarize).join("\n")}`);
2959
+ }
2960
+ const ungrouped = list.filter((s) => !s.folder);
2961
+ if (ungrouped.length) {
2962
+ parts.push(
2963
+ `_Ungrouped (${ungrouped.length})_
2964
+ ${ungrouped.map(summarize).join("\n")}`
2965
+ );
2966
+ }
2967
+ return parts.join("\n");
2968
+ }
2969
+ function formatActive(a) {
2970
+ const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
2971
+ return [
2972
+ `**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
2973
+ vars ? `variables: ${vars}` : null,
2974
+ `effects:
2975
+ ${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
2976
+ ].filter(Boolean).join("\n");
2977
+ }
2978
+ function registerScenariosTools(server, sync) {
2979
+ server.registerTool(
2980
+ "get_scenarios",
2981
+ {
2982
+ 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.",
2983
+ inputSchema: {
2984
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2985
+ }
2986
+ },
2987
+ async (args) => {
2988
+ try {
2989
+ const target = sync.requireProDevice(args.deviceId);
2990
+ const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
2991
+ const p = payload ?? {};
2992
+ const code = p.code ?? [];
2993
+ const device = p.device ?? [];
2994
+ const drafts = p.drafts ?? [];
2995
+ const folders = p.folders ?? [];
2996
+ const parts = [];
2997
+ parts.push(
2998
+ p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
2999
+ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
3000
+ );
3001
+ if (folders.length) {
3002
+ parts.push(
3003
+ `**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.`
3004
+ );
3005
+ }
3006
+ parts.push(renderSection("Shipped with this app", code, folders));
3007
+ parts.push(renderSection("Saved on this device", device, folders));
3008
+ if (drafts.length) {
3009
+ parts.push(
3010
+ `**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
3011
+ ${drafts.map(summarize).join("\n")}`
3012
+ );
3013
+ }
3014
+ return text12(parts.join("\n\n"));
3015
+ } catch (e) {
3016
+ return fail13(errMsg13(e));
3017
+ }
3018
+ }
3019
+ );
3020
+ server.registerTool(
3021
+ "scenario_action",
3022
+ {
3023
+ 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.`,
3024
+ inputSchema: {
3025
+ action: import_zod13.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3026
+ folder: import_zod13.z.string().optional().describe(
3027
+ "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."
3028
+ ),
3029
+ id: import_zod13.z.string().optional().describe(
3030
+ "Scenario id (required for preview/run/delete; from get_scenarios)."
3031
+ ),
3032
+ params: import_zod13.z.record(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional().describe(
3033
+ "Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
3034
+ ),
3035
+ scenario: import_zod13.z.object({
3036
+ id: import_zod13.z.string().describe("Stable kebab-case id."),
3037
+ name: import_zod13.z.string().describe("Human name; may contain {{variable}} placeholders."),
3038
+ description: import_zod13.z.string().optional(),
3039
+ expectedOutcome: import_zod13.z.string().optional().describe(
3040
+ "What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
3041
+ ),
3042
+ author: import_zod13.z.string().optional(),
3043
+ authoredAgainst: import_zod13.z.string().optional().describe("App version this was written against."),
3044
+ folder: import_zod13.z.string().optional().describe(
3045
+ 'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
3046
+ ),
3047
+ tags: import_zod13.z.array(import_zod13.z.string()).optional(),
3048
+ vars: import_zod13.z.array(
3049
+ import_zod13.z.object({
3050
+ key: import_zod13.z.string(),
3051
+ label: import_zod13.z.string().optional(),
3052
+ type: import_zod13.z.enum(["string", "number", "boolean", "enum"]),
3053
+ options: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional(),
3054
+ default: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]).optional()
3055
+ })
3056
+ ).optional().describe(
3057
+ "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."
3058
+ ),
3059
+ steps: import_zod13.z.array(
3060
+ import_zod13.z.object({
3061
+ id: import_zod13.z.string().optional(),
3062
+ tool: import_zod13.z.string().describe(
3063
+ "Tool id: network | storage | impersonate | route-events | query | time-machine"
3064
+ ),
3065
+ action: import_zod13.z.string().describe(
3066
+ "Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
3067
+ ),
3068
+ params: import_zod13.z.unknown().optional(),
3069
+ label: import_zod13.z.string().optional()
3070
+ })
3071
+ ).describe("Steps, in order."),
3072
+ undoSteps: import_zod13.z.array(
3073
+ import_zod13.z.object({
3074
+ id: import_zod13.z.string().optional(),
3075
+ tool: import_zod13.z.string(),
3076
+ action: import_zod13.z.string(),
3077
+ params: import_zod13.z.unknown().optional(),
3078
+ label: import_zod13.z.string().optional()
3079
+ })
3080
+ ).optional().describe(
3081
+ "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."
3082
+ )
3083
+ }).optional().describe("The scenario to save (action 'save')."),
3084
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3085
+ }
3086
+ },
3087
+ async (args) => {
3088
+ try {
3089
+ const target = sync.requireProDevice(args.deviceId);
3090
+ const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
3091
+ switch (args.action) {
3092
+ case "preview": {
3093
+ if (!args.id) return fail13("preview requires `id` (from get_scenarios).");
3094
+ const r = await call("preview", {
3095
+ id: args.id,
3096
+ params: args.params ?? {}
3097
+ });
3098
+ const lines = [
3099
+ `**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
3100
+ `variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
3101
+ `**Would do:**
3102
+ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
3103
+ ];
3104
+ if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
3105
+ if (r.conflict) {
3106
+ lines.push(
3107
+ `\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.`
3108
+ );
3109
+ }
3110
+ if (r.warnings?.length) lines.push(`**Warnings:**
3111
+ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
3112
+ lines.push(
3113
+ r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
3114
+ ${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
3115
+ );
3116
+ return text12(lines.join("\n\n"));
3117
+ }
3118
+ case "run": {
3119
+ if (!args.id) return fail13("run requires `id` (from get_scenarios).");
3120
+ const r = await call("run", {
3121
+ id: args.id,
3122
+ params: args.params ?? {}
3123
+ });
3124
+ if (r.preflightErrors?.length) {
3125
+ return fail13(
3126
+ `Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
3127
+ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3128
+ );
3129
+ }
3130
+ const steps = (r.steps ?? []).map((s) => {
3131
+ const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
3132
+ const err = s.error ? `
3133
+ ${s.error}` : "";
3134
+ return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
3135
+ }).join("\n");
3136
+ if (!r.ok) {
3137
+ return fail13(
3138
+ `Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
3139
+ ${steps}
3140
+
3141
+ The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
3142
+ );
3143
+ }
3144
+ return text12(
3145
+ [
3146
+ `\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
3147
+ steps,
3148
+ `**Now bent:**
3149
+ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
3150
+ "The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
3151
+ ].join("\n\n")
3152
+ );
3153
+ }
3154
+ case "deactivate": {
3155
+ const r = await call("deactivate", {});
3156
+ const rep = r.report;
3157
+ const lines = [
3158
+ `Removed ${rep.removedRules} override rule(s)`,
3159
+ rep.stoppedImpersonation ? "Ended impersonation" : null,
3160
+ rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
3161
+ ].filter(Boolean);
3162
+ const warn = rep.unrestoredStorageKeys.length ? `
3163
+
3164
+ \u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
3165
+ const errs = rep.errors.length ? `
3166
+
3167
+ Errors: ${rep.errors.join("; ")}` : "";
3168
+ return text12(`**Deactivated.**
3169
+ ${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
3170
+ }
3171
+ case "save": {
3172
+ if (!args.scenario) return fail13("save requires `scenario`.");
3173
+ const r = await call("save", { scenario: args.scenario });
3174
+ if (!r.ok) {
3175
+ return fail13(
3176
+ `Save rejected:
3177
+ ${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
3178
+ );
3179
+ }
3180
+ return text12(
3181
+ `Saved \`${r.id}\` **as a draft**.
3182
+
3183
+ 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.`
3184
+ );
3185
+ }
3186
+ case "setFolder": {
3187
+ if (!args.id) return fail13("setFolder requires `id` (from get_scenarios).");
3188
+ const r = await call("setFolder", {
3189
+ id: args.id,
3190
+ folder: args.folder
3191
+ });
3192
+ return text12(
3193
+ r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
3194
+ );
3195
+ }
3196
+ case "delete": {
3197
+ if (!args.id) return fail13("delete requires `id`.");
3198
+ await call("delete", { id: args.id });
3199
+ return text12(`Deleted \`${args.id}\` from the device library.`);
3200
+ }
3201
+ }
3202
+ } catch (e) {
3203
+ return fail13(errMsg13(e));
3204
+ }
3205
+ }
3206
+ );
3207
+ }
3208
+
3209
+ // src/tools/packs/screenshot.ts
3210
+ var import_zod14 = require("zod");
2916
3211
 
2917
3212
  // src/capture/simctl.ts
2918
3213
  var import_node_child_process = require("child_process");
@@ -3026,8 +3321,8 @@ function frameToRect(frame, screen) {
3026
3321
  var TOOL_ID = "highlight-updates";
3027
3322
  var MARGIN_PT = 12;
3028
3323
  var CAPTURE_PADDING_PT = 6;
3029
- var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3030
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
3324
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3325
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3031
3326
  function reasonMessage(res, query) {
3032
3327
  switch (res.reason) {
3033
3328
  case "no-renders":
@@ -3057,10 +3352,10 @@ function registerScreenshotTools(server, sync) {
3057
3352
  {
3058
3353
  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.",
3059
3354
  inputSchema: {
3060
- query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3061
- nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3062
- scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3063
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3355
+ query: import_zod14.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3356
+ nativeTag: import_zod14.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3357
+ scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3358
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3064
3359
  }
3065
3360
  },
3066
3361
  async (args) => {
@@ -3102,11 +3397,11 @@ function registerScreenshotTools(server, sync) {
3102
3397
  }
3103
3398
  }
3104
3399
  if (!located.matched || !located.rect || !located.screen) {
3105
- return fail13(reasonMessage(located, args.query) + candidatesText(located.candidates));
3400
+ return fail14(reasonMessage(located, args.query) + candidatesText(located.candidates));
3106
3401
  }
3107
3402
  const shot = await captureIosFramebuffer();
3108
3403
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
3109
- return fail13(shot.error ?? "Screenshot capture failed.");
3404
+ return fail14(shot.error ?? "Screenshot capture failed.");
3110
3405
  }
3111
3406
  const pxScale = shot.width / located.screen.width;
3112
3407
  const r = located.rect;
@@ -3122,7 +3417,7 @@ function registerScreenshotTools(server, sync) {
3122
3417
  }
3123
3418
  );
3124
3419
  if (!crop.ok || !crop.buffer) {
3125
- return fail13(crop.error ?? "Crop failed.");
3420
+ return fail14(crop.error ?? "Crop failed.");
3126
3421
  }
3127
3422
  const label = located.componentName || "component";
3128
3423
  const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
@@ -3135,21 +3430,21 @@ function registerScreenshotTools(server, sync) {
3135
3430
  ]
3136
3431
  };
3137
3432
  } catch (e) {
3138
- return fail13(errMsg13(e));
3433
+ return fail14(errMsg14(e));
3139
3434
  }
3140
3435
  }
3141
3436
  );
3142
3437
  }
3143
3438
 
3144
3439
  // src/tools/packs/interact.ts
3145
- var import_zod14 = require("zod");
3440
+ var import_zod15 = require("zod");
3146
3441
  var TOOL_ID2 = "highlight-updates";
3147
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
3148
- var fail14 = (message) => ({
3442
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3443
+ var fail15 = (message) => ({
3149
3444
  isError: true,
3150
3445
  content: [{ type: "text", text: message }]
3151
3446
  });
3152
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3447
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3153
3448
  function formatElement(e) {
3154
3449
  let line = e.interactive ? "\u2022 " : " ";
3155
3450
  line += e.name;
@@ -3172,8 +3467,8 @@ function registerInteractTools(server, sync) {
3172
3467
  {
3173
3468
  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.",
3174
3469
  inputSchema: {
3175
- interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3176
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3470
+ interactiveOnly: import_zod15.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3471
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3177
3472
  }
3178
3473
  },
3179
3474
  async (args) => {
@@ -3183,16 +3478,16 @@ function registerInteractTools(server, sync) {
3183
3478
  const all = res?.elements ?? [];
3184
3479
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3185
3480
  if (elements.length === 0) {
3186
- return text12(
3481
+ return text13(
3187
3482
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
3188
3483
  );
3189
3484
  }
3190
3485
  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 }).`;
3191
- return text12(`${header}
3486
+ return text13(`${header}
3192
3487
 
3193
3488
  ${elements.map(formatElement).join("\n")}`);
3194
3489
  } catch (e) {
3195
- return fail14(errMsg14(e));
3490
+ return fail15(errMsg15(e));
3196
3491
  }
3197
3492
  }
3198
3493
  );
@@ -3201,20 +3496,20 @@ ${elements.map(formatElement).join("\n")}`);
3201
3496
  {
3202
3497
  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.",
3203
3498
  inputSchema: {
3204
- nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3205
- testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
3206
- query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3207
- 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)."),
3208
- text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3209
- 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."),
3210
- scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3211
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3499
+ nativeTag: import_zod15.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3500
+ testID: import_zod15.z.string().optional().describe("Exact testID of the element."),
3501
+ query: import_zod15.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3502
+ 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)."),
3503
+ text: import_zod15.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3504
+ 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."),
3505
+ scrollIntoView: import_zod15.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3506
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3212
3507
  }
3213
3508
  },
3214
3509
  async (args) => {
3215
3510
  try {
3216
3511
  if (args.nativeTag == null && !args.testID && !args.query) {
3217
- return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
3512
+ return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
3218
3513
  }
3219
3514
  const target = sync.requireProDevice(args.deviceId);
3220
3515
  const res = await sync.callAction(
@@ -3239,26 +3534,26 @@ ${elements.map(formatElement).join("\n")}`);
3239
3534
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3240
3535
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3241
3536
  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}`;
3242
- return text12(`${verb}${scrolledNote}.${flowTip}`);
3537
+ return text13(`${verb}${scrolledNote}.${flowTip}`);
3243
3538
  }
3244
3539
  let msg = res?.reason ?? "Tap failed.";
3245
3540
  if (res?.candidates?.length) {
3246
3541
  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");
3247
3542
  }
3248
- return fail14(msg);
3543
+ return fail15(msg);
3249
3544
  } catch (e) {
3250
- return fail14(errMsg14(e));
3545
+ return fail15(errMsg15(e));
3251
3546
  }
3252
3547
  }
3253
3548
  );
3254
3549
  }
3255
3550
 
3256
3551
  // src/tools/packs/jsThread.ts
3257
- var import_zod15 = require("zod");
3552
+ var import_zod16 = require("zod");
3258
3553
  var JS_TOP_TOOL_ID = "js-top";
3259
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3260
- var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3261
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3554
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3555
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3556
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3262
3557
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3263
3558
  function sparkline(values) {
3264
3559
  const max = Math.max(1, ...values);
@@ -3329,9 +3624,9 @@ function registerJsThreadTools(server, sync) {
3329
3624
  {
3330
3625
  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.',
3331
3626
  inputSchema: {
3332
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3333
- durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3334
- clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3627
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3628
+ durationMs: import_zod16.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3629
+ clearFirst: import_zod16.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3335
3630
  }
3336
3631
  },
3337
3632
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -3344,7 +3639,7 @@ function registerJsThreadTools(server, sync) {
3344
3639
  { durationMs: duration, clearFirst: clearFirst === true },
3345
3640
  target
3346
3641
  );
3347
- if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3642
+ if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3348
3643
  const md = formatTopMarkdown(snap, duration);
3349
3644
  const slim = {
3350
3645
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -3363,13 +3658,13 @@ function registerJsThreadTools(server, sync) {
3363
3658
  worstFreezeMs: r.worstFreezeMs
3364
3659
  }))
3365
3660
  };
3366
- return text13(`${md}
3661
+ return text14(`${md}
3367
3662
 
3368
3663
  \`\`\`json
3369
3664
  ${JSON.stringify(slim)}
3370
3665
  \`\`\``);
3371
3666
  } catch (e) {
3372
- return fail15(errMsg15(e));
3667
+ return fail16(errMsg16(e));
3373
3668
  }
3374
3669
  }
3375
3670
  );
@@ -3378,8 +3673,8 @@ ${JSON.stringify(slim)}
3378
3673
  {
3379
3674
  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).',
3380
3675
  inputSchema: {
3381
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3382
- key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
3676
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3677
+ key: import_zod16.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
3383
3678
  }
3384
3679
  },
3385
3680
  async ({ deviceId, key }) => {
@@ -3392,18 +3687,18 @@ ${JSON.stringify(slim)}
3392
3687
  target
3393
3688
  );
3394
3689
  if (!detail) {
3395
- return fail15(
3690
+ return fail16(
3396
3691
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
3397
3692
  );
3398
3693
  }
3399
3694
  const md = formatDetailMarkdown(detail);
3400
- return text13(`${md}
3695
+ return text14(`${md}
3401
3696
 
3402
3697
  \`\`\`json
3403
3698
  ${JSON.stringify(detail)}
3404
3699
  \`\`\``);
3405
3700
  } catch (e) {
3406
- return fail15(errMsg15(e));
3701
+ return fail16(errMsg16(e));
3407
3702
  }
3408
3703
  }
3409
3704
  );
@@ -3412,27 +3707,27 @@ ${JSON.stringify(detail)}
3412
3707
  {
3413
3708
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
3414
3709
  inputSchema: {
3415
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3710
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3416
3711
  }
3417
3712
  },
3418
3713
  async ({ deviceId }) => {
3419
3714
  try {
3420
3715
  const target = sync.requireProDevice(deviceId);
3421
3716
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
3422
- return text13("Cleared js-top stats.");
3717
+ return text14("Cleared js-top stats.");
3423
3718
  } catch (e) {
3424
- return fail15(errMsg15(e));
3719
+ return fail16(errMsg16(e));
3425
3720
  }
3426
3721
  }
3427
3722
  );
3428
3723
  }
3429
3724
 
3430
3725
  // src/tools/packs/images.ts
3431
- var import_zod16 = require("zod");
3726
+ var import_zod17 = require("zod");
3432
3727
  var IMAGES_TOOL_ID = "images";
3433
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3434
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3435
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3728
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3729
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3730
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3436
3731
  function uriTail(uri) {
3437
3732
  if (!uri) return "(no source)";
3438
3733
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -3520,9 +3815,9 @@ function registerImagesTools(server, sync) {
3520
3815
  {
3521
3816
  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.",
3522
3817
  inputSchema: {
3523
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3524
- limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3525
- status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3818
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3819
+ limit: import_zod17.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3820
+ status: import_zod17.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3526
3821
  }
3527
3822
  },
3528
3823
  async ({ deviceId, limit, status }) => {
@@ -3535,7 +3830,7 @@ function registerImagesTools(server, sync) {
3535
3830
  { limit: capped, status },
3536
3831
  target
3537
3832
  );
3538
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3833
+ if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3539
3834
  const md = formatListMarkdown(list, capped);
3540
3835
  const slim = {
3541
3836
  stats: list.stats,
@@ -3562,13 +3857,13 @@ function registerImagesTools(server, sync) {
3562
3857
  layoutShifts: r.layoutShifts
3563
3858
  }))
3564
3859
  };
3565
- return text14(`${md}
3860
+ return text15(`${md}
3566
3861
 
3567
3862
  \`\`\`json
3568
3863
  ${JSON.stringify(slim)}
3569
3864
  \`\`\``);
3570
3865
  } catch (e) {
3571
- return fail16(errMsg16(e));
3866
+ return fail17(errMsg17(e));
3572
3867
  }
3573
3868
  }
3574
3869
  );
@@ -3577,9 +3872,9 @@ ${JSON.stringify(slim)}
3577
3872
  {
3578
3873
  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.",
3579
3874
  inputSchema: {
3580
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3581
- id: import_zod16.z.number().describe("Record id from get_images."),
3582
- action: import_zod16.z.enum([
3875
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3876
+ id: import_zod17.z.number().describe("Record id from get_images."),
3877
+ action: import_zod17.z.enum([
3583
3878
  "hard_reload",
3584
3879
  "retry",
3585
3880
  "evict_disk",
@@ -3591,7 +3886,7 @@ ${JSON.stringify(slim)}
3591
3886
  "swap_url",
3592
3887
  "restore"
3593
3888
  ]).describe("What to do."),
3594
- url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
3889
+ url: import_zod17.z.string().optional().describe("Replacement URL (swap_url only).")
3595
3890
  }
3596
3891
  },
3597
3892
  async ({ deviceId, id, action, url }) => {
@@ -3599,8 +3894,8 @@ ${JSON.stringify(slim)}
3599
3894
  const target = sync.requireProDevice(deviceId);
3600
3895
  if (action === "prove_savings") {
3601
3896
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
3602
- if (!savings) return fail16("Device returned nothing for proveSavings.");
3603
- if (!savings.ok) return fail16(savings.message);
3897
+ if (!savings) return fail17("Device returned nothing for proveSavings.");
3898
+ if (!savings.ok) return fail17(savings.message);
3604
3899
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
3605
3900
  if (savings.originalBytes != null)
3606
3901
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -3612,11 +3907,11 @@ ${JSON.stringify(slim)}
3612
3907
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
3613
3908
  if (savings.optimizedUri)
3614
3909
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
3615
- return text14(parts.join("\n"));
3910
+ return text15(parts.join("\n"));
3616
3911
  }
3617
3912
  const entry = ACTION_MAP[action];
3618
- if (!entry) return fail16(`Unknown action: ${action}`);
3619
- if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
3913
+ if (!entry) return fail17(`Unknown action: ${action}`);
3914
+ if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
3620
3915
  const result = await sync.callAction(
3621
3916
  IMAGES_TOOL_ID,
3622
3917
  entry.action,
@@ -3636,9 +3931,9 @@ ${JSON.stringify(slim)}
3636
3931
  );
3637
3932
  if (detail.error) lines.push(`Error: ${detail.error}`);
3638
3933
  }
3639
- return text14(lines.join("\n"));
3934
+ return text15(lines.join("\n"));
3640
3935
  } catch (e) {
3641
- return fail16(errMsg16(e));
3936
+ return fail17(errMsg17(e));
3642
3937
  }
3643
3938
  }
3644
3939
  );
@@ -3647,12 +3942,12 @@ ${JSON.stringify(slim)}
3647
3942
  {
3648
3943
  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.",
3649
3944
  inputSchema: {
3650
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3651
- network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3652
- blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
3653
- 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."),
3654
- clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
3655
- clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3945
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3946
+ network: import_zod17.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3947
+ blankImages: import_zod17.z.boolean().optional().describe("Render all images without a source."),
3948
+ 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."),
3949
+ clearRegistry: import_zod17.z.boolean().optional().describe("Clear the captured image log."),
3950
+ clearExpoCaches: import_zod17.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3656
3951
  }
3657
3952
  },
3658
3953
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3679,17 +3974,17 @@ ${JSON.stringify(slim)}
3679
3974
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3680
3975
  done.push(r?.message ?? "expo caches cleared");
3681
3976
  }
3682
- if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3683
- return text14(`Applied: ${done.join(" \xB7 ")}`);
3977
+ if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3978
+ return text15(`Applied: ${done.join(" \xB7 ")}`);
3684
3979
  } catch (e) {
3685
- return fail16(errMsg16(e));
3980
+ return fail17(errMsg17(e));
3686
3981
  }
3687
3982
  }
3688
3983
  );
3689
3984
  }
3690
3985
 
3691
3986
  // src/tools/packs/assets.ts
3692
- var import_zod17 = require("zod");
3987
+ var import_zod18 = require("zod");
3693
3988
 
3694
3989
  // src/tools/packs/assetsGraphNote.ts
3695
3990
  function graphUnavailableNote(ss) {
@@ -3704,9 +3999,9 @@ function graphUnavailableNote(ss) {
3704
3999
 
3705
4000
  // src/tools/packs/assets.ts
3706
4001
  var ASSETS_TOOL_ID = "assets";
3707
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3708
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3709
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
4002
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4003
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4004
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3710
4005
  function kb2(bytes) {
3711
4006
  if (bytes == null || bytes <= 0) return "\u2013";
3712
4007
  if (bytes < 1024) return `${bytes}B`;
@@ -3794,10 +4089,10 @@ function registerAssetsTools(server, sync) {
3794
4089
  {
3795
4090
  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.",
3796
4091
  inputSchema: {
3797
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3798
- limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3799
- kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3800
- unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4092
+ deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4093
+ limit: import_zod18.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4094
+ kind: import_zod18.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4095
+ unusedOnly: import_zod18.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3801
4096
  }
3802
4097
  },
3803
4098
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3810,7 +4105,7 @@ function registerAssetsTools(server, sync) {
3810
4105
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3811
4106
  target
3812
4107
  );
3813
- if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4108
+ if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3814
4109
  if (unusedOnly) {
3815
4110
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3816
4111
  }
@@ -3834,13 +4129,13 @@ function registerAssetsTools(server, sync) {
3834
4129
  sizeSource: r.sizeSource
3835
4130
  }))
3836
4131
  };
3837
- return text15(`${md}
4132
+ return text16(`${md}
3838
4133
 
3839
4134
  \`\`\`json
3840
4135
  ${JSON.stringify(slim)}
3841
4136
  \`\`\``);
3842
4137
  } catch (e) {
3843
- return fail17(errMsg17(e));
4138
+ return fail18(errMsg18(e));
3844
4139
  }
3845
4140
  }
3846
4141
  );
@@ -3849,9 +4144,9 @@ ${JSON.stringify(slim)}
3849
4144
  {
3850
4145
  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).",
3851
4146
  inputSchema: {
3852
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3853
- action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3854
- id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
4147
+ deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4148
+ action: import_zod18.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4149
+ id: import_zod18.z.number().optional().describe("Asset id from get_assets (detail only).")
3855
4150
  }
3856
4151
  },
3857
4152
  async ({ deviceId, action, id }) => {
@@ -3861,19 +4156,19 @@ ${JSON.stringify(slim)}
3861
4156
  case "rescan": {
3862
4157
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3863
4158
  const ss = result?.scanStatus;
3864
- if (!ss) return fail17("Device returned nothing for rescan.");
3865
- return text15(
4159
+ if (!ss) return fail18("Device returned nothing for rescan.");
4160
+ return text16(
3866
4161
  `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" : "")
3867
4162
  );
3868
4163
  }
3869
4164
  case "measure_sizes": {
3870
4165
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3871
- return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4166
+ return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3872
4167
  }
3873
4168
  case "detail": {
3874
- if (id == null) return fail17("detail requires the `id` param (from get_assets).");
4169
+ if (id == null) return fail18("detail requires the `id` param (from get_assets).");
3875
4170
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3876
- if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4171
+ if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3877
4172
  const lines = [
3878
4173
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3879
4174
  "",
@@ -3891,32 +4186,32 @@ ${JSON.stringify(slim)}
3891
4186
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3892
4187
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3893
4188
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3894
- return text15(lines.join("\n"));
4189
+ return text16(lines.join("\n"));
3895
4190
  }
3896
4191
  case "save_baseline": {
3897
4192
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3898
- return text15(
4193
+ return text16(
3899
4194
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3900
4195
  );
3901
4196
  }
3902
4197
  case "clear_baseline": {
3903
4198
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3904
- return text15("Baseline cleared.");
4199
+ return text16("Baseline cleared.");
3905
4200
  }
3906
4201
  case "clear_records": {
3907
4202
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3908
- return text15(result?.message ?? "Inventory cleared.");
4203
+ return text16(result?.message ?? "Inventory cleared.");
3909
4204
  }
3910
4205
  }
3911
4206
  } catch (e) {
3912
- return fail17(errMsg17(e));
4207
+ return fail18(errMsg18(e));
3913
4208
  }
3914
4209
  }
3915
4210
  );
3916
4211
  }
3917
4212
 
3918
4213
  // src/tools/packs/reload.ts
3919
- var import_zod18 = require("zod");
4214
+ var import_zod19 = require("zod");
3920
4215
 
3921
4216
  // src/capture/devServerReload.ts
3922
4217
  var import_ws = require("ws");
@@ -3979,9 +4274,9 @@ var DEFAULT_WAIT_MS = 3e4;
3979
4274
  var TEARDOWN_GRACE_MS = 1200;
3980
4275
  var PROBE_TIMEOUT_MS = 1500;
3981
4276
  var PROBE_INTERVAL_MS = 400;
3982
- var text16 = (s) => ({ content: [{ type: "text", text: s }] });
3983
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3984
- var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
4277
+ var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4278
+ var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4279
+ var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
3985
4280
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3986
4281
  async function ping(sync, deviceId, timeoutMs) {
3987
4282
  try {
@@ -4020,12 +4315,12 @@ function registerReloadTools(server, sync) {
4020
4315
  {
4021
4316
  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.",
4022
4317
  inputSchema: {
4023
- strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4318
+ strategy: import_zod19.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4024
4319
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
4025
4320
  ),
4026
- 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."),
4027
- timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4028
- deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4321
+ 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."),
4322
+ timeoutMs: import_zod19.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4323
+ deviceId: import_zod19.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4029
4324
  }
4030
4325
  },
4031
4326
  async (args) => {
@@ -4067,32 +4362,32 @@ function registerReloadTools(server, sync) {
4067
4362
  }
4068
4363
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
4069
4364
  if (args.wait === false) {
4070
- return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4365
+ return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4071
4366
  }
4072
4367
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4073
4368
  if (elapsed == null) {
4074
- return fail18(
4369
+ return fail19(
4075
4370
  `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.`
4076
4371
  );
4077
4372
  }
4078
- return text16(
4373
+ return text17(
4079
4374
  `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.`
4080
4375
  );
4081
4376
  } catch (e) {
4082
- return fail18(errMsg18(e));
4377
+ return fail19(errMsg19(e));
4083
4378
  }
4084
4379
  }
4085
4380
  );
4086
4381
  }
4087
4382
 
4088
4383
  // src/tools/packs/triage.ts
4089
- var import_zod19 = require("zod");
4090
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4091
- var fail19 = (message) => ({
4384
+ var import_zod20 = require("zod");
4385
+ var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4386
+ var fail20 = (message) => ({
4092
4387
  isError: true,
4093
4388
  content: [{ type: "text", text: message }]
4094
4389
  });
4095
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4390
+ var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4096
4391
  var PROBE_TIMEOUT_MS2 = 5e3;
4097
4392
  var MAX_STORAGE_KEYS = 20;
4098
4393
  var MAX_STORAGE_VALUE = 400;
@@ -4129,7 +4424,7 @@ async function section(title, run) {
4129
4424
  return body ? `${title}
4130
4425
  ${body}` : null;
4131
4426
  } catch (e) {
4132
- const msg = errMsg19(e);
4427
+ const msg = errMsg20(e);
4133
4428
  if (/not responding|timed out|got no answer/i.test(msg)) {
4134
4429
  return `${title}
4135
4430
  (no answer \u2014 see the note above)`;
@@ -4144,7 +4439,7 @@ function registerTriageTools(server, sync) {
4144
4439
  {
4145
4440
  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.",
4146
4441
  inputSchema: {
4147
- deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4442
+ deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4148
4443
  }
4149
4444
  },
4150
4445
  async (args) => {
@@ -4251,7 +4546,7 @@ _Values: get_storage includeValues:true_`;
4251
4546
 
4252
4547
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4253
4548
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4254
- return text17(
4549
+ return text18(
4255
4550
  `${head}${banner}
4256
4551
 
4257
4552
  ${body}
@@ -4259,22 +4554,22 @@ ${body}
4259
4554
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4260
4555
  );
4261
4556
  } catch (e) {
4262
- return fail19(errMsg19(e));
4557
+ return fail20(errMsg20(e));
4263
4558
  }
4264
4559
  }
4265
4560
  );
4266
4561
  }
4267
4562
 
4268
4563
  // src/tools/packs/flow.ts
4269
- var import_zod20 = require("zod");
4564
+ var import_zod21 = require("zod");
4270
4565
  var UI_TOOL_ID = "highlight-updates";
4271
4566
  var APP_TOOL_ID2 = "app";
4272
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4273
- var fail20 = (message) => ({
4567
+ var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4568
+ var fail21 = (message) => ({
4274
4569
  isError: true,
4275
4570
  content: [{ type: "text", text: message }]
4276
4571
  });
4277
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4572
+ var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4278
4573
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4279
4574
  var POLL_MS = 250;
4280
4575
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4291,19 +4586,19 @@ function formatElement2(e) {
4291
4586
  const display = e.text ?? e.label;
4292
4587
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4293
4588
  }
4294
- var stepSchema = import_zod20.z.object({
4295
- tap: import_zod20.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4296
- longPress: import_zod20.z.boolean().optional().describe("Make `tap` a long-press instead."),
4297
- type: import_zod20.z.string().optional().describe("Text to type (needs `into`)."),
4298
- into: import_zod20.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4299
- 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."),
4300
- reload: import_zod20.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4301
- waitMs: import_zod20.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4302
- waitFor: import_zod20.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4303
- waitGone: import_zod20.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4304
- 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."),
4305
- 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."),
4306
- 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).")
4589
+ var stepSchema = import_zod21.z.object({
4590
+ tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4591
+ longPress: import_zod21.z.boolean().optional().describe("Make `tap` a long-press instead."),
4592
+ type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
4593
+ into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4594
+ 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."),
4595
+ reload: import_zod21.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4596
+ waitMs: import_zod21.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4597
+ waitFor: import_zod21.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4598
+ waitGone: import_zod21.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4599
+ 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."),
4600
+ 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."),
4601
+ 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).")
4307
4602
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4308
4603
  function registerFlowTools(server, sync) {
4309
4604
  server.registerTool(
@@ -4311,10 +4606,10 @@ function registerFlowTools(server, sync) {
4311
4606
  {
4312
4607
  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.",
4313
4608
  inputSchema: {
4314
- steps: import_zod20.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4315
- 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."),
4316
- continueOnFailure: import_zod20.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4317
- deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4609
+ steps: import_zod21.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4610
+ 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."),
4611
+ continueOnFailure: import_zod21.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4612
+ deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4318
4613
  }
4319
4614
  },
4320
4615
  async (args) => {
@@ -4486,7 +4781,7 @@ function registerFlowTools(server, sync) {
4486
4781
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
4487
4782
  }
4488
4783
  } catch (e) {
4489
- stepErrors.push(errMsg20(e));
4784
+ stepErrors.push(errMsg21(e));
4490
4785
  }
4491
4786
  if (stepErrors.length) {
4492
4787
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -4509,7 +4804,7 @@ function registerFlowTools(server, sync) {
4509
4804
  ${shown.map(formatElement2).join("\n")}`
4510
4805
  );
4511
4806
  } catch (e) {
4512
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg20(e))}`);
4807
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
4513
4808
  }
4514
4809
  }
4515
4810
  if (include === "all") {
@@ -4538,9 +4833,9 @@ ${shown.map(formatElement2).join("\n")}`
4538
4833
  }
4539
4834
  }
4540
4835
  }
4541
- return text18(parts.join("\n\n"));
4836
+ return text19(parts.join("\n\n"));
4542
4837
  } catch (e) {
4543
- return fail20(errMsg20(e));
4838
+ return fail21(errMsg21(e));
4544
4839
  }
4545
4840
  }
4546
4841
  );
@@ -4561,25 +4856,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4561
4856
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
4562
4857
 
4563
4858
  // src/tools/packs/renders.ts
4564
- var import_zod21 = require("zod");
4859
+ var import_zod22 = require("zod");
4565
4860
  var UI_TOOL_ID2 = "highlight-updates";
4566
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4567
- var fail21 = (message) => ({
4861
+ var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4862
+ var fail22 = (message) => ({
4568
4863
  isError: true,
4569
4864
  content: [{ type: "text", text: message }]
4570
4865
  });
4571
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4866
+ var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
4572
4867
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
4573
4868
  var DEFAULT_IDLE_MS = 3e3;
4574
4869
  var SETTLE_MS = 400;
4575
4870
  var DEFAULT_TOP = 12;
4576
4871
  var lastCapture = /* @__PURE__ */ new Map();
4577
- var stepSchema2 = import_zod21.z.object({
4578
- tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4579
- type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
4580
- into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4581
- waitMs: import_zod21.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4582
- repeat: import_zod21.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
4872
+ var stepSchema2 = import_zod22.z.object({
4873
+ tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4874
+ type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
4875
+ into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4876
+ waitMs: import_zod22.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
4877
+ repeat: import_zod22.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
4583
4878
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
4584
4879
  function selector2(needle) {
4585
4880
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -4672,14 +4967,14 @@ function registerRenderTools(server, sync) {
4672
4967
  {
4673
4968
  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.",
4674
4969
  inputSchema: {
4675
- steps: import_zod21.z.array(stepSchema2).max(20).optional().describe(
4970
+ steps: import_zod22.z.array(stepSchema2).max(20).optional().describe(
4676
4971
  "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."
4677
4972
  ),
4678
- idleMs: import_zod21.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
4679
- label: import_zod21.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
4680
- compareToPrevious: import_zod21.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
4681
- top: import_zod21.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
4682
- deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4973
+ idleMs: import_zod22.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
4974
+ label: import_zod22.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
4975
+ compareToPrevious: import_zod22.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
4976
+ top: import_zod22.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
4977
+ deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4683
4978
  }
4684
4979
  },
4685
4980
  async (args) => {
@@ -4688,18 +4983,18 @@ function registerRenderTools(server, sync) {
4688
4983
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
4689
4984
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
4690
4985
  if (!uiTool) {
4691
- return fail21(
4986
+ return fail22(
4692
4987
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
4693
4988
  );
4694
4989
  }
4695
4990
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
4696
- return fail21(
4991
+ return fail22(
4697
4992
  "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."
4698
4993
  );
4699
4994
  }
4700
4995
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
4701
4996
  if (began && began.ok === false) {
4702
- return fail21(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4997
+ return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4703
4998
  }
4704
4999
  await delay5(SETTLE_MS);
4705
5000
  const startedAt = Date.now();
@@ -4744,7 +5039,7 @@ function registerRenderTools(server, sync) {
4744
5039
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
4745
5040
  const summary = ended?.summary;
4746
5041
  if (!summary) {
4747
- return fail21(
5042
+ return fail22(
4748
5043
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
4749
5044
  );
4750
5045
  }
@@ -4769,9 +5064,9 @@ function registerRenderTools(server, sync) {
4769
5064
  } else if (!args.compareToPrevious) {
4770
5065
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
4771
5066
  }
4772
- return text19(parts.join("\n"));
5067
+ return text20(parts.join("\n"));
4773
5068
  } catch (e) {
4774
- return fail21(errMsg21(e));
5069
+ return fail22(errMsg22(e));
4775
5070
  }
4776
5071
  }
4777
5072
  );
@@ -4992,6 +5287,7 @@ require the connected app to have an active Buoy Pro license. If they error with
4992
5287
  required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
4993
5288
  var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
4994
5289
  "measure_renders",
5290
+ "scenario_action",
4995
5291
  "run_flow",
4996
5292
  "run_benchmark_batch",
4997
5293
  "get_triage",
@@ -5042,6 +5338,7 @@ function createMcpServer(sync) {
5042
5338
  registerRoutesTools(server, sync);
5043
5339
  registerConsoleTools(server, sync);
5044
5340
  registerTimeMachineTools(server, sync);
5341
+ registerScenariosTools(server, sync);
5045
5342
  registerScreenshotTools(server, sync);
5046
5343
  registerInteractTools(server, sync);
5047
5344
  registerJsThreadTools(server, sync);
@@ -5051,7 +5348,7 @@ function createMcpServer(sync) {
5051
5348
  return server;
5052
5349
  }
5053
5350
  var deviceIdField = {
5054
- deviceId: import_zod22.z.string().optional().describe(
5351
+ deviceId: import_zod23.z.string().optional().describe(
5055
5352
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5056
5353
  )
5057
5354
  };