@buoy-gg/mcp 7.0.30 → 7.0.32

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_zod24 = 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.32" ? "7.0.32" : "0.0.0-dev";
655
655
  var PACKAGE_NAME = "@buoy-gg/mcp";
656
656
 
657
657
  // src/update/checkForUpdate.ts
@@ -1384,7 +1384,7 @@ ${JSON.stringify(summary, null, 2)}
1384
1384
  let sawActive = false;
1385
1385
  let idleStreak = 0;
1386
1386
  let lastCount = -1;
1387
- let resolved = null;
1387
+ let resolved2 = null;
1388
1388
  while (Date.now() - start < cap3) {
1389
1389
  let snap2;
1390
1390
  try {
@@ -1396,7 +1396,7 @@ ${JSON.stringify(summary, null, 2)}
1396
1396
  const completed = snap2.live?.automationCompleted;
1397
1397
  if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
1398
1398
  if (completed?.phase === "done") {
1399
- resolved = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
1399
+ resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
1400
1400
  break;
1401
1401
  }
1402
1402
  const phase = snap2.live?.automation?.phase ?? "idle";
@@ -1406,11 +1406,11 @@ ${JSON.stringify(summary, null, 2)}
1406
1406
  const count = runCountFor(snap2.index, newest);
1407
1407
  if (count === lastCount) {
1408
1408
  if (sawActive && ++idleStreak >= 3) {
1409
- resolved = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1409
+ resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1410
1410
  break;
1411
1411
  }
1412
1412
  if (!sawActive && Date.now() - start > 4e3) {
1413
- resolved = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1413
+ resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
1414
1414
  break;
1415
1415
  }
1416
1416
  } else {
@@ -1422,11 +1422,11 @@ ${JSON.stringify(summary, null, 2)}
1422
1422
  }
1423
1423
  await delay2(POLL_INTERVAL_MS);
1424
1424
  }
1425
- if (!resolved) return fail2("No automation batch completed within the wait window.");
1425
+ if (!resolved2) return fail2("No automation batch completed within the wait window.");
1426
1426
  await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
1427
1427
  });
1428
1428
  const snap = await readPerfSnapshot(sync, target);
1429
- const ranking = rankBatchFromIndex(snap.index ?? [], resolved.batchId, void 0, resolved.failures);
1429
+ const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
1430
1430
  await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
1431
1431
  });
1432
1432
  return text(`${formatRankingMarkdown(ranking)}
@@ -1753,8 +1753,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
1753
1753
  }
1754
1754
  function bodyLines(e, fetched) {
1755
1755
  const got = e.id ? fetched.get(e.id) : void 0;
1756
- const label = (inline, resolved, omitted) => {
1757
- const value = inline ?? resolved;
1756
+ const label = (inline, resolved2, omitted) => {
1757
+ const value = inline ?? resolved2;
1758
1758
  if (value !== void 0 && value !== null) return truncate(value);
1759
1759
  if (!omitted) return "(none)";
1760
1760
  return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
@@ -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":
@@ -3040,9 +3335,9 @@ function reasonMessage(res, query) {
3040
3335
  return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
3041
3336
  }
3042
3337
  }
3043
- function candidatesText(candidates) {
3044
- if (!candidates || candidates.length === 0) return "";
3045
- const lines = candidates.map((c) => {
3338
+ function candidatesText(candidates2) {
3339
+ if (!candidates2 || candidates2.length === 0) return "";
3340
+ const lines = candidates2.map((c) => {
3046
3341
  const id = c.testID || c.nativeID;
3047
3342
  return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
3048
3343
  });
@@ -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,390 @@ function registerScreenshotTools(server, sync) {
3135
3430
  ]
3136
3431
  };
3137
3432
  } catch (e) {
3138
- return fail13(errMsg13(e));
3433
+ return fail14(errMsg14(e));
3434
+ }
3435
+ }
3436
+ );
3437
+ }
3438
+
3439
+ // src/tools/packs/camera.ts
3440
+ var import_zod15 = require("zod");
3441
+
3442
+ // src/capture/camera.ts
3443
+ var import_node_child_process2 = require("child_process");
3444
+ var import_node_util2 = require("util");
3445
+ var import_node_fs3 = require("fs");
3446
+ var import_node_os3 = __toESM(require("os"));
3447
+ var import_node_path3 = __toESM(require("path"));
3448
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
3449
+ function candidates() {
3450
+ const fromEnv = process.env.BUOY_SIMCAM_DIR;
3451
+ const list = [
3452
+ ...fromEnv ? [import_node_path3.default.join(fromEnv, "buoycam")] : [],
3453
+ "/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
3454
+ import_node_path3.default.join(import_node_os3.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
3455
+ ];
3456
+ let dir = __dirname;
3457
+ for (let i = 0; i < 8; i++) {
3458
+ list.push(import_node_path3.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
3459
+ const parent = import_node_path3.default.dirname(dir);
3460
+ if (parent === dir) break;
3461
+ dir = parent;
3462
+ }
3463
+ return list;
3464
+ }
3465
+ var resolved = null;
3466
+ function cliPath() {
3467
+ if (resolved) return resolved;
3468
+ resolved = candidates().find((c) => (0, import_node_fs3.existsSync)(c)) ?? null;
3469
+ return resolved;
3470
+ }
3471
+ async function run(args) {
3472
+ const cli = cliPath();
3473
+ if (!cli) {
3474
+ return {
3475
+ ok: false,
3476
+ error: "The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."
3477
+ };
3478
+ }
3479
+ try {
3480
+ const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
3481
+ return { ok: true, data: JSON.parse(stdout) };
3482
+ } catch (err) {
3483
+ const stdout = err.stdout ?? "";
3484
+ try {
3485
+ return { ok: false, data: JSON.parse(stdout) };
3486
+ } catch {
3487
+ return {
3488
+ ok: false,
3489
+ error: err.message || "the simulator camera CLI failed"
3490
+ };
3491
+ }
3492
+ }
3493
+ }
3494
+ function describeDiagnosis(checks) {
3495
+ const failed = checks.filter((c) => c.level === "fail");
3496
+ const warned = checks.filter((c) => c.level === "warn");
3497
+ if (failed.length === 0 && warned.length === 0) {
3498
+ return "The simulator camera is healthy \u2014 every check passed.";
3499
+ }
3500
+ const lines = [];
3501
+ for (const c of [...failed, ...warned]) {
3502
+ lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
3503
+ if (c.fix) lines.push(` ${c.fix}`);
3504
+ }
3505
+ const headline = failed.length > 0 ? `${failed.length} problem${failed.length === 1 ? "" : "s"} found.` : `No hard failures, but ${warned.length} thing${warned.length === 1 ? "" : "s"} worth knowing.`;
3506
+ return [headline, "", ...lines].join("\n");
3507
+ }
3508
+ function describeDevices(devices, apps) {
3509
+ if (devices.length === 0) {
3510
+ return "No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._";
3511
+ }
3512
+ const lines = [
3513
+ devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
3514
+ ...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
3515
+ ];
3516
+ if (devices.length > 1 && apps === null) {
3517
+ lines.push(
3518
+ "",
3519
+ "_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._"
3520
+ );
3521
+ return lines.join("\n");
3522
+ }
3523
+ if (apps === null) {
3524
+ lines.push("", "_Could not read the installed apps._");
3525
+ return lines.join("\n");
3526
+ }
3527
+ if (apps.length === 0) {
3528
+ lines.push("", "No apps installed on it.");
3529
+ return lines.join("\n");
3530
+ }
3531
+ lines.push(
3532
+ "",
3533
+ `${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
3534
+ ...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
3535
+ "",
3536
+ "_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
3537
+ );
3538
+ return lines.join("\n");
3539
+ }
3540
+ function describeInputs(cameras, windows) {
3541
+ const lines = [];
3542
+ lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
3543
+ if (cameras.length) {
3544
+ lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
3545
+ } else {
3546
+ lines.push(
3547
+ "_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
3548
+ );
3549
+ }
3550
+ lines.push("");
3551
+ lines.push(
3552
+ windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
3553
+ );
3554
+ if (windows.length) {
3555
+ lines.push(
3556
+ ...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
3557
+ );
3558
+ } else {
3559
+ lines.push(
3560
+ "_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
3561
+ );
3562
+ }
3563
+ return lines.join("\n");
3564
+ }
3565
+ function entitlementPaths() {
3566
+ const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
3567
+ if (fromEnv) return [fromEnv];
3568
+ return [
3569
+ import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
3570
+ import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
3571
+ ];
3572
+ }
3573
+ function readEntitlement() {
3574
+ for (const p of entitlementPaths()) {
3575
+ try {
3576
+ if (!(0, import_node_fs3.existsSync)(p)) continue;
3577
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf8"));
3578
+ return {
3579
+ tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
3580
+ isPro: parsed.isPro === true
3581
+ };
3582
+ } catch {
3583
+ }
3584
+ }
3585
+ return { tier: "anonymous", isPro: false };
3586
+ }
3587
+ function proRequired(tool) {
3588
+ return `\`${tool}\` is a Buoy Pro feature.
3589
+
3590
+ The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel without a licence. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
3591
+
3592
+ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`;
3593
+ }
3594
+
3595
+ // src/tools/packs/camera.ts
3596
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3597
+ var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
3598
+ var fail15 = (message) => ({
3599
+ isError: true,
3600
+ content: [{ type: "text", text: message }]
3601
+ });
3602
+ function registerCameraTools(server) {
3603
+ server.registerTool(
3604
+ "camera_source",
3605
+ {
3606
+ description: "Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",
3607
+ inputSchema: {
3608
+ kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
3609
+ value: import_zod15.z.string().optional().describe(
3610
+ "Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."
3611
+ ),
3612
+ format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
3613
+ }
3614
+ },
3615
+ async (args) => {
3616
+ const blocked = gate("camera_source");
3617
+ if (blocked) return blocked;
3618
+ const cmd = ["source", args.kind];
3619
+ if (args.value) cmd.push(args.value);
3620
+ if (args.format) cmd.push("--format", args.format);
3621
+ const result = await run(cmd);
3622
+ if (!result.ok) {
3623
+ return fail15(
3624
+ result.error ?? result.data?.error ?? "could not set the camera source"
3625
+ );
3626
+ }
3627
+ const what = args.kind === "barcode" ? `a ${args.format ?? "qr"} code reading "${args.value}"` : args.kind === "screen" ? `this Mac's screen (${args.value ?? "behind the simulator window"})` : args.value ?? args.kind;
3628
+ return text13(
3629
+ `The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
3630
+
3631
+ _Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`
3632
+ );
3633
+ }
3634
+ );
3635
+ server.registerTool(
3636
+ "camera_status",
3637
+ {
3638
+ description: "Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",
3639
+ inputSchema: {}
3640
+ },
3641
+ async () => {
3642
+ const blocked = gate("camera_status");
3643
+ if (blocked) return blocked;
3644
+ const result = await run(["status"]);
3645
+ if (!result.ok || !result.data) {
3646
+ return fail15(result.error ?? "could not read the simulator camera status");
3647
+ }
3648
+ const { publishing, fps, codes } = result.data;
3649
+ if (!publishing) {
3650
+ return text13(
3651
+ "The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
3652
+ );
3653
+ }
3654
+ return text13(
3655
+ [
3656
+ `Publishing at ${fps} fps.`,
3657
+ codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
3658
+ "",
3659
+ "_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"
3660
+ ].join("\n")
3661
+ );
3662
+ }
3663
+ );
3664
+ server.registerTool(
3665
+ "camera_launch",
3666
+ {
3667
+ description: "Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",
3668
+ inputSchema: {
3669
+ bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
3670
+ udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
3671
+ }
3672
+ },
3673
+ async (args) => {
3674
+ const blocked = gate("camera_launch");
3675
+ if (blocked) return blocked;
3676
+ const cmd = ["launch", args.bundleId];
3677
+ if (args.udid) cmd.push("--udid", args.udid);
3678
+ const result = await run(cmd);
3679
+ if (!result.ok) {
3680
+ return fail15(result.error ?? result.data?.error ?? "could not launch the app");
3681
+ }
3682
+ return text13(
3683
+ `Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
3684
+ );
3685
+ }
3686
+ );
3687
+ server.registerTool(
3688
+ "camera_diagnose",
3689
+ {
3690
+ description: "Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",
3691
+ inputSchema: {
3692
+ bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
3693
+ }
3694
+ },
3695
+ async (args) => {
3696
+ const cmd = ["diagnose"];
3697
+ if (args.bundleId) cmd.push(args.bundleId);
3698
+ const result = await run(cmd);
3699
+ if (!result.data?.checks) {
3700
+ return fail15(result.error ?? "could not run the simulator camera diagnosis");
3701
+ }
3702
+ const ent = readEntitlement();
3703
+ const tierNote = ent.isPro ? "" : "\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";
3704
+ return text13(describeDiagnosis(result.data.checks) + tierNote);
3705
+ }
3706
+ );
3707
+ server.registerTool(
3708
+ "camera_stop",
3709
+ {
3710
+ description: "Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",
3711
+ inputSchema: {}
3712
+ },
3713
+ async () => {
3714
+ const blocked = gate("camera_stop");
3715
+ if (blocked) return blocked;
3716
+ const result = await run(["stop"]);
3717
+ if (!result.ok) {
3718
+ return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
3719
+ }
3720
+ return text13("The simulator camera is off.");
3721
+ }
3722
+ );
3723
+ server.registerTool(
3724
+ "camera_devices",
3725
+ {
3726
+ description: "List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",
3727
+ inputSchema: {
3728
+ udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
3729
+ }
3730
+ },
3731
+ async (args) => {
3732
+ const blocked = gate("camera_devices");
3733
+ if (blocked) return blocked;
3734
+ const devicesResult = await run(["devices"]);
3735
+ if (!devicesResult.ok || !devicesResult.data) {
3736
+ return fail15(devicesResult.error ?? "could not list booted simulators");
3737
+ }
3738
+ const devices = devicesResult.data.devices ?? [];
3739
+ let apps = null;
3740
+ if (devices.length === 1 || args.udid) {
3741
+ const appsResult = await run([
3742
+ "apps",
3743
+ ...args.udid ? ["--udid", args.udid] : []
3744
+ ]);
3745
+ apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
3746
+ }
3747
+ return text13(describeDevices(devices, apps));
3748
+ }
3749
+ );
3750
+ server.registerTool(
3751
+ "camera_inputs",
3752
+ {
3753
+ description: "List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",
3754
+ inputSchema: {}
3755
+ },
3756
+ async () => {
3757
+ const blocked = gate("camera_inputs");
3758
+ if (blocked) return blocked;
3759
+ const [cams, wins] = await Promise.all([
3760
+ run(["cameras"]),
3761
+ run(["windows"])
3762
+ ]);
3763
+ if (!cams.ok && !wins.ok) {
3764
+ return fail15(
3765
+ cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
3766
+ );
3767
+ }
3768
+ return text13(
3769
+ describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
3770
+ );
3771
+ }
3772
+ );
3773
+ server.registerTool(
3774
+ "camera_zero_setup",
3775
+ {
3776
+ description: "Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",
3777
+ inputSchema: {
3778
+ action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
3779
+ udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
3139
3780
  }
3781
+ },
3782
+ async (args) => {
3783
+ const blocked = gate("camera_zero_setup");
3784
+ if (blocked) return blocked;
3785
+ const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
3786
+ if (!result.ok) {
3787
+ return fail15(
3788
+ result.error ?? result.data?.error ?? "could not change zero-setup mode"
3789
+ );
3790
+ }
3791
+ const d = result.data ?? {};
3792
+ if (args.action === "status") {
3793
+ if (!d.enabled) return text13("Zero-setup mode is **off**.");
3794
+ return text13(
3795
+ d.foreign ? `Zero-setup is on, but **another tool owns it** (\`${d.path}\`). Quit that tool before turning Buoy's on.` : "Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera."
3796
+ );
3797
+ }
3798
+ if (args.action === "on") {
3799
+ return text13(
3800
+ "Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._"
3801
+ );
3802
+ }
3803
+ return text13("Zero-setup mode is **off**.");
3140
3804
  }
3141
3805
  );
3142
3806
  }
3143
3807
 
3144
3808
  // src/tools/packs/interact.ts
3145
- var import_zod14 = require("zod");
3809
+ var import_zod16 = require("zod");
3146
3810
  var TOOL_ID2 = "highlight-updates";
3147
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
3148
- var fail14 = (message) => ({
3811
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3812
+ var fail16 = (message) => ({
3149
3813
  isError: true,
3150
3814
  content: [{ type: "text", text: message }]
3151
3815
  });
3152
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3816
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3153
3817
  function formatElement(e) {
3154
3818
  let line = e.interactive ? "\u2022 " : " ";
3155
3819
  line += e.name;
@@ -3172,8 +3836,8 @@ function registerInteractTools(server, sync) {
3172
3836
  {
3173
3837
  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
3838
  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).")
3839
+ interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3840
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3177
3841
  }
3178
3842
  },
3179
3843
  async (args) => {
@@ -3183,16 +3847,16 @@ function registerInteractTools(server, sync) {
3183
3847
  const all = res?.elements ?? [];
3184
3848
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3185
3849
  if (elements.length === 0) {
3186
- return text12(
3850
+ return text14(
3187
3851
  "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
3852
  );
3189
3853
  }
3190
3854
  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}
3855
+ return text14(`${header}
3192
3856
 
3193
3857
  ${elements.map(formatElement).join("\n")}`);
3194
3858
  } catch (e) {
3195
- return fail14(errMsg14(e));
3859
+ return fail16(errMsg15(e));
3196
3860
  }
3197
3861
  }
3198
3862
  );
@@ -3201,20 +3865,20 @@ ${elements.map(formatElement).join("\n")}`);
3201
3865
  {
3202
3866
  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
3867
  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).")
3868
+ nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3869
+ testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
3870
+ query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3871
+ value: import_zod16.z.union([import_zod16.z.number(), import_zod16.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
3872
+ text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3873
+ longPress: import_zod16.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3874
+ scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3875
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3212
3876
  }
3213
3877
  },
3214
3878
  async (args) => {
3215
3879
  try {
3216
3880
  if (args.nativeTag == null && !args.testID && !args.query) {
3217
- return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
3881
+ return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
3218
3882
  }
3219
3883
  const target = sync.requireProDevice(args.deviceId);
3220
3884
  const res = await sync.callAction(
@@ -3239,26 +3903,26 @@ ${elements.map(formatElement).join("\n")}`);
3239
3903
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3240
3904
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3241
3905
  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}`);
3906
+ return text14(`${verb}${scrolledNote}.${flowTip}`);
3243
3907
  }
3244
3908
  let msg = res?.reason ?? "Tap failed.";
3245
3909
  if (res?.candidates?.length) {
3246
3910
  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
3911
  }
3248
- return fail14(msg);
3912
+ return fail16(msg);
3249
3913
  } catch (e) {
3250
- return fail14(errMsg14(e));
3914
+ return fail16(errMsg15(e));
3251
3915
  }
3252
3916
  }
3253
3917
  );
3254
3918
  }
3255
3919
 
3256
3920
  // src/tools/packs/jsThread.ts
3257
- var import_zod15 = require("zod");
3921
+ var import_zod17 = require("zod");
3258
3922
  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);
3923
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3924
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3925
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3262
3926
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3263
3927
  function sparkline(values) {
3264
3928
  const max = Math.max(1, ...values);
@@ -3329,9 +3993,9 @@ function registerJsThreadTools(server, sync) {
3329
3993
  {
3330
3994
  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
3995
  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.")
3996
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3997
+ durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3998
+ clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3335
3999
  }
3336
4000
  },
3337
4001
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -3344,7 +4008,7 @@ function registerJsThreadTools(server, sync) {
3344
4008
  { durationMs: duration, clearFirst: clearFirst === true },
3345
4009
  target
3346
4010
  );
3347
- if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
4011
+ if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3348
4012
  const md = formatTopMarkdown(snap, duration);
3349
4013
  const slim = {
3350
4014
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -3363,13 +4027,13 @@ function registerJsThreadTools(server, sync) {
3363
4027
  worstFreezeMs: r.worstFreezeMs
3364
4028
  }))
3365
4029
  };
3366
- return text13(`${md}
4030
+ return text15(`${md}
3367
4031
 
3368
4032
  \`\`\`json
3369
4033
  ${JSON.stringify(slim)}
3370
4034
  \`\`\``);
3371
4035
  } catch (e) {
3372
- return fail15(errMsg15(e));
4036
+ return fail17(errMsg16(e));
3373
4037
  }
3374
4038
  }
3375
4039
  );
@@ -3378,8 +4042,8 @@ ${JSON.stringify(slim)}
3378
4042
  {
3379
4043
  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
4044
  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.")
4045
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4046
+ key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
3383
4047
  }
3384
4048
  },
3385
4049
  async ({ deviceId, key }) => {
@@ -3392,18 +4056,18 @@ ${JSON.stringify(slim)}
3392
4056
  target
3393
4057
  );
3394
4058
  if (!detail) {
3395
- return fail15(
4059
+ return fail17(
3396
4060
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
3397
4061
  );
3398
4062
  }
3399
4063
  const md = formatDetailMarkdown(detail);
3400
- return text13(`${md}
4064
+ return text15(`${md}
3401
4065
 
3402
4066
  \`\`\`json
3403
4067
  ${JSON.stringify(detail)}
3404
4068
  \`\`\``);
3405
4069
  } catch (e) {
3406
- return fail15(errMsg15(e));
4070
+ return fail17(errMsg16(e));
3407
4071
  }
3408
4072
  }
3409
4073
  );
@@ -3412,27 +4076,27 @@ ${JSON.stringify(detail)}
3412
4076
  {
3413
4077
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
3414
4078
  inputSchema: {
3415
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4079
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3416
4080
  }
3417
4081
  },
3418
4082
  async ({ deviceId }) => {
3419
4083
  try {
3420
4084
  const target = sync.requireProDevice(deviceId);
3421
4085
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
3422
- return text13("Cleared js-top stats.");
4086
+ return text15("Cleared js-top stats.");
3423
4087
  } catch (e) {
3424
- return fail15(errMsg15(e));
4088
+ return fail17(errMsg16(e));
3425
4089
  }
3426
4090
  }
3427
4091
  );
3428
4092
  }
3429
4093
 
3430
4094
  // src/tools/packs/images.ts
3431
- var import_zod16 = require("zod");
4095
+ var import_zod18 = require("zod");
3432
4096
  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);
4097
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4098
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4099
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3436
4100
  function uriTail(uri) {
3437
4101
  if (!uri) return "(no source)";
3438
4102
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -3520,9 +4184,9 @@ function registerImagesTools(server, sync) {
3520
4184
  {
3521
4185
  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
4186
  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).")
4187
+ deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4188
+ limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
4189
+ status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3526
4190
  }
3527
4191
  },
3528
4192
  async ({ deviceId, limit, status }) => {
@@ -3535,7 +4199,7 @@ function registerImagesTools(server, sync) {
3535
4199
  { limit: capped, status },
3536
4200
  target
3537
4201
  );
3538
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
4202
+ if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3539
4203
  const md = formatListMarkdown(list, capped);
3540
4204
  const slim = {
3541
4205
  stats: list.stats,
@@ -3562,13 +4226,13 @@ function registerImagesTools(server, sync) {
3562
4226
  layoutShifts: r.layoutShifts
3563
4227
  }))
3564
4228
  };
3565
- return text14(`${md}
4229
+ return text16(`${md}
3566
4230
 
3567
4231
  \`\`\`json
3568
4232
  ${JSON.stringify(slim)}
3569
4233
  \`\`\``);
3570
4234
  } catch (e) {
3571
- return fail16(errMsg16(e));
4235
+ return fail18(errMsg17(e));
3572
4236
  }
3573
4237
  }
3574
4238
  );
@@ -3577,9 +4241,9 @@ ${JSON.stringify(slim)}
3577
4241
  {
3578
4242
  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
4243
  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([
4244
+ deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4245
+ id: import_zod18.z.number().describe("Record id from get_images."),
4246
+ action: import_zod18.z.enum([
3583
4247
  "hard_reload",
3584
4248
  "retry",
3585
4249
  "evict_disk",
@@ -3591,7 +4255,7 @@ ${JSON.stringify(slim)}
3591
4255
  "swap_url",
3592
4256
  "restore"
3593
4257
  ]).describe("What to do."),
3594
- url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
4258
+ url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
3595
4259
  }
3596
4260
  },
3597
4261
  async ({ deviceId, id, action, url }) => {
@@ -3599,8 +4263,8 @@ ${JSON.stringify(slim)}
3599
4263
  const target = sync.requireProDevice(deviceId);
3600
4264
  if (action === "prove_savings") {
3601
4265
  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);
4266
+ if (!savings) return fail18("Device returned nothing for proveSavings.");
4267
+ if (!savings.ok) return fail18(savings.message);
3604
4268
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
3605
4269
  if (savings.originalBytes != null)
3606
4270
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -3612,11 +4276,11 @@ ${JSON.stringify(slim)}
3612
4276
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
3613
4277
  if (savings.optimizedUri)
3614
4278
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
3615
- return text14(parts.join("\n"));
4279
+ return text16(parts.join("\n"));
3616
4280
  }
3617
4281
  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.");
4282
+ if (!entry) return fail18(`Unknown action: ${action}`);
4283
+ if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
3620
4284
  const result = await sync.callAction(
3621
4285
  IMAGES_TOOL_ID,
3622
4286
  entry.action,
@@ -3636,9 +4300,9 @@ ${JSON.stringify(slim)}
3636
4300
  );
3637
4301
  if (detail.error) lines.push(`Error: ${detail.error}`);
3638
4302
  }
3639
- return text14(lines.join("\n"));
4303
+ return text16(lines.join("\n"));
3640
4304
  } catch (e) {
3641
- return fail16(errMsg16(e));
4305
+ return fail18(errMsg17(e));
3642
4306
  }
3643
4307
  }
3644
4308
  );
@@ -3647,12 +4311,12 @@ ${JSON.stringify(slim)}
3647
4311
  {
3648
4312
  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
4313
  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.")
4314
+ deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4315
+ network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
4316
+ blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
4317
+ massAction: import_zod18.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
4318
+ clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
4319
+ clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3656
4320
  }
3657
4321
  },
3658
4322
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3679,17 +4343,17 @@ ${JSON.stringify(slim)}
3679
4343
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3680
4344
  done.push(r?.message ?? "expo caches cleared");
3681
4345
  }
3682
- if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3683
- return text14(`Applied: ${done.join(" \xB7 ")}`);
4346
+ if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4347
+ return text16(`Applied: ${done.join(" \xB7 ")}`);
3684
4348
  } catch (e) {
3685
- return fail16(errMsg16(e));
4349
+ return fail18(errMsg17(e));
3686
4350
  }
3687
4351
  }
3688
4352
  );
3689
4353
  }
3690
4354
 
3691
4355
  // src/tools/packs/assets.ts
3692
- var import_zod17 = require("zod");
4356
+ var import_zod19 = require("zod");
3693
4357
 
3694
4358
  // src/tools/packs/assetsGraphNote.ts
3695
4359
  function graphUnavailableNote(ss) {
@@ -3704,9 +4368,9 @@ function graphUnavailableNote(ss) {
3704
4368
 
3705
4369
  // src/tools/packs/assets.ts
3706
4370
  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);
4371
+ var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4372
+ var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4373
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3710
4374
  function kb2(bytes) {
3711
4375
  if (bytes == null || bytes <= 0) return "\u2013";
3712
4376
  if (bytes < 1024) return `${bytes}B`;
@@ -3794,10 +4458,10 @@ function registerAssetsTools(server, sync) {
3794
4458
  {
3795
4459
  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
4460
  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.")
4461
+ deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4462
+ limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4463
+ kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4464
+ unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3801
4465
  }
3802
4466
  },
3803
4467
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3810,7 +4474,7 @@ function registerAssetsTools(server, sync) {
3810
4474
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3811
4475
  target
3812
4476
  );
3813
- if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4477
+ if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3814
4478
  if (unusedOnly) {
3815
4479
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3816
4480
  }
@@ -3834,13 +4498,13 @@ function registerAssetsTools(server, sync) {
3834
4498
  sizeSource: r.sizeSource
3835
4499
  }))
3836
4500
  };
3837
- return text15(`${md}
4501
+ return text17(`${md}
3838
4502
 
3839
4503
  \`\`\`json
3840
4504
  ${JSON.stringify(slim)}
3841
4505
  \`\`\``);
3842
4506
  } catch (e) {
3843
- return fail17(errMsg17(e));
4507
+ return fail19(errMsg18(e));
3844
4508
  }
3845
4509
  }
3846
4510
  );
@@ -3849,9 +4513,9 @@ ${JSON.stringify(slim)}
3849
4513
  {
3850
4514
  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
4515
  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).")
4516
+ deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4517
+ action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4518
+ id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
3855
4519
  }
3856
4520
  },
3857
4521
  async ({ deviceId, action, id }) => {
@@ -3861,19 +4525,19 @@ ${JSON.stringify(slim)}
3861
4525
  case "rescan": {
3862
4526
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3863
4527
  const ss = result?.scanStatus;
3864
- if (!ss) return fail17("Device returned nothing for rescan.");
3865
- return text15(
4528
+ if (!ss) return fail19("Device returned nothing for rescan.");
4529
+ return text17(
3866
4530
  `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
4531
  );
3868
4532
  }
3869
4533
  case "measure_sizes": {
3870
4534
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3871
- return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4535
+ return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3872
4536
  }
3873
4537
  case "detail": {
3874
- if (id == null) return fail17("detail requires the `id` param (from get_assets).");
4538
+ if (id == null) return fail19("detail requires the `id` param (from get_assets).");
3875
4539
  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.`);
4540
+ if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3877
4541
  const lines = [
3878
4542
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3879
4543
  "",
@@ -3891,32 +4555,32 @@ ${JSON.stringify(slim)}
3891
4555
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3892
4556
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3893
4557
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3894
- return text15(lines.join("\n"));
4558
+ return text17(lines.join("\n"));
3895
4559
  }
3896
4560
  case "save_baseline": {
3897
4561
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3898
- return text15(
4562
+ return text17(
3899
4563
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3900
4564
  );
3901
4565
  }
3902
4566
  case "clear_baseline": {
3903
4567
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3904
- return text15("Baseline cleared.");
4568
+ return text17("Baseline cleared.");
3905
4569
  }
3906
4570
  case "clear_records": {
3907
4571
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3908
- return text15(result?.message ?? "Inventory cleared.");
4572
+ return text17(result?.message ?? "Inventory cleared.");
3909
4573
  }
3910
4574
  }
3911
4575
  } catch (e) {
3912
- return fail17(errMsg17(e));
4576
+ return fail19(errMsg18(e));
3913
4577
  }
3914
4578
  }
3915
4579
  );
3916
4580
  }
3917
4581
 
3918
4582
  // src/tools/packs/reload.ts
3919
- var import_zod18 = require("zod");
4583
+ var import_zod20 = require("zod");
3920
4584
 
3921
4585
  // src/capture/devServerReload.ts
3922
4586
  var import_ws = require("ws");
@@ -3979,9 +4643,9 @@ var DEFAULT_WAIT_MS = 3e4;
3979
4643
  var TEARDOWN_GRACE_MS = 1200;
3980
4644
  var PROBE_TIMEOUT_MS = 1500;
3981
4645
  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);
4646
+ var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4647
+ var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4648
+ var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
3985
4649
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3986
4650
  async function ping(sync, deviceId, timeoutMs) {
3987
4651
  try {
@@ -4020,12 +4684,12 @@ function registerReloadTools(server, sync) {
4020
4684
  {
4021
4685
  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
4686
  inputSchema: {
4023
- strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4687
+ strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4024
4688
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
4025
4689
  ),
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.')
4690
+ wait: import_zod20.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4691
+ timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4692
+ deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4029
4693
  }
4030
4694
  },
4031
4695
  async (args) => {
@@ -4067,32 +4731,32 @@ function registerReloadTools(server, sync) {
4067
4731
  }
4068
4732
  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
4733
  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.`);
4734
+ return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4071
4735
  }
4072
4736
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4073
4737
  if (elapsed == null) {
4074
- return fail18(
4738
+ return fail20(
4075
4739
  `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
4740
  );
4077
4741
  }
4078
- return text16(
4742
+ return text18(
4079
4743
  `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
4744
  );
4081
4745
  } catch (e) {
4082
- return fail18(errMsg18(e));
4746
+ return fail20(errMsg19(e));
4083
4747
  }
4084
4748
  }
4085
4749
  );
4086
4750
  }
4087
4751
 
4088
4752
  // src/tools/packs/triage.ts
4089
- var import_zod19 = require("zod");
4090
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4091
- var fail19 = (message) => ({
4753
+ var import_zod21 = require("zod");
4754
+ var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4755
+ var fail21 = (message) => ({
4092
4756
  isError: true,
4093
4757
  content: [{ type: "text", text: message }]
4094
4758
  });
4095
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4759
+ var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4096
4760
  var PROBE_TIMEOUT_MS2 = 5e3;
4097
4761
  var MAX_STORAGE_KEYS = 20;
4098
4762
  var MAX_STORAGE_VALUE = 400;
@@ -4123,13 +4787,13 @@ async function otherBackends(sync, target, opts) {
4123
4787
  return notes.length ? `
4124
4788
  ${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
4125
4789
  }
4126
- async function section(title, run) {
4790
+ async function section(title, run2) {
4127
4791
  try {
4128
- const body = await run();
4792
+ const body = await run2();
4129
4793
  return body ? `${title}
4130
4794
  ${body}` : null;
4131
4795
  } catch (e) {
4132
- const msg = errMsg19(e);
4796
+ const msg = errMsg20(e);
4133
4797
  if (/not responding|timed out|got no answer/i.test(msg)) {
4134
4798
  return `${title}
4135
4799
  (no answer \u2014 see the note above)`;
@@ -4144,7 +4808,7 @@ function registerTriageTools(server, sync) {
4144
4808
  {
4145
4809
  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
4810
  inputSchema: {
4147
- deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4811
+ deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4148
4812
  }
4149
4813
  },
4150
4814
  async (args) => {
@@ -4251,7 +4915,7 @@ _Values: get_storage includeValues:true_`;
4251
4915
 
4252
4916
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4253
4917
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4254
- return text17(
4918
+ return text19(
4255
4919
  `${head}${banner}
4256
4920
 
4257
4921
  ${body}
@@ -4259,22 +4923,22 @@ ${body}
4259
4923
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4260
4924
  );
4261
4925
  } catch (e) {
4262
- return fail19(errMsg19(e));
4926
+ return fail21(errMsg20(e));
4263
4927
  }
4264
4928
  }
4265
4929
  );
4266
4930
  }
4267
4931
 
4268
4932
  // src/tools/packs/flow.ts
4269
- var import_zod20 = require("zod");
4933
+ var import_zod22 = require("zod");
4270
4934
  var UI_TOOL_ID = "highlight-updates";
4271
4935
  var APP_TOOL_ID2 = "app";
4272
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4273
- var fail20 = (message) => ({
4936
+ var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4937
+ var fail22 = (message) => ({
4274
4938
  isError: true,
4275
4939
  content: [{ type: "text", text: message }]
4276
4940
  });
4277
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4941
+ var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
4278
4942
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4279
4943
  var POLL_MS = 250;
4280
4944
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4291,19 +4955,19 @@ function formatElement2(e) {
4291
4955
  const display = e.text ?? e.label;
4292
4956
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4293
4957
  }
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).")
4958
+ var stepSchema = import_zod22.z.object({
4959
+ tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4960
+ longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
4961
+ type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
4962
+ into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4963
+ set: import_zod22.z.union([import_zod22.z.number(), import_zod22.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4964
+ reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4965
+ waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4966
+ waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4967
+ waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4968
+ expect: import_zod22.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4969
+ expectGone: import_zod22.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4970
+ timeoutMs: import_zod22.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
4307
4971
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4308
4972
  function registerFlowTools(server, sync) {
4309
4973
  server.registerTool(
@@ -4311,10 +4975,10 @@ function registerFlowTools(server, sync) {
4311
4975
  {
4312
4976
  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
4977
  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).")
4978
+ steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4979
+ include: import_zod22.z.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final screen + network/console produced by the flow; 'screen' = final screen only; 'none' = step results only."),
4980
+ continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4981
+ deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4318
4982
  }
4319
4983
  },
4320
4984
  async (args) => {
@@ -4486,7 +5150,7 @@ function registerFlowTools(server, sync) {
4486
5150
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
4487
5151
  }
4488
5152
  } catch (e) {
4489
- stepErrors.push(errMsg20(e));
5153
+ stepErrors.push(errMsg21(e));
4490
5154
  }
4491
5155
  if (stepErrors.length) {
4492
5156
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -4509,7 +5173,7 @@ function registerFlowTools(server, sync) {
4509
5173
  ${shown.map(formatElement2).join("\n")}`
4510
5174
  );
4511
5175
  } catch (e) {
4512
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg20(e))}`);
5176
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
4513
5177
  }
4514
5178
  }
4515
5179
  if (include === "all") {
@@ -4538,9 +5202,9 @@ ${shown.map(formatElement2).join("\n")}`
4538
5202
  }
4539
5203
  }
4540
5204
  }
4541
- return text18(parts.join("\n\n"));
5205
+ return text20(parts.join("\n\n"));
4542
5206
  } catch (e) {
4543
- return fail20(errMsg20(e));
5207
+ return fail22(errMsg21(e));
4544
5208
  }
4545
5209
  }
4546
5210
  );
@@ -4561,25 +5225,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4561
5225
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
4562
5226
 
4563
5227
  // src/tools/packs/renders.ts
4564
- var import_zod21 = require("zod");
5228
+ var import_zod23 = require("zod");
4565
5229
  var UI_TOOL_ID2 = "highlight-updates";
4566
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4567
- var fail21 = (message) => ({
5230
+ var text21 = (s) => ({ content: [{ type: "text", text: s }] });
5231
+ var fail23 = (message) => ({
4568
5232
  isError: true,
4569
5233
  content: [{ type: "text", text: message }]
4570
5234
  });
4571
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
5235
+ var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
4572
5236
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
4573
5237
  var DEFAULT_IDLE_MS = 3e3;
4574
5238
  var SETTLE_MS = 400;
4575
5239
  var DEFAULT_TOP = 12;
4576
5240
  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'.")
5241
+ var stepSchema2 = import_zod23.z.object({
5242
+ tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5243
+ type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
5244
+ into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5245
+ waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
5246
+ repeat: import_zod23.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
4583
5247
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
4584
5248
  function selector2(needle) {
4585
5249
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -4672,14 +5336,14 @@ function registerRenderTools(server, sync) {
4672
5336
  {
4673
5337
  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
5338
  inputSchema: {
4675
- steps: import_zod21.z.array(stepSchema2).max(20).optional().describe(
5339
+ steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
4676
5340
  "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
5341
  ),
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).")
5342
+ idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5343
+ label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5344
+ compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5345
+ top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5346
+ deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4683
5347
  }
4684
5348
  },
4685
5349
  async (args) => {
@@ -4688,18 +5352,18 @@ function registerRenderTools(server, sync) {
4688
5352
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
4689
5353
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
4690
5354
  if (!uiTool) {
4691
- return fail21(
5355
+ return fail23(
4692
5356
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
4693
5357
  );
4694
5358
  }
4695
5359
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
4696
- return fail21(
5360
+ return fail23(
4697
5361
  "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
5362
  );
4699
5363
  }
4700
5364
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
4701
5365
  if (began && began.ok === false) {
4702
- return fail21(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5366
+ return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
4703
5367
  }
4704
5368
  await delay5(SETTLE_MS);
4705
5369
  const startedAt = Date.now();
@@ -4744,7 +5408,7 @@ function registerRenderTools(server, sync) {
4744
5408
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
4745
5409
  const summary = ended?.summary;
4746
5410
  if (!summary) {
4747
- return fail21(
5411
+ return fail23(
4748
5412
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
4749
5413
  );
4750
5414
  }
@@ -4769,17 +5433,17 @@ function registerRenderTools(server, sync) {
4769
5433
  } else if (!args.compareToPrevious) {
4770
5434
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
4771
5435
  }
4772
- return text19(parts.join("\n"));
5436
+ return text21(parts.join("\n"));
4773
5437
  } catch (e) {
4774
- return fail21(errMsg21(e));
5438
+ return fail23(errMsg22(e));
4775
5439
  }
4776
5440
  }
4777
5441
  );
4778
5442
  }
4779
5443
 
4780
5444
  // src/verify/editWatcher.ts
4781
- var import_node_fs3 = __toESM(require("fs"));
4782
- var import_node_path3 = __toESM(require("path"));
5445
+ var import_node_fs4 = __toESM(require("fs"));
5446
+ var import_node_path4 = __toESM(require("path"));
4783
5447
  var WATCHED_EXT = /* @__PURE__ */ new Set([
4784
5448
  ".ts",
4785
5449
  ".tsx",
@@ -4822,14 +5486,14 @@ var EditWatcher = class {
4822
5486
  if (mode === "never" || this.started) return;
4823
5487
  this.started = true;
4824
5488
  try {
4825
- this.watcher = import_node_fs3.default.watch(
5489
+ this.watcher = import_node_fs4.default.watch(
4826
5490
  rootDir,
4827
5491
  { recursive: true },
4828
5492
  (_event, filename) => {
4829
5493
  if (!filename) return;
4830
5494
  const rel2 = String(filename);
4831
- if (rel2.split(import_node_path3.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
4832
- if (!WATCHED_EXT.has(import_node_path3.default.extname(rel2))) return;
5495
+ if (rel2.split(import_node_path4.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
5496
+ if (!WATCHED_EXT.has(import_node_path4.default.extname(rel2))) return;
4833
5497
  this.noteEdit(rel2);
4834
5498
  }
4835
5499
  );
@@ -4929,6 +5593,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
4929
5593
  restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
4930
5594
  pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
4931
5595
 
5596
+ Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
5597
+ picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
5598
+ all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
5599
+ (pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
5600
+ the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
5601
+ you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
5602
+ one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
5603
+ can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
5604
+ the app. \`camera_status\` shows what is being decoded right now, which separates "the
5605
+ scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
5606
+ works, and \`camera_stop\` turns it off when you are done. These need no device connection
5607
+ and no Buoy integration in the app.
5608
+
4932
5609
  Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
4933
5610
  a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
4934
5611
  iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
@@ -4992,6 +5669,7 @@ require the connected app to have an active Buoy Pro license. If they error with
4992
5669
  required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
4993
5670
  var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
4994
5671
  "measure_renders",
5672
+ "scenario_action",
4995
5673
  "run_flow",
4996
5674
  "run_benchmark_batch",
4997
5675
  "get_triage",
@@ -5042,7 +5720,9 @@ function createMcpServer(sync) {
5042
5720
  registerRoutesTools(server, sync);
5043
5721
  registerConsoleTools(server, sync);
5044
5722
  registerTimeMachineTools(server, sync);
5723
+ registerScenariosTools(server, sync);
5045
5724
  registerScreenshotTools(server, sync);
5725
+ registerCameraTools(server);
5046
5726
  registerInteractTools(server, sync);
5047
5727
  registerJsThreadTools(server, sync);
5048
5728
  registerImagesTools(server, sync);
@@ -5051,16 +5731,16 @@ function createMcpServer(sync) {
5051
5731
  return server;
5052
5732
  }
5053
5733
  var deviceIdField = {
5054
- deviceId: import_zod22.z.string().optional().describe(
5734
+ deviceId: import_zod24.z.string().optional().describe(
5055
5735
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5056
5736
  )
5057
5737
  };
5058
5738
 
5059
5739
  // src/init.ts
5060
- var import_node_child_process2 = require("child_process");
5061
- var import_node_fs4 = __toESM(require("fs"));
5062
- var import_node_os3 = __toESM(require("os"));
5063
- var import_node_path4 = __toESM(require("path"));
5740
+ var import_node_child_process3 = require("child_process");
5741
+ var import_node_fs5 = __toESM(require("fs"));
5742
+ var import_node_os4 = __toESM(require("os"));
5743
+ var import_node_path5 = __toESM(require("path"));
5064
5744
  function defaultEnv() {
5065
5745
  return { BUOY_VERIFY: "auto" };
5066
5746
  }
@@ -5068,19 +5748,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
5068
5748
  var PROBE_TIMEOUT_MS3 = 3e3;
5069
5749
  function runInit(opts = {}) {
5070
5750
  const cwd = opts.cwd ?? process.cwd();
5071
- const skillSrcDir = opts.skillSrcDir ?? import_node_path4.default.resolve(__dirname, "../skill");
5751
+ const skillSrcDir = opts.skillSrcDir ?? import_node_path5.default.resolve(__dirname, "../skill");
5072
5752
  const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
5073
5753
  const entry = resolveEntry(opts, log);
5074
5754
  const done = [];
5075
- upsertMcpConfig(import_node_path4.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
5076
- import_node_fs4.default.mkdirSync(import_node_path4.default.join(cwd, ".cursor"), { recursive: true });
5077
- upsertMcpConfig(import_node_path4.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
5078
- if (import_node_fs4.default.existsSync(import_node_path4.default.join(cwd, ".vscode"))) {
5079
- upsertMcpConfig(import_node_path4.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
5755
+ upsertMcpConfig(import_node_path5.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
5756
+ import_node_fs5.default.mkdirSync(import_node_path5.default.join(cwd, ".cursor"), { recursive: true });
5757
+ upsertMcpConfig(import_node_path5.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
5758
+ if (import_node_fs5.default.existsSync(import_node_path5.default.join(cwd, ".vscode"))) {
5759
+ upsertMcpConfig(import_node_path5.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
5080
5760
  }
5081
5761
  installSkills(skillSrcDir, cwd, done);
5082
- appendDebuggingGuide(import_node_path4.default.join(cwd, "CLAUDE.md"), cwd, done);
5083
- appendDebuggingGuide(import_node_path4.default.join(cwd, "AGENTS.md"), cwd, done);
5762
+ appendDebuggingGuide(import_node_path5.default.join(cwd, "CLAUDE.md"), cwd, done);
5763
+ appendDebuggingGuide(import_node_path5.default.join(cwd, "AGENTS.md"), cwd, done);
5084
5764
  log("Buoy MCP installed:");
5085
5765
  for (const line of done) log(" \u2022 " + line);
5086
5766
  log("");
@@ -5122,21 +5802,21 @@ function npxEntry() {
5122
5802
  return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
5123
5803
  }
5124
5804
  function localEntry(opts, log) {
5125
- const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path4.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
5805
+ const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path5.default.join(import_node_os4.default.homedir(), ".buoy-mcp");
5126
5806
  const registry = opts.registry ?? PUBLIC_REGISTRY;
5127
- const cliPath = installLocalCopy(installDir, registry, log);
5128
- if (cliPath) {
5129
- return { type: "stdio", command: "node", args: [cliPath], env: defaultEnv() };
5807
+ const cliPath2 = installLocalCopy(installDir, registry, log);
5808
+ if (cliPath2) {
5809
+ return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
5130
5810
  }
5131
5811
  log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
5132
5812
  return npxEntry();
5133
5813
  }
5134
5814
  function installLocalCopy(installDir, registry, log) {
5135
5815
  const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
5136
- import_node_fs4.default.mkdirSync(installDir, { recursive: true });
5816
+ import_node_fs5.default.mkdirSync(installDir, { recursive: true });
5137
5817
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
5138
5818
  log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
5139
- const res = (0, import_node_child_process2.spawnSync)(
5819
+ const res = (0, import_node_child_process3.spawnSync)(
5140
5820
  npm,
5141
5821
  [
5142
5822
  "install",
@@ -5156,12 +5836,12 @@ function installLocalCopy(installDir, registry, log) {
5156
5836
  log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
5157
5837
  return null;
5158
5838
  }
5159
- const cliPath = import_node_path4.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
5160
- if (!import_node_fs4.default.existsSync(cliPath)) {
5161
- log(`\u26A0 install finished but ${cliPath} is missing.`);
5839
+ const cliPath2 = import_node_path5.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
5840
+ if (!import_node_fs5.default.existsSync(cliPath2)) {
5841
+ log(`\u26A0 install finished but ${cliPath2} is missing.`);
5162
5842
  return null;
5163
5843
  }
5164
- return cliPath;
5844
+ return cliPath2;
5165
5845
  }
5166
5846
  function probeRegistry() {
5167
5847
  const registry = effectiveRegistry();
@@ -5179,14 +5859,14 @@ function effectiveRegistry() {
5179
5859
  }
5180
5860
  function npmConfigGet(key) {
5181
5861
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
5182
- const res = (0, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
5862
+ const res = (0, import_node_child_process3.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
5183
5863
  if (res.status !== 0 || typeof res.stdout !== "string") return null;
5184
5864
  const val = res.stdout.trim();
5185
5865
  if (!val || val === "undefined" || val === "null") return null;
5186
5866
  return val;
5187
5867
  }
5188
5868
  function hostReachable(registry) {
5189
- const done = (0, import_node_child_process2.spawnSync)(
5869
+ const done = (0, import_node_child_process3.spawnSync)(
5190
5870
  process.execPath,
5191
5871
  ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS3)],
5192
5872
  { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 + 2e3 }
@@ -5209,22 +5889,22 @@ fetch(url, { method: 'HEAD', signal: c.signal })
5209
5889
  `;
5210
5890
  function upsertMcpConfig(file, key, entry, cwd, done) {
5211
5891
  let json3 = {};
5212
- if (import_node_fs4.default.existsSync(file)) {
5892
+ if (import_node_fs5.default.existsSync(file)) {
5213
5893
  try {
5214
- json3 = JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
5894
+ json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
5215
5895
  } catch {
5216
- import_node_fs4.default.copyFileSync(file, file + ".bak");
5896
+ import_node_fs5.default.copyFileSync(file, file + ".bak");
5217
5897
  done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
5218
5898
  json3 = {};
5219
5899
  }
5220
5900
  } else {
5221
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
5901
+ import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
5222
5902
  }
5223
5903
  const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
5224
5904
  const existed = "buoy" in servers;
5225
5905
  servers.buoy = entry;
5226
5906
  json3[key] = servers;
5227
- import_node_fs4.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
5907
+ import_node_fs5.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
5228
5908
  done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
5229
5909
  }
5230
5910
  var GUIDE_HEADING = "# Debugging with Buoy";
@@ -5284,9 +5964,9 @@ mentions it.
5284
5964
  `;
5285
5965
  function appendDebuggingGuide(file, cwd, done) {
5286
5966
  let existing = "";
5287
- if (import_node_fs4.default.existsSync(file)) {
5967
+ if (import_node_fs5.default.existsSync(file)) {
5288
5968
  try {
5289
- existing = import_node_fs4.default.readFileSync(file, "utf8");
5969
+ existing = import_node_fs5.default.readFileSync(file, "utf8");
5290
5970
  } catch {
5291
5971
  done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
5292
5972
  return;
@@ -5298,40 +5978,40 @@ function appendDebuggingGuide(file, cwd, done) {
5298
5978
  }
5299
5979
  const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
5300
5980
  try {
5301
- import_node_fs4.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
5981
+ import_node_fs5.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
5302
5982
  done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
5303
5983
  } catch {
5304
5984
  done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
5305
5985
  }
5306
5986
  }
5307
5987
  function installSkills(skillSrcDir, cwd, done) {
5308
- if (!import_node_fs4.default.existsSync(skillSrcDir)) {
5988
+ if (!import_node_fs5.default.existsSync(skillSrcDir)) {
5309
5989
  done.push("\u26A0 no bundled skills found \u2014 skipped");
5310
5990
  return;
5311
5991
  }
5312
- const skills = import_node_fs4.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs4.default.existsSync(import_node_path4.default.join(skillSrcDir, e.name, "SKILL.md")));
5992
+ const skills = import_node_fs5.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs5.default.existsSync(import_node_path5.default.join(skillSrcDir, e.name, "SKILL.md")));
5313
5993
  if (skills.length === 0) {
5314
5994
  done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
5315
5995
  return;
5316
5996
  }
5317
5997
  for (const skill of skills) {
5318
- const from = import_node_path4.default.join(skillSrcDir, skill.name);
5319
- const to = import_node_path4.default.join(cwd, ".claude", "skills", skill.name);
5998
+ const from = import_node_path5.default.join(skillSrcDir, skill.name);
5999
+ const to = import_node_path5.default.join(cwd, ".claude", "skills", skill.name);
5320
6000
  copyDir(from, to);
5321
- done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path4.default.join(to, "SKILL.md"), cwd)}`);
6001
+ done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path5.default.join(to, "SKILL.md"), cwd)}`);
5322
6002
  }
5323
6003
  }
5324
6004
  function copyDir(from, to) {
5325
- import_node_fs4.default.mkdirSync(to, { recursive: true });
5326
- for (const entry of import_node_fs4.default.readdirSync(from, { withFileTypes: true })) {
5327
- const s = import_node_path4.default.join(from, entry.name);
5328
- const d = import_node_path4.default.join(to, entry.name);
6005
+ import_node_fs5.default.mkdirSync(to, { recursive: true });
6006
+ for (const entry of import_node_fs5.default.readdirSync(from, { withFileTypes: true })) {
6007
+ const s = import_node_path5.default.join(from, entry.name);
6008
+ const d = import_node_path5.default.join(to, entry.name);
5329
6009
  if (entry.isDirectory()) copyDir(s, d);
5330
- else import_node_fs4.default.copyFileSync(s, d);
6010
+ else import_node_fs5.default.copyFileSync(s, d);
5331
6011
  }
5332
6012
  }
5333
6013
  function rel(file, cwd) {
5334
- const r = import_node_path4.default.relative(cwd, file);
6014
+ const r = import_node_path5.default.relative(cwd, file);
5335
6015
  return r && !r.startsWith("..") ? r : file;
5336
6016
  }
5337
6017