@buoy-gg/mcp 7.0.33 → 7.0.35

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
@@ -641,7 +641,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
641
641
 
642
642
  // src/server.ts
643
643
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
644
- var import_zod24 = require("zod");
644
+ var import_zod26 = require("zod");
645
645
 
646
646
  // src/tools/generic.ts
647
647
  var import_zod = require("zod");
@@ -652,7 +652,7 @@ var import_node_os = __toESM(require("os"));
652
652
  var import_node_path = __toESM(require("path"));
653
653
 
654
654
  // src/version.ts
655
- var VERSION = "7.0.33" ? "7.0.33" : "0.0.0-dev";
655
+ var VERSION = "7.0.35" ? "7.0.35" : "0.0.0-dev";
656
656
  var PACKAGE_NAME = "@buoy-gg/mcp";
657
657
 
658
658
  // src/update/checkForUpdate.ts
@@ -827,7 +827,40 @@ function errMsg(e) {
827
827
  }
828
828
 
829
829
  // src/tools/packs/perfMonitor.ts
830
+ var import_zod3 = require("zod");
831
+
832
+ // src/tools/packKit.ts
830
833
  var import_zod2 = require("zod");
834
+ var text = (s) => ({
835
+ content: [{ type: "text", text: s }]
836
+ });
837
+ var fail2 = (message) => ({
838
+ isError: true,
839
+ content: [{ type: "text", text: message }]
840
+ });
841
+ var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
842
+ var deviceIdInput = import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");
843
+ function clampLimit(value, fallback) {
844
+ return typeof value === "number" && value > 0 ? value : fallback;
845
+ }
846
+ function staleHeader(stale) {
847
+ return stale ? `${stale}
848
+
849
+ ` : "";
850
+ }
851
+ function inactiveToolMessage(options) {
852
+ return `No ${options.toolId} result from the device. The ${options.toolId} tool may be inactive, or the app needs a newer Buoy (${options.mcpTool} requires the ${options.action} action).`;
853
+ }
854
+ function withDevice(sync, run2) {
855
+ return async (args) => {
856
+ try {
857
+ const target = sync.requireProDevice(args.deviceId);
858
+ return await run2(args, target);
859
+ } catch (e) {
860
+ return fail2(errMsg2(e));
861
+ }
862
+ };
863
+ }
831
864
 
832
865
  // src/tools/packs/perfMonitorTypes.ts
833
866
  var PERF_TOOL_ID = "perf-monitor";
@@ -1144,13 +1177,10 @@ function round(n, d) {
1144
1177
  var POLL_INTERVAL_MS = 1500;
1145
1178
  var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
1146
1179
  var json2 = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
1147
- var text = (s) => ({ content: [{ type: "text", text: s }] });
1148
- var fail2 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1149
- var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
1150
- var caseSchema = import_zod2.z.object({
1151
- name: import_zod2.z.string().describe("Display name for this variant (becomes the saved run's name)."),
1152
- params: import_zod2.z.record(import_zod2.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
1153
- route: import_zod2.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
1180
+ var caseSchema = import_zod3.z.object({
1181
+ name: import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),
1182
+ params: import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
1183
+ route: import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
1154
1184
  });
1155
1185
  async function readDeviceSettings(sync, deviceId) {
1156
1186
  try {
@@ -1269,22 +1299,22 @@ function registerPerfMonitorTools(server, sync) {
1269
1299
  {
1270
1300
  description: "Run a perf-monitor benchmark batch and return a RANKED comparison of all cases (winner by UI FPS). Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",
1271
1301
  inputSchema: {
1272
- targetRoute: import_zod2.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
1273
- cases: import_zod2.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
1274
- bounceRoute: import_zod2.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
1275
- runsPerCase: import_zod2.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
1276
- perCaseDurationMs: import_zod2.z.number().optional().describe("How long to record each run. Default 5000."),
1277
- settleMs: import_zod2.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),
1278
- coolDownMs: import_zod2.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
1279
- navTimeoutMs: import_zod2.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
1280
- discardWarmupRuns: import_zod2.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
1281
- shuffleCases: import_zod2.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
1282
- reloadBetweenCases: import_zod2.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
1283
- captureRenders: import_zod2.z.boolean().optional().describe(
1302
+ targetRoute: import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
1303
+ cases: import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
1304
+ bounceRoute: import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
1305
+ runsPerCase: import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
1306
+ perCaseDurationMs: import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),
1307
+ settleMs: import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),
1308
+ coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
1309
+ navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
1310
+ discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
1311
+ shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
1312
+ reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
1313
+ captureRenders: import_zod3.z.boolean().optional().describe(
1284
1314
  "Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds slight JS-thread overhead applied equally to every case. Default true."
1285
1315
  ),
1286
- deviceId: import_zod2.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
1287
- maxWaitMs: import_zod2.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
1316
+ deviceId: import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
1317
+ maxWaitMs: import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
1288
1318
  }
1289
1319
  },
1290
1320
  async (args) => {
@@ -1354,7 +1384,7 @@ ${JSON.stringify(summary, null, 2)}
1354
1384
  "get_automation_status",
1355
1385
  {
1356
1386
  description: "Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",
1357
- inputSchema: { deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).") }
1387
+ inputSchema: { deviceId: import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).") }
1358
1388
  },
1359
1389
  async ({ deviceId }) => {
1360
1390
  try {
@@ -1375,8 +1405,8 @@ ${JSON.stringify(summary, null, 2)}
1375
1405
  {
1376
1406
  description: "Block until the in-flight perf-monitor batch finishes, then return its ranked comparison. Use this if a batch was started another way (e.g. on the device).",
1377
1407
  inputSchema: {
1378
- deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
1379
- maxWaitMs: import_zod2.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")
1408
+ deviceId: deviceIdInput,
1409
+ maxWaitMs: import_zod3.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")
1380
1410
  }
1381
1411
  },
1382
1412
  async ({ deviceId, maxWaitMs }) => {
@@ -1449,8 +1479,8 @@ ${JSON.stringify(ranking, null, 2)}
1449
1479
  {
1450
1480
  description: "Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",
1451
1481
  inputSchema: {
1452
- batchId: import_zod2.z.string().describe("The batch id to report on."),
1453
- deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1482
+ batchId: import_zod3.z.string().describe("The batch id to report on."),
1483
+ deviceId: deviceIdInput
1454
1484
  }
1455
1485
  },
1456
1486
  async ({ batchId, deviceId }) => {
@@ -1477,9 +1507,9 @@ ${JSON.stringify(ranking, null, 2)}
1477
1507
  {
1478
1508
  description: "Compare two saved benchmark runs head-to-head: avg + p95 for FPS/CPU/memory, jank, AND render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use this to prove a memoization or re-render fix actually landed: the wasted-render delta is the number that says so.",
1479
1509
  inputSchema: {
1480
- reportIdA: import_zod2.z.string().describe("First (baseline) report id."),
1481
- reportIdB: import_zod2.z.string().describe("Second report id to compare against the baseline."),
1482
- deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1510
+ reportIdA: import_zod3.z.string().describe("First (baseline) report id."),
1511
+ reportIdB: import_zod3.z.string().describe("Second report id to compare against the baseline."),
1512
+ deviceId: deviceIdInput
1483
1513
  }
1484
1514
  },
1485
1515
  async ({ reportIdA, reportIdB, deviceId }) => {
@@ -1507,8 +1537,8 @@ ${JSON.stringify(ranking, null, 2)}
1507
1537
  {
1508
1538
  description: "Read the device's saved benchmark settings (per-page duration, runs, cooldown, reload, shuffle) in plain English with a time estimate. Call this before run_benchmark_batch so you can tell the user what the run will do \u2014 run_benchmark_batch inherits these unless you override them.",
1509
1539
  inputSchema: {
1510
- pages: import_zod2.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
1511
- deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1540
+ pages: import_zod3.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
1541
+ deviceId: deviceIdInput
1512
1542
  }
1513
1543
  },
1514
1544
  async ({ pages, deviceId }) => {
@@ -1538,16 +1568,16 @@ ${JSON.stringify(settings, null, 2)}
1538
1568
  {
1539
1569
  description: `PERSIST benchmark settings on the device so they stick across runs. Only use when the user explicitly wants to save a change ("always", "save this"). For a one-off, pass overrides to run_benchmark_batch instead \u2014 don't change their saved profile.`,
1540
1570
  inputSchema: {
1541
- perCaseDurationMs: import_zod2.z.number().optional().describe("Recording length per page (ms)."),
1542
- settleMs: import_zod2.z.number().optional().describe("Idle after navigation before recording (ms)."),
1543
- coolDownMs: import_zod2.z.number().optional().describe("Idle between runs/cases (ms)."),
1544
- navTimeoutMs: import_zod2.z.number().optional().describe("Max wait for navigation per run (ms)."),
1545
- runsPerCase: import_zod2.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
1546
- discardWarmupRuns: import_zod2.z.number().optional().describe("Discard first N runs of each page before the median."),
1547
- shuffleCases: import_zod2.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
1548
- reloadBetweenCases: import_zod2.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
1549
- captureRenders: import_zod2.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
1550
- deviceId: import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1571
+ perCaseDurationMs: import_zod3.z.number().optional().describe("Recording length per page (ms)."),
1572
+ settleMs: import_zod3.z.number().optional().describe("Idle after navigation before recording (ms)."),
1573
+ coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases (ms)."),
1574
+ navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),
1575
+ runsPerCase: import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
1576
+ discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),
1577
+ shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
1578
+ reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
1579
+ captureRenders: import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
1580
+ deviceId: deviceIdInput
1551
1581
  }
1552
1582
  },
1553
1583
  async (args) => {
@@ -1595,11 +1625,8 @@ ${JSON.stringify(saved ?? settings, null, 2)}
1595
1625
  }
1596
1626
 
1597
1627
  // src/tools/packs/events.ts
1598
- var import_zod3 = require("zod");
1628
+ var import_zod4 = require("zod");
1599
1629
  var EVENTS_TOOL_ID = "events";
1600
- var text2 = (s) => ({ content: [{ type: "text", text: s }] });
1601
- var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1602
- var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
1603
1630
  var SOURCE_ALIASES = {
1604
1631
  storage: ["storage-async", "storage-mmkv"],
1605
1632
  "react-query": ["react-query", "react-query-query", "react-query-mutation"]
@@ -1614,13 +1641,13 @@ function registerEventsTools(server, sync) {
1614
1641
  {
1615
1642
  description: "The UNIVERSAL, token-bounded activity reader for what's happening in the app across ALL Buoy tools. Check this timeline BEFORE driving the UI to reproduce a bug \u2014 the failure is usually already recorded here. The events tool aggregates every source \u2014 network requests, redux/zustand/jotai actions, react-query query/mutation updates, storage writes, route changes, renders \u2014 so filter to one with `sources`: ['network'] lists HTTP requests, ['redux'] dispatched actions, ['react-query'] query updates, ['storage'] writes, ['route'] navigation. Returns a compact one-line summary per event (source \xB7 status \xB7 title \xB7 timing) by DEFAULT \u2014 not the heavy raw payloads \u2014 so it's cheap. Re-call with includeData:true, a bigger limit, or specific sources only when you need detail. PREFER this over get_snapshot (and over reading source code) for any 'what happened' / 'show me the requests/actions' question; get_snapshot is the raw heavy current-state escape hatch.",
1616
1643
  inputSchema: {
1617
- limit: import_zod3.z.number().optional().describe("Most-recent N events to include. Default 25."),
1618
- sources: import_zod3.z.array(import_zod3.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
1619
- status: import_zod3.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
1620
- includeData: import_zod3.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
1621
- format: import_zod3.z.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
1622
- preset: import_zod3.z.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
1623
- deviceId: import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1644
+ limit: import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),
1645
+ sources: import_zod4.z.array(import_zod4.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
1646
+ status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
1647
+ includeData: import_zod4.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
1648
+ format: import_zod4.z.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
1649
+ preset: import_zod4.z.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
1650
+ deviceId: deviceIdInput
1624
1651
  }
1625
1652
  },
1626
1653
  async (args) => {
@@ -1639,7 +1666,7 @@ function registerEventsTools(server, sync) {
1639
1666
  target
1640
1667
  );
1641
1668
  if (!res || typeof res.output !== "string") {
1642
- return fail3(
1669
+ return fail2(
1643
1670
  "No events result from the device. The events tool may be inactive, or the app needs a newer Buoy (get_events requires the exportEvents action)."
1644
1671
  );
1645
1672
  }
@@ -1647,20 +1674,17 @@ function registerEventsTools(server, sync) {
1647
1674
  const note = `
1648
1675
 
1649
1676
  _Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
1650
- return text2((res.output || "(no events captured)") + note);
1677
+ return text((res.output || "(no events captured)") + note);
1651
1678
  } catch (e) {
1652
- return fail3(errMsg3(e));
1679
+ return fail2(errMsg2(e));
1653
1680
  }
1654
1681
  }
1655
1682
  );
1656
1683
  }
1657
1684
 
1658
1685
  // src/tools/packs/network.ts
1659
- var import_zod4 = require("zod");
1686
+ var import_zod5 = require("zod");
1660
1687
  var NETWORK_TOOL_ID = "network";
1661
- var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1662
- var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1663
- var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1664
1688
  var masterNote = (state) => {
1665
1689
  if (state?.enabled !== false) return "";
1666
1690
  return state.autoPaused ? ' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).' : " \u2014 MASTER SWITCH IS OFF, nothing is firing";
@@ -1786,12 +1810,12 @@ function registerNetworkTools(server, sync) {
1786
1810
  {
1787
1811
  description: "List captured HTTP requests WITH their request ids, plus which ones are pinned/saved. API data wrong or calls failing \u2192 read the actual request+response bodies here instead of guessing from code; capture starts at app launch, so boot-time requests (session bootstrap, first queries) are included. Use this (not get_events) when you need to ACT on a request \u2014 every network action is keyed by the id, and get_events does not emit ids. Compact by default: one line per request (id \xB7 status \xB7 method \xB7 path \xB7 timing \xB7 size). Set flagged:'pinned'|'saved'|'any' to read the developer's KEPT requests \u2014 those are snapshots that outlive Clear, the 500-request cap and app restarts, so treat a pinned request as 'a human flagged this as the problem'. Filter with status:'errors' to go straight to failures, or `pattern` to match a URL substring. includeBodies:true adds truncated request/response payloads (heavier).",
1788
1812
  inputSchema: {
1789
- limit: import_zod4.z.number().optional().describe("Most-recent N requests. Default 25."),
1790
- pattern: import_zod4.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
1791
- status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
1792
- flagged: import_zod4.z.enum(["pinned", "saved", "any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These survive Clear and app restarts."),
1793
- includeBodies: import_zod4.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
1794
- deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1813
+ limit: import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),
1814
+ pattern: import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
1815
+ status: import_zod5.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
1816
+ flagged: import_zod5.z.enum(["pinned", "saved", "any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These survive Clear and app restarts."),
1817
+ includeBodies: import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
1818
+ deviceId: deviceIdInput
1795
1819
  }
1796
1820
  },
1797
1821
  async (args) => {
@@ -1815,7 +1839,7 @@ function registerNetworkTools(server, sync) {
1815
1839
  );
1816
1840
  const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1817
1841
  if (rows2.length === 0) {
1818
- return text3(
1842
+ return text(
1819
1843
  `No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
1820
1844
 
1821
1845
  The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`
@@ -1832,7 +1856,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
1832
1856
  if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
1833
1857
  return out;
1834
1858
  }).join("\n");
1835
- return text3(
1859
+ return text(
1836
1860
  `**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
1837
1861
 
1838
1862
  ${body2}
@@ -1843,7 +1867,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
1843
1867
  const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1844
1868
  if (rows.length === 0) {
1845
1869
  const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
1846
- return text3(`No requests on ${target}. ${why}`);
1870
+ return text(`No requests on ${target}. ${why}`);
1847
1871
  }
1848
1872
  const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
1849
1873
  const body = rows.map((e, i) => {
@@ -1853,7 +1877,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
1853
1877
  return out;
1854
1878
  }).join("\n");
1855
1879
  const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
1856
- return text3(
1880
+ return text(
1857
1881
  `**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
1858
1882
 
1859
1883
  ${body}
@@ -1861,7 +1885,7 @@ ${body}
1861
1885
  _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._${skippedNote(live.skipped)}`
1862
1886
  );
1863
1887
  } catch (e) {
1864
- return fail4(errMsg4(e));
1888
+ return fail2(errMsg2(e));
1865
1889
  }
1866
1890
  }
1867
1891
  );
@@ -1870,10 +1894,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1870
1894
  {
1871
1895
  description: "Act on captured HTTP requests: PIN one (holds it at the top of the developer's Network list, ignoring their filters), SAVE one to their favorites, or clear the list. Pinning and saving keep a full snapshot that survives Clear, the 500-request cap and app restarts \u2014 so this is how you hand a request to a human ('I pinned the failing call') or keep one you'll need after the log rolls over. Get ids from get_network_requests (get_events does not emit them). `remove` takes the record `key` from get_network_requests({flagged:'any'}), not a request id.",
1872
1896
  inputSchema: {
1873
- action: import_zod4.z.enum(["pin", "unpin", "save", "unsave", "remove", "clearPinned", "clearSaved", "clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),
1874
- id: import_zod4.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
1875
- key: import_zod4.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
1876
- deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1897
+ action: import_zod5.z.enum(["pin", "unpin", "save", "unsave", "remove", "clearPinned", "clearSaved", "clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),
1898
+ id: import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
1899
+ key: import_zod5.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
1900
+ deviceId: deviceIdInput
1877
1901
  }
1878
1902
  },
1879
1903
  async (args) => {
@@ -1881,10 +1905,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1881
1905
  const target = sync.requireProDevice(args.deviceId);
1882
1906
  const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
1883
1907
  if (needsId && !args.id) {
1884
- return fail4(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
1908
+ return fail2(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
1885
1909
  }
1886
1910
  if (args.action === "remove" && !args.key) {
1887
- return fail4('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
1911
+ return fail2('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
1888
1912
  }
1889
1913
  switch (args.action) {
1890
1914
  case "pin":
@@ -1895,10 +1919,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1895
1919
  { id: args.id, pinned: args.action === "pin" },
1896
1920
  target
1897
1921
  );
1898
- if (res === null) return fail4(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
1922
+ if (res === null) return fail2(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
1899
1923
  const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
1900
- if (capped) return fail4(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
1901
- return text3(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
1924
+ if (capped) return fail2(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
1925
+ return text(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
1902
1926
  }
1903
1927
  case "save":
1904
1928
  case "unsave": {
@@ -1908,26 +1932,26 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1908
1932
  { id: args.id, saved: args.action === "save" },
1909
1933
  target
1910
1934
  );
1911
- if (res === null) return fail4(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
1935
+ if (res === null) return fail2(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
1912
1936
  const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
1913
- if (capped) return fail4(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
1914
- return text3(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
1937
+ if (capped) return fail2(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
1938
+ return text(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
1915
1939
  }
1916
1940
  case "remove":
1917
1941
  await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
1918
- return text3(`Removed kept record \`${args.key}\` on ${target}.`);
1942
+ return text(`Removed kept record \`${args.key}\` on ${target}.`);
1919
1943
  case "clearPinned":
1920
1944
  await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
1921
- return text3(`Unpinned all requests on ${target} (saved ones kept).`);
1945
+ return text(`Unpinned all requests on ${target} (saved ones kept).`);
1922
1946
  case "clearSaved":
1923
1947
  await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
1924
- return text3(`Cleared saved requests on ${target} (pinned ones kept).`);
1948
+ return text(`Cleared saved requests on ${target} (pinned ones kept).`);
1925
1949
  case "clearRequests":
1926
1950
  await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
1927
- return text3(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
1951
+ return text(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
1928
1952
  }
1929
1953
  } catch (e) {
1930
- return fail4(errMsg4(e));
1954
+ return fail2(errMsg2(e));
1931
1955
  }
1932
1956
  }
1933
1957
  );
@@ -1936,48 +1960,48 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1936
1960
  {
1937
1961
  description: "Force matching HTTP requests to return a chosen status/body, to fail outright, or to arrive late \u2014 without touching the backend or the app's code. This is how you drive an app into a state it won't reach on its own: force a 500 and see whether the error UI appears, return [] and check the empty state, simulate offline or a slow connection. Rules run INSIDE the app and survive an app reload, so they also cover the boot path. After adding one, trigger the request (navigate/tap_element) and read the result with get_network_requests or describe_screen. ALWAYS remove your rules when done \u2014 a forgotten rule looks exactly like a real bug. Requires Buoy Pro.",
1938
1962
  inputSchema: {
1939
- action: import_zod4.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
1963
+ action: import_zod5.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
1940
1964
  "add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
1941
1965
  ),
1942
- urlPattern: import_zod4.z.string().optional().describe(
1966
+ urlPattern: import_zod5.z.string().optional().describe(
1943
1967
  "For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."
1944
1968
  ),
1945
- fromRequestId: import_zod4.z.string().optional().describe(
1969
+ fromRequestId: import_zod5.z.string().optional().describe(
1946
1970
  "For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."
1947
1971
  ),
1948
- methods: import_zod4.z.array(import_zod4.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
1949
- kind: import_zod4.z.enum(["respond", "fail", "delay"]).optional().describe(
1972
+ methods: import_zod5.z.array(import_zod5.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
1973
+ kind: import_zod5.z.enum(["respond", "fail", "delay"]).optional().describe(
1950
1974
  "respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."
1951
1975
  ),
1952
- status: import_zod4.z.number().optional().describe(
1976
+ status: import_zod5.z.number().optional().describe(
1953
1977
  "For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."
1954
1978
  ),
1955
- body: import_zod4.z.union([import_zod4.z.string(), import_zod4.z.record(import_zod4.z.unknown()), import_zod4.z.array(import_zod4.z.unknown())]).optional().describe(
1979
+ body: import_zod5.z.union([import_zod5.z.string(), import_zod5.z.record(import_zod5.z.unknown()), import_zod5.z.array(import_zod5.z.unknown())]).optional().describe(
1956
1980
  "For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
1957
1981
  ),
1958
- statusText: import_zod4.z.string().optional().describe(
1982
+ statusText: import_zod5.z.string().optional().describe(
1959
1983
  "For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
1960
1984
  ),
1961
- headers: import_zod4.z.record(import_zod4.z.string()).optional().describe("For kind 'respond': response headers to send."),
1962
- failKind: import_zod4.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
1963
- delayMs: import_zod4.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
1964
- times: import_zod4.z.number().optional().describe(
1985
+ headers: import_zod5.z.record(import_zod5.z.string()).optional().describe("For kind 'respond': response headers to send."),
1986
+ failKind: import_zod5.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
1987
+ delayMs: import_zod5.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
1988
+ times: import_zod5.z.number().optional().describe(
1965
1989
  "Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
1966
1990
  ),
1967
- alternate: import_zod4.z.boolean().optional().describe(
1991
+ alternate: import_zod5.z.boolean().optional().describe(
1968
1992
  "Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."
1969
1993
  ),
1970
- name: import_zod4.z.string().optional().describe("Label shown in the developer's UI."),
1971
- enabled: import_zod4.z.boolean().optional().describe(
1994
+ name: import_zod5.z.string().optional().describe("Label shown in the developer's UI."),
1995
+ enabled: import_zod5.z.boolean().optional().describe(
1972
1996
  "For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
1973
1997
  ),
1974
- id: import_zod4.z.string().optional().describe(
1998
+ id: import_zod5.z.string().optional().describe(
1975
1999
  "Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
1976
2000
  ),
1977
- url: import_zod4.z.string().optional().describe(
2001
+ url: import_zod5.z.string().optional().describe(
1978
2002
  "For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
1979
2003
  ),
1980
- deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2004
+ deviceId: deviceIdInput
1981
2005
  }
1982
2006
  },
1983
2007
  async (args) => {
@@ -1986,7 +2010,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1986
2010
  switch (args.action) {
1987
2011
  case "add": {
1988
2012
  if (!args.urlPattern && !args.fromRequestId) {
1989
- return fail4(
2013
+ return fail2(
1990
2014
  "`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'."
1991
2015
  );
1992
2016
  }
@@ -2018,13 +2042,13 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2018
2042
  );
2019
2043
  const shaped = res;
2020
2044
  if (!shaped?.ok) {
2021
- return fail4(shaped?.error ?? `Could not add the rule on ${target}.`);
2045
+ return fail2(shaped?.error ?? `Could not add the rule on ${target}.`);
2022
2046
  }
2023
2047
  const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
2024
2048
  const armed = shaped.rule?.enabled === false ? `
2025
2049
 
2026
2050
  It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
2027
- return text3(
2051
+ return text(
2028
2052
  `Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
2029
2053
 
2030
2054
  Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${shaped.rule?.id}"}) when you're done.`
@@ -2032,14 +2056,14 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2032
2056
  }
2033
2057
  case "remove": {
2034
2058
  if (!args.id) {
2035
- return fail4('`remove` needs an `id`. List them with network_override({action:"list"}).');
2059
+ return fail2('`remove` needs an `id`. List them with network_override({action:"list"}).');
2036
2060
  }
2037
2061
  await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
2038
- return text3(`Removed override \`${args.id}\` on ${target}.`);
2062
+ return text(`Removed override \`${args.id}\` on ${target}.`);
2039
2063
  }
2040
2064
  case "clear":
2041
2065
  await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
2042
- return text3(`Removed all override rules on ${target}. Traffic is real again.`);
2066
+ return text(`Removed all override rules on ${target}. Traffic is real again.`);
2043
2067
  case "enable":
2044
2068
  case "disable": {
2045
2069
  const enabled = args.action === "enable";
@@ -2050,12 +2074,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2050
2074
  { id: args.id, enabled },
2051
2075
  target
2052
2076
  );
2053
- return text3(
2077
+ return text(
2054
2078
  `Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
2055
2079
  );
2056
2080
  }
2057
2081
  await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
2058
- return text3(
2082
+ return text(
2059
2083
  enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
2060
2084
  );
2061
2085
  }
@@ -2070,11 +2094,11 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2070
2094
  if (args.id) {
2071
2095
  const rule = rules.find((r) => r.id === args.id);
2072
2096
  if (!rule) {
2073
- return fail4(
2097
+ return fail2(
2074
2098
  `No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
2075
2099
  );
2076
2100
  }
2077
- return text3(
2101
+ return text(
2078
2102
  `**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
2079
2103
 
2080
2104
  \`\`\`json
@@ -2083,7 +2107,7 @@ ${JSON.stringify(rule, null, 2)}
2083
2107
  );
2084
2108
  }
2085
2109
  if (rules.length === 0) {
2086
- return text3(
2110
+ return text(
2087
2111
  `No override rules on ${target}.
2088
2112
 
2089
2113
  Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
@@ -2111,7 +2135,7 @@ Add one with network_override({action:"add", urlPattern:"*", status:500}) to for
2111
2135
  return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
2112
2136
  \u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
2113
2137
  });
2114
- return text3(
2138
+ return text(
2115
2139
  `**Override rules on ${target}**${masterNote(res)}
2116
2140
 
2117
2141
  ${rows.join("\n")}
@@ -2127,11 +2151,11 @@ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).
2127
2151
  target
2128
2152
  );
2129
2153
  if (!res) {
2130
- return fail4(
2154
+ return fail2(
2131
2155
  `${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
2132
2156
  );
2133
2157
  }
2134
- return text3(
2158
+ return text(
2135
2159
  `**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
2136
2160
 
2137
2161
  \`\`\`json
@@ -2143,24 +2167,21 @@ A rule that never fires is usually one of: the master switch is off, the interce
2143
2167
  }
2144
2168
  }
2145
2169
  } catch (e) {
2146
- return fail4(errMsg4(e));
2170
+ return fail2(errMsg2(e));
2147
2171
  }
2148
2172
  }
2149
2173
  );
2150
2174
  }
2151
2175
 
2152
2176
  // src/tools/packs/reactQuery.ts
2153
- var import_zod5 = require("zod");
2177
+ var import_zod6 = require("zod");
2154
2178
  var RQ_TOOL_ID = "query";
2155
2179
  var RQ_TOOL_ID_LEGACY = "react-query";
2156
- var text4 = (s) => ({ content: [{ type: "text", text: s }] });
2157
- var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2158
- var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
2159
2180
  async function callQueryAction(sync, action, params, target) {
2160
2181
  try {
2161
2182
  return await sync.callAction(RQ_TOOL_ID, action, params, target);
2162
2183
  } catch (e) {
2163
- const msg = errMsg5(e);
2184
+ const msg = errMsg2(e);
2164
2185
  if (!/unknown tool/i.test(msg)) throw e;
2165
2186
  return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
2166
2187
  }
@@ -2188,10 +2209,10 @@ function registerReactQueryTools(server, sync) {
2188
2209
  {
2189
2210
  description: "Read the app's CURRENT React Query cache \u2014 every query with its status (fresh/stale/fetching/error), staleness, observer count, last-updated time, and any error. Compact by DEFAULT (no cached payloads) so it's token-cheap; pass includeData:true for the actual cached data, or staleOnly:true to see just stale queries. Use this for 'what's in the cache / why is this stale / which queries errored'. (For the history of query updates over time, use get_events with sources:['react-query'].) Each row's hash is what you pass to react_query_action.",
2190
2211
  inputSchema: {
2191
- includeData: import_zod5.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
2192
- staleOnly: import_zod5.z.boolean().optional().describe("Only stale queries. Default false."),
2193
- limit: import_zod5.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
2194
- deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2212
+ includeData: import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
2213
+ staleOnly: import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),
2214
+ limit: import_zod6.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
2215
+ deviceId: deviceIdInput
2195
2216
  }
2196
2217
  },
2197
2218
  async (args) => {
@@ -2204,12 +2225,12 @@ function registerReactQueryTools(server, sync) {
2204
2225
  target
2205
2226
  );
2206
2227
  if (!res || !Array.isArray(res.queries)) {
2207
- return fail5(
2228
+ return fail2(
2208
2229
  "No react-query result from the device. The react-query tool may be inactive, or the app needs a newer Buoy (get_react_query requires the listQueries action)."
2209
2230
  );
2210
2231
  }
2211
2232
  if (res.queries.length === 0) {
2212
- return text4(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
2233
+ return text(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
2213
2234
  }
2214
2235
  const lines = res.queries.map((q, i) => {
2215
2236
  const icon = STATUS_ICON[q.status] ?? "\u2022";
@@ -2222,11 +2243,11 @@ function registerReactQueryTools(server, sync) {
2222
2243
  const staleCount = res.queries.filter((q) => q.isStale).length;
2223
2244
  const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
2224
2245
  const note = res.includedData ? "" : "\n\n_Compact view. Pass includeData:true for cached payloads, staleOnly:true to narrow. Act on one with react_query_action using the `hash` in backticks._";
2225
- return text4(`${header}
2246
+ return text(`${header}
2226
2247
 
2227
2248
  ${lines.join("\n")}${note}`);
2228
2249
  } catch (e) {
2229
- return fail5(errMsg5(e));
2250
+ return fail2(errMsg2(e));
2230
2251
  }
2231
2252
  }
2232
2253
  );
@@ -2235,9 +2256,9 @@ ${lines.join("\n")}${note}`);
2235
2256
  {
2236
2257
  description: "Mutate the app's React Query cache: invalidate / refetch / reset / remove a single query (by queryHash, from get_react_query), or clear the entire query cache. Use this to fix stale data, force a refetch, or reset state during debugging.",
2237
2258
  inputSchema: {
2238
- action: import_zod5.z.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
2239
- queryHash: import_zod5.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
2240
- deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2259
+ action: import_zod6.z.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
2260
+ queryHash: import_zod6.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
2261
+ deviceId: deviceIdInput
2241
2262
  }
2242
2263
  },
2243
2264
  async (args) => {
@@ -2245,174 +2266,145 @@ ${lines.join("\n")}${note}`);
2245
2266
  const target = sync.requireProDevice(args.deviceId);
2246
2267
  if (args.action === "clear") {
2247
2268
  await callQueryAction(sync, "clearQueryCache", {}, target);
2248
- return text4("Cleared the entire React Query cache.");
2269
+ return text("Cleared the entire React Query cache.");
2249
2270
  }
2250
2271
  if (!args.queryHash) {
2251
- return fail5(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
2272
+ return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
2252
2273
  }
2253
2274
  await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
2254
- return text4(`Ran ${args.action} on query \`${args.queryHash}\`.`);
2275
+ return text(`Ran ${args.action} on query \`${args.queryHash}\`.`);
2255
2276
  } catch (e) {
2256
- return fail5(errMsg5(e));
2277
+ return fail2(errMsg2(e));
2257
2278
  }
2258
2279
  }
2259
2280
  );
2260
2281
  }
2261
2282
 
2262
2283
  // src/tools/packs/zustand.ts
2263
- var import_zod6 = require("zod");
2284
+ var import_zod7 = require("zod");
2264
2285
  var ZUSTAND_TOOL_ID = "zustand";
2265
- var text5 = (s) => ({ content: [{ type: "text", text: s }] });
2266
- var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2267
- var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
2268
2286
  function registerZustandTools(server, sync) {
2269
2287
  server.registerTool(
2270
2288
  "get_zustand_state",
2271
2289
  {
2272
2290
  description: "Read the app's CURRENT Zustand store state \u2014 each registered store with its name, change count, persistence, and (by default) just its top-level keys so it's token-cheap. Pass includeValues:true for the full state objects. Use this for 'what's in my zustand stores / what's the current count'. (For the history of state changes over time, use get_events with sources:['zustand'].)",
2273
2291
  inputSchema: {
2274
- includeValues: import_zod6.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
2275
- limit: import_zod6.z.number().optional().describe("First N stores. Default all."),
2276
- deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2292
+ includeValues: import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
2293
+ limit: import_zod7.z.number().optional().describe("First N stores. Default all."),
2294
+ deviceId: deviceIdInput
2277
2295
  }
2278
2296
  },
2279
- async (args) => {
2280
- try {
2281
- const target = sync.requireProDevice(args.deviceId);
2282
- const res = await sync.callAction(
2283
- ZUSTAND_TOOL_ID,
2284
- "listStores",
2285
- { includeValues: args.includeValues ?? false, limit: args.limit },
2286
- target
2297
+ withDevice(sync, async (args, target) => {
2298
+ const res = await sync.callAction(
2299
+ ZUSTAND_TOOL_ID,
2300
+ "listStores",
2301
+ { includeValues: args.includeValues ?? false, limit: args.limit },
2302
+ target
2303
+ );
2304
+ if (!res || !Array.isArray(res.stores)) {
2305
+ return fail2(
2306
+ inactiveToolMessage({ toolId: ZUSTAND_TOOL_ID, mcpTool: "get_zustand_state", action: "listStores" })
2287
2307
  );
2288
- if (!res || !Array.isArray(res.stores)) {
2289
- return fail6(
2290
- "No zustand result from the device. The zustand tool may be inactive, or the app needs a newer Buoy (get_zustand_state requires the listStores action)."
2291
- );
2292
- }
2293
- if (res.stores.length === 0) {
2294
- return text5("No Zustand stores are registered (call watchStores to register them).");
2295
- }
2296
- const lines = res.stores.map((s, i) => {
2297
- const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
2298
- const body = res.includedValues ? `
2308
+ }
2309
+ if (res.stores.length === 0) {
2310
+ return text("No Zustand stores are registered (call watchStores to register them).");
2311
+ }
2312
+ const lines = res.stores.map((s, i) => {
2313
+ const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
2314
+ const body = res.includedValues ? `
2299
2315
  ${JSON.stringify(s.currentState)}` : ` \xB7 keys: ${(s.keys ?? []).join(", ") || "\u2014"}`;
2300
- return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
2301
- });
2302
- const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
2303
- return text5(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
2316
+ return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
2317
+ });
2318
+ const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
2319
+ return text(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
2304
2320
 
2305
2321
  ${lines.join("\n")}${note}`);
2306
- } catch (e) {
2307
- return fail6(errMsg6(e));
2308
- }
2309
- }
2322
+ })
2310
2323
  );
2311
2324
  server.registerTool(
2312
2325
  "zustand_action",
2313
2326
  {
2314
2327
  description: "Set the state of a Zustand store (time-travel / reset / tweak during debugging). Targets a store by name (from get_zustand_state). By default replaces the whole state; pass replace:false to merge a partial.",
2315
2328
  inputSchema: {
2316
- store: import_zod6.z.string().describe("Store name to target (from get_zustand_state)."),
2317
- state: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).describe("The new state object."),
2318
- replace: import_zod6.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
2319
- deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2329
+ store: import_zod7.z.string().describe("Store name to target (from get_zustand_state)."),
2330
+ state: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).describe("The new state object."),
2331
+ replace: import_zod7.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
2332
+ deviceId: deviceIdInput
2320
2333
  }
2321
2334
  },
2322
- async (args) => {
2323
- try {
2324
- const target = sync.requireProDevice(args.deviceId);
2325
- await sync.callAction(
2326
- ZUSTAND_TOOL_ID,
2327
- "setState",
2328
- { storeName: args.store, state: args.state, replace: args.replace ?? true },
2329
- target
2330
- );
2331
- return text5(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
2332
- } catch (e) {
2333
- return fail6(errMsg6(e));
2334
- }
2335
- }
2335
+ withDevice(sync, async (args, target) => {
2336
+ await sync.callAction(
2337
+ ZUSTAND_TOOL_ID,
2338
+ "setState",
2339
+ { storeName: args.store, state: args.state, replace: args.replace ?? true },
2340
+ target
2341
+ );
2342
+ return text(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
2343
+ })
2336
2344
  );
2337
2345
  }
2338
2346
 
2339
2347
  // src/tools/packs/jotai.ts
2340
- var import_zod7 = require("zod");
2348
+ var import_zod8 = require("zod");
2341
2349
  var JOTAI_TOOL_ID = "jotai";
2342
- var text6 = (s) => ({ content: [{ type: "text", text: s }] });
2343
- var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2344
- var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
2345
2350
  function registerJotaiTools(server, sync) {
2346
2351
  server.registerTool(
2347
2352
  "get_jotai_state",
2348
2353
  {
2349
2354
  description: "Read the app's CURRENT Jotai atom values \u2014 each watched atom with its label, change count, and whether it's writable. Compact by DEFAULT (no values) so it's token-cheap; pass includeValues:true for the actual current values. Use this for 'what are my jotai atoms / what's the current value'. (For the history of atom changes over time, use get_events with sources:['jotai'].)",
2350
2355
  inputSchema: {
2351
- includeValues: import_zod7.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
2352
- limit: import_zod7.z.number().optional().describe("First N atoms. Default all."),
2353
- deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2356
+ includeValues: import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
2357
+ limit: import_zod8.z.number().optional().describe("First N atoms. Default all."),
2358
+ deviceId: deviceIdInput
2354
2359
  }
2355
2360
  },
2356
- async (args) => {
2357
- try {
2358
- const target = sync.requireProDevice(args.deviceId);
2359
- const res = await sync.callAction(
2360
- JOTAI_TOOL_ID,
2361
- "listAtoms",
2362
- { includeValues: args.includeValues ?? false, limit: args.limit },
2363
- target
2361
+ withDevice(sync, async (args, target) => {
2362
+ const res = await sync.callAction(
2363
+ JOTAI_TOOL_ID,
2364
+ "listAtoms",
2365
+ { includeValues: args.includeValues ?? false, limit: args.limit },
2366
+ target
2367
+ );
2368
+ if (!res || !Array.isArray(res.atoms)) {
2369
+ return fail2(
2370
+ inactiveToolMessage({ toolId: JOTAI_TOOL_ID, mcpTool: "get_jotai_state", action: "listAtoms" })
2364
2371
  );
2365
- if (!res || !Array.isArray(res.atoms)) {
2366
- return fail7(
2367
- "No jotai result from the device. The jotai tool may be inactive, or the app needs a newer Buoy (get_jotai_state requires the listAtoms action)."
2368
- );
2369
- }
2370
- if (res.atoms.length === 0) {
2371
- return text6("No Jotai atoms are being watched (call watchAtoms to register them).");
2372
- }
2373
- const lines = res.atoms.map((a, i) => {
2374
- const kind = a.writable ? "writable" : "read-only";
2375
- const value = res.includedValues ? `
2372
+ }
2373
+ if (res.atoms.length === 0) {
2374
+ return text("No Jotai atoms are being watched (call watchAtoms to register them).");
2375
+ }
2376
+ const lines = res.atoms.map((a, i) => {
2377
+ const kind = a.writable ? "writable" : "read-only";
2378
+ const value = res.includedValues ? `
2376
2379
  ${JSON.stringify(a.currentValue)}` : "";
2377
- return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
2378
- });
2379
- const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
2380
- return text6(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
2380
+ return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
2381
+ });
2382
+ const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
2383
+ return text(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
2381
2384
 
2382
2385
  ${lines.join("\n")}${note}`);
2383
- } catch (e) {
2384
- return fail7(errMsg7(e));
2385
- }
2386
- }
2386
+ })
2387
2387
  );
2388
2388
  server.registerTool(
2389
2389
  "jotai_action",
2390
2390
  {
2391
2391
  description: "Set the value of a writable Jotai atom (by label, from get_jotai_state). Read-only/derived atoms cannot be set (you'll get a clear error). Use for tweaking state during debugging.",
2392
2392
  inputSchema: {
2393
- atom: import_zod7.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
2394
- value: import_zod7.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
2395
- deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2393
+ atom: import_zod8.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
2394
+ value: import_zod8.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
2395
+ deviceId: deviceIdInput
2396
2396
  }
2397
2397
  },
2398
- async (args) => {
2399
- try {
2400
- const target = sync.requireProDevice(args.deviceId);
2401
- await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
2402
- return text6(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
2403
- } catch (e) {
2404
- return fail7(errMsg7(e));
2405
- }
2406
- }
2398
+ withDevice(sync, async (args, target) => {
2399
+ await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
2400
+ return text(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
2401
+ })
2407
2402
  );
2408
2403
  }
2409
2404
 
2410
2405
  // src/tools/packs/redux.ts
2411
- var import_zod8 = require("zod");
2406
+ var import_zod9 = require("zod");
2412
2407
  var REDUX_TOOL_ID = "redux";
2413
- var text7 = (s) => ({ content: [{ type: "text", text: s }] });
2414
- var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2415
- var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
2416
2408
  function noStoreMessage(reason) {
2417
2409
  switch (reason) {
2418
2410
  case "no-react-redux":
@@ -2431,8 +2423,8 @@ function registerReduxTools(server, sync) {
2431
2423
  {
2432
2424
  description: "Read the app's CURRENT Redux state. Compact by DEFAULT \u2014 just the top-level slice names \u2014 so it's token-cheap; pass includeValues:true for the full state tree. Use this for 'what's in my redux store / current auth state'. (For the history of dispatched actions over time, use get_events with sources:['redux'].)",
2433
2425
  inputSchema: {
2434
- includeValues: import_zod8.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
2435
- deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2426
+ includeValues: import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
2427
+ deviceId: deviceIdInput
2436
2428
  }
2437
2429
  },
2438
2430
  async (args) => {
@@ -2445,15 +2437,15 @@ function registerReduxTools(server, sync) {
2445
2437
  target
2446
2438
  );
2447
2439
  if (!res) {
2448
- return fail8(
2440
+ return fail2(
2449
2441
  "No redux result from the device. The redux tool may be inactive, or the app needs a newer Buoy (get_redux_state requires the getState action)."
2450
2442
  );
2451
2443
  }
2452
- if (!res.available) return fail8(noStoreMessage(res.reason));
2444
+ if (!res.available) return fail2(noStoreMessage(res.reason));
2453
2445
  const slices = res.slices.length ? res.slices.join(", ") : "(none)";
2454
2446
  const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
2455
2447
  if (args.includeValues) {
2456
- return text7(
2448
+ return text(
2457
2449
  `**Redux store \u2014 slices: ${slices}**
2458
2450
 
2459
2451
  \`\`\`json
@@ -2461,13 +2453,13 @@ ${JSON.stringify(res.state, null, 2)}
2461
2453
  \`\`\`${captureNote}`
2462
2454
  );
2463
2455
  }
2464
- return text7(
2456
+ return text(
2465
2457
  `**Redux store \u2014 slices: ${slices}**
2466
2458
 
2467
2459
  _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
2468
2460
  );
2469
2461
  } catch (e) {
2470
- return fail8(errMsg8(e));
2462
+ return fail2(errMsg2(e));
2471
2463
  }
2472
2464
  }
2473
2465
  );
@@ -2476,9 +2468,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
2476
2468
  {
2477
2469
  description: "Dispatch a Redux action to the app's store (e.g. to trigger a state change while debugging). Provide the action type and optional payload \u2014 they're sent as { type, payload }.",
2478
2470
  inputSchema: {
2479
- type: import_zod8.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
2480
- payload: import_zod8.z.unknown().optional().describe("Optional action payload (any JSON)."),
2481
- deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2471
+ type: import_zod9.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
2472
+ payload: import_zod9.z.unknown().optional().describe("Optional action payload (any JSON)."),
2473
+ deviceId: deviceIdInput
2482
2474
  }
2483
2475
  },
2484
2476
  async (args) => {
@@ -2487,21 +2479,18 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
2487
2479
  const action = { type: args.type };
2488
2480
  if (args.payload !== void 0) action.payload = args.payload;
2489
2481
  const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
2490
- if (!res || !res.dispatched) return fail8(noStoreMessage(res?.reason));
2491
- return text7(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
2482
+ if (!res || !res.dispatched) return fail2(noStoreMessage(res?.reason));
2483
+ return text(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
2492
2484
  } catch (e) {
2493
- return fail8(errMsg8(e));
2485
+ return fail2(errMsg2(e));
2494
2486
  }
2495
2487
  }
2496
2488
  );
2497
2489
  }
2498
2490
 
2499
2491
  // src/tools/packs/storage.ts
2500
- var import_zod9 = require("zod");
2492
+ var import_zod10 = require("zod");
2501
2493
  var STORAGE_TOOL_ID = "storage";
2502
- var text8 = (s) => ({ content: [{ type: "text", text: s }] });
2503
- var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2504
- var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
2505
2494
  function truncate2(v, n = 200) {
2506
2495
  if (v === null || v === void 0) return "(null)";
2507
2496
  const s = typeof v === "string" ? v : JSON.stringify(v);
@@ -2513,10 +2502,10 @@ function registerStorageTools(server, sync) {
2513
2502
  {
2514
2503
  description: "Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Data wrong or missing after an app restart \u2192 read the persisted values here FIRST \u2014 the bug is usually a stale, missing, or malformed stored value, and reading it beats hunting for it in code. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out.",
2515
2504
  inputSchema: {
2516
- pattern: import_zod9.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
2517
- includeValues: import_zod9.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
2518
- limit: import_zod9.z.number().optional().describe("Max keys per backend section. Default 100."),
2519
- deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2505
+ pattern: import_zod10.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
2506
+ includeValues: import_zod10.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
2507
+ limit: import_zod10.z.number().optional().describe("Max keys per backend section. Default 100."),
2508
+ deviceId: deviceIdInput
2520
2509
  }
2521
2510
  },
2522
2511
  async (args) => {
@@ -2528,7 +2517,7 @@ function registerStorageTools(server, sync) {
2528
2517
  const sections = [];
2529
2518
  const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
2530
2519
  if (!Array.isArray(allKeys)) {
2531
- return fail9("No storage result from the device. The storage tool may be inactive.");
2520
+ return fail2("No storage result from the device. The storage tool may be inactive.");
2532
2521
  }
2533
2522
  const asyncKeys = allKeys.filter(matches2).slice(0, limit);
2534
2523
  if (asyncKeys.length > 0) {
@@ -2569,12 +2558,12 @@ ${lines.join("\n")}`);
2569
2558
  ${lines.join("\n")}`);
2570
2559
  }
2571
2560
  if (sections.length === 0) {
2572
- return text8(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
2561
+ return text(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
2573
2562
  }
2574
2563
  const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
2575
- return text8(`${sections.join("\n\n")}${note}`);
2564
+ return text(`${sections.join("\n\n")}${note}`);
2576
2565
  } catch (e) {
2577
- return fail9(errMsg9(e));
2566
+ return fail2(errMsg2(e));
2578
2567
  }
2579
2568
  }
2580
2569
  );
@@ -2583,10 +2572,10 @@ ${lines.join("\n")}`);
2583
2572
  {
2584
2573
  description: "Modify the app's AsyncStorage: set a key, remove a key, or clear all app storage (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",
2585
2574
  inputSchema: {
2586
- action: import_zod9.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
2587
- key: import_zod9.z.string().optional().describe("The key (required for set and remove)."),
2588
- value: import_zod9.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
2589
- deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2575
+ action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
2576
+ key: import_zod10.z.string().optional().describe("The key (required for set and remove)."),
2577
+ value: import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
2578
+ deviceId: deviceIdInput
2590
2579
  }
2591
2580
  },
2592
2581
  async (args) => {
@@ -2594,33 +2583,30 @@ ${lines.join("\n")}`);
2594
2583
  const target = sync.requireProDevice(args.deviceId);
2595
2584
  if (args.action === "clear") {
2596
2585
  await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
2597
- return text8("Cleared all app storage (Buoy's own devtool keys preserved).");
2586
+ return text("Cleared all app storage (Buoy's own devtool keys preserved).");
2598
2587
  }
2599
2588
  if (!args.key) {
2600
- return fail9(`'${args.action}' needs a key.`);
2589
+ return fail2(`'${args.action}' needs a key.`);
2601
2590
  }
2602
2591
  if (args.action === "remove") {
2603
2592
  await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
2604
- return text8(`Removed storage key "${args.key}".`);
2593
+ return text(`Removed storage key "${args.key}".`);
2605
2594
  }
2606
2595
  if (args.value === void 0) {
2607
- return fail9("'set' needs a value (a string).");
2596
+ return fail2("'set' needs a value (a string).");
2608
2597
  }
2609
2598
  await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
2610
- return text8(`Set storage key "${args.key}".`);
2599
+ return text(`Set storage key "${args.key}".`);
2611
2600
  } catch (e) {
2612
- return fail9(errMsg9(e));
2601
+ return fail2(errMsg2(e));
2613
2602
  }
2614
2603
  }
2615
2604
  );
2616
2605
  }
2617
2606
 
2618
2607
  // src/tools/packs/routes.ts
2619
- var import_zod10 = require("zod");
2608
+ var import_zod11 = require("zod");
2620
2609
  var ROUTES_TOOL_ID = "route-events";
2621
- var text9 = (s) => ({ content: [{ type: "text", text: s }] });
2622
- var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2623
- var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
2624
2610
  function flattenRoutes(routes, acc = []) {
2625
2611
  for (const r of routes ?? []) {
2626
2612
  if (r.path) acc.push(r.path);
@@ -2634,8 +2620,8 @@ function registerRoutesTools(server, sync) {
2634
2620
  {
2635
2621
  description: "Read the app's CURRENT route + navigation stack (where the user is and how they got there). Compact by DEFAULT; pass includeSitemap:true to also list every route path the app defines (useful before navigate). Use for 'what screen am I on / what's the nav stack / what routes exist'. (For navigation history over time, use get_events with sources:['route'].)",
2636
2622
  inputSchema: {
2637
- includeSitemap: import_zod10.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
2638
- deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2623
+ includeSitemap: import_zod11.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
2624
+ deviceId: deviceIdInput
2639
2625
  }
2640
2626
  },
2641
2627
  async (args) => {
@@ -2668,9 +2654,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2668
2654
  **Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
2669
2655
  }
2670
2656
  parts.push("\n_Navigate with navigate({ path })._");
2671
- return text9(parts.join("\n"));
2657
+ return text(parts.join("\n"));
2672
2658
  } catch (e) {
2673
- return fail10(errMsg10(e));
2659
+ return fail2(errMsg2(e));
2674
2660
  }
2675
2661
  }
2676
2662
  );
@@ -2679,9 +2665,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2679
2665
  {
2680
2666
  description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",
2681
2667
  inputSchema: {
2682
- path: import_zod10.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2683
- replace: import_zod10.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
2684
- deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2668
+ path: import_zod11.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2669
+ replace: import_zod11.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
2670
+ deviceId: deviceIdInput
2685
2671
  }
2686
2672
  },
2687
2673
  async (args) => {
@@ -2693,20 +2679,17 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2693
2679
  { path: args.path, replace: args.replace === true },
2694
2680
  target
2695
2681
  );
2696
- return text9(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
2682
+ return text(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
2697
2683
  } catch (e) {
2698
- return fail10(errMsg10(e));
2684
+ return fail2(errMsg2(e));
2699
2685
  }
2700
2686
  }
2701
2687
  );
2702
2688
  }
2703
2689
 
2704
2690
  // src/tools/packs/console.ts
2705
- var import_zod11 = require("zod");
2691
+ var import_zod12 = require("zod");
2706
2692
  var CONSOLE_TOOL_ID = "console";
2707
- var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2708
- var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2709
- var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
2710
2693
  var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
2711
2694
  var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
2712
2695
  function hhmmss(ts) {
@@ -2720,57 +2703,275 @@ function registerConsoleTools(server, sync) {
2720
2703
  {
2721
2704
  description: "Read the app's captured console logs (console.log/warn/error/\u2026) as a compact tail \u2014 time \xB7 level \xB7 message. App crashing or erroring \u2192 read this FIRST: it captures crashes and errors including FATAL uncaught JS errors (tagged [FATAL], hooked from RN's global error handler), so a dead app's last words are here, not in the code. Token-cheap (uses each entry's pre-rendered message, not raw args). Filter by minimum level (e.g. level:'warning' for warnings+errors) or a message substring. For full args/stack of an entry, fall back to get_snapshot('console').",
2722
2705
  inputSchema: {
2723
- level: import_zod11.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
2724
- limit: import_zod11.z.number().optional().describe("Most-recent N entries. Default 50."),
2725
- pattern: import_zod11.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
2726
- deviceId: import_zod11.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2706
+ level: import_zod12.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
2707
+ limit: import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),
2708
+ pattern: import_zod12.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
2709
+ deviceId: deviceIdInput
2710
+ }
2711
+ },
2712
+ withDevice(sync, async (args, target) => {
2713
+ const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
2714
+ const banner = staleHeader(stale);
2715
+ const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
2716
+ const all = Array.isArray(p.entries) ? p.entries : [];
2717
+ const minSev = args.level ? SEVERITY[args.level] : 0;
2718
+ const pattern = args.pattern?.toLowerCase();
2719
+ const filtered = all.filter(
2720
+ (e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
2721
+ );
2722
+ const sorted = [...filtered].sort((a, b) => a.id - b.id);
2723
+ const limit = clampLimit(args.limit, 50);
2724
+ const shown = sorted.slice(-limit);
2725
+ if (shown.length === 0) {
2726
+ return text(`${banner}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
2727
+ }
2728
+ const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
2729
+ const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
2730
+ const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
2731
+ return text(`${banner}${header}
2732
+
2733
+ \`\`\`
2734
+ ${lines.join("\n")}
2735
+ \`\`\`
2736
+
2737
+ _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
2738
+ })
2739
+ );
2740
+ }
2741
+
2742
+ // src/tools/packs/sentry.ts
2743
+ var import_zod13 = require("zod");
2744
+ var SENTRY_TOOL_ID = "sentry";
2745
+ var text2 = (s) => ({ content: [{ type: "text", text: s }] });
2746
+ var fail3 = (message) => ({
2747
+ isError: true,
2748
+ content: [{ type: "text", text: message }]
2749
+ });
2750
+ var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
2751
+ function hhmmss2(ts) {
2752
+ const d = new Date(ts);
2753
+ const p = (n) => String(n).padStart(2, "0");
2754
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
2755
+ }
2756
+ function bytes(n) {
2757
+ if (n < 1024) return `${n}B`;
2758
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
2759
+ return `${(n / (1024 * 1024)).toFixed(2)}MB`;
2760
+ }
2761
+ function units(n, format) {
2762
+ if (format === "bytes") return bytes(n);
2763
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2764
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
2765
+ return String(n);
2766
+ }
2767
+ var FREE_QUOTA = {
2768
+ errors: 5e3,
2769
+ spans: 5e6,
2770
+ replays: 50,
2771
+ logBytes: 5 * 1024 ** 3,
2772
+ attachmentBytes: 1 * 1024 ** 3
2773
+ };
2774
+ var CATEGORY_FORMAT = {
2775
+ errors: "count",
2776
+ spans: "count",
2777
+ replays: "count",
2778
+ logBytes: "bytes",
2779
+ attachmentBytes: "bytes"
2780
+ };
2781
+ function statusNote(status) {
2782
+ switch (status) {
2783
+ case "attached":
2784
+ return "";
2785
+ case "sdk-not-found":
2786
+ return "\n\n\u26A0\uFE0F The tool never found @sentry/react-native \u2014 nothing is being captured. The app must pass `getClient` to SentryRoot, or the SDK isn't installed.";
2787
+ case "no-client":
2788
+ return "\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";
2789
+ default:
2790
+ return "\n\n\u23F3 Still looking for the app's Sentry client.";
2791
+ }
2792
+ }
2793
+ function registerSentryTools(server, sync) {
2794
+ server.registerTool(
2795
+ "get_sentry_envelopes",
2796
+ {
2797
+ description: "Read what the app is actually sending to Sentry \u2014 every outgoing envelope with its item types, byte size, span count, and summary. Use this to answer 'is this error reaching Sentry', 'what does my app send at startup', or 'why is my Sentry bill high'. Debugging an error locally? These payloads are the final post-processing event, so you can read the exception, breadcrumbs and contexts here WITHOUT waiting for ingestion or spending quota. Filter by envelope item type (event / transaction / log / session / client_report / attachment / replay_event).",
2798
+ inputSchema: {
2799
+ type: import_zod13.z.string().optional().describe(
2800
+ "Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
2801
+ ),
2802
+ limit: import_zod13.z.number().optional().describe("Most-recent N envelopes. Default 25."),
2803
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2727
2804
  }
2728
2805
  },
2729
2806
  async (args) => {
2730
2807
  try {
2731
2808
  const target = sync.requireProDevice(args.deviceId);
2732
- const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
2733
- const staleHeader = stale ? `${stale}
2809
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
2810
+ const staleHeader2 = stale ? `${stale}
2734
2811
 
2735
2812
  ` : "";
2736
- const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
2737
- const all = Array.isArray(p.entries) ? p.entries : [];
2738
- const minSev = args.level ? SEVERITY[args.level] : 0;
2739
- const pattern = args.pattern?.toLowerCase();
2740
- const filtered = all.filter(
2741
- (e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
2742
- );
2743
- const sorted = [...filtered].sort((a, b) => a.id - b.id);
2744
- const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
2745
- const shown = sorted.slice(-limit);
2813
+ const p = payload ?? {};
2814
+ const all = Array.isArray(p.envelopes) ? p.envelopes : [];
2815
+ const filtered = args.type ? all.filter((e) => e.items.some((i) => i.type === args.type)) : all;
2816
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
2817
+ const shown = filtered.slice(-limit);
2746
2818
  if (shown.length === 0) {
2747
- return text10(`${staleHeader}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
2819
+ return text2(
2820
+ `${staleHeader2}No Sentry envelopes${args.type ? ` containing a '${args.type}' item` : ""} (${all.length} captured this session).${statusNote(p.status)}`
2821
+ );
2748
2822
  }
2749
- const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
2750
- const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
2751
- const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
2752
- return text10(`${staleHeader}${header}
2823
+ const lines = shown.map((e) => {
2824
+ const types = e.items.map((i) => i.type).join("+");
2825
+ const spans = e.items.reduce((sum, i) => sum + (i.spanCount ?? 0), 0);
2826
+ const spanNote = spans > 0 ? ` \xB7 ${spans} span${spans === 1 ? "" : "s"}` : "";
2827
+ const head = `${e.id}. ${hhmmss2(e.timestamp)} [${types}] ${bytes(e.totalBytes)}${spanNote}`;
2828
+ return `${head}
2829
+ \u2514\u2500 ${e.items[0]?.summary ?? "envelope"}`;
2830
+ });
2831
+ const header = `**Sentry \u2014 ${all.length} envelope${all.length === 1 ? "" : "s"} sent (showing ${shown.length}${args.type ? `, type '${args.type}'` : ""}):**`;
2832
+ return text2(
2833
+ `${staleHeader2}${header}${statusNote(p.status)}
2753
2834
 
2754
- \`\`\`
2755
2835
  ${lines.join("\n")}
2836
+
2837
+ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`
2838
+ );
2839
+ } catch (e) {
2840
+ return fail3(errMsg3(e));
2841
+ }
2842
+ }
2843
+ );
2844
+ server.registerTool(
2845
+ "get_sentry_cost",
2846
+ {
2847
+ description: "What this session would cost in Sentry's billing units, and what that rate implies for a month against the free plan. Sentry bills on RECEIPT \u2014 data its server-side sampling later discards is still charged \u2014 and spans are billed individually, so one transaction is many units. Use this before shipping a Sentry config, or when asked why a Sentry bill is high.",
2848
+ inputSchema: {
2849
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2850
+ }
2851
+ },
2852
+ async (args) => {
2853
+ try {
2854
+ const target = sync.requireProDevice(args.deviceId);
2855
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
2856
+ const staleHeader2 = stale ? `${stale}
2857
+
2858
+ ` : "";
2859
+ const p = payload ?? {};
2860
+ const usage = p.usage;
2861
+ if (!usage?.session) {
2862
+ return text2(
2863
+ `${staleHeader2}No usage recorded yet \u2014 nothing has been sent to Sentry this session.${statusNote(p.status)}`
2864
+ );
2865
+ }
2866
+ const session = usage.session;
2867
+ const projected = usage.projectedMonthly;
2868
+ const keys = Object.keys(session);
2869
+ const rows = keys.map((key) => {
2870
+ const format = CATEGORY_FORMAT[key];
2871
+ const now = units(session[key], format);
2872
+ if (!projected) return `- ${key}: ${now} this session`;
2873
+ const month = projected[key];
2874
+ const quota = FREE_QUOTA[key];
2875
+ const pct = quota > 0 ? Math.round(month / quota * 100) : null;
2876
+ const pctNote = pct === null ? " (not on the free plan)" : ` \u2014 ${pct}% of the free plan's ${units(quota, format)}`;
2877
+ return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
2878
+ });
2879
+ const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
2880
+ const contributors = usage.topContributors?.length ? `
2881
+
2882
+ **What produced it:**
2883
+ ${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
2884
+ const caveat = projected ? "\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._" : "\n\n_Session too short to project a month from._";
2885
+ return text2(
2886
+ `${staleHeader2}**Sentry cost \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
2887
+
2888
+ ${rows.join("\n")}${contributors}${caveat}`
2889
+ );
2890
+ } catch (e) {
2891
+ return fail3(errMsg3(e));
2892
+ }
2893
+ }
2894
+ );
2895
+ server.registerTool(
2896
+ "get_sentry_drops",
2897
+ {
2898
+ description: "Why events never reached Sentry. Answers 'I called captureException but the event isn't in Sentry' \u2014 the single most common Sentry problem \u2014 by naming the layer that discarded it: a beforeSend callback returning null, a sample-rate roll, dedupe or another event processor, React Native tracing discarding an empty transaction, or a native rate limit.",
2899
+ inputSchema: {
2900
+ limit: import_zod13.z.number().optional().describe("Most-recent N drop records. Default 30."),
2901
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2902
+ }
2903
+ },
2904
+ async (args) => {
2905
+ try {
2906
+ const target = sync.requireProDevice(args.deviceId);
2907
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
2908
+ const staleHeader2 = stale ? `${stale}
2909
+
2910
+ ` : "";
2911
+ const p = payload ?? {};
2912
+ const summary = p.drops?.summary ?? [];
2913
+ const recent = p.drops?.recent ?? [];
2914
+ if (summary.length === 0) {
2915
+ return text2(
2916
+ `${staleHeader2}Nothing dropped this session \u2014 every captured event reached the transport.${statusNote(p.status)}
2917
+
2918
+ _If an event is still missing from sentry.io, it left the device: check the DSN, the project, and Sentry's own inbound filters and quota._`
2919
+ );
2920
+ }
2921
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
2922
+ const shown = recent.slice(-limit);
2923
+ const summaryLines = summary.map(
2924
+ (s) => `- ${s.reason}: ${s.units} unit${s.units === 1 ? "" : "s"} across ${s.records} record${s.records === 1 ? "" : "s"}`
2925
+ );
2926
+ const rows = shown.map(
2927
+ (d) => `${hhmmss2(d.timestamp)} [${d.reason}] ${d.category}${d.quantity > 1 ? ` \xD7${d.quantity}` : ""} \u2014 ${d.detail} (${d.source})`
2928
+ );
2929
+ return text2(
2930
+ `${staleHeader2}**Sentry drops \u2014 by reason:**
2931
+
2932
+ ${summaryLines.join("\n")}
2933
+
2934
+ **Recent (${shown.length}):**
2935
+
2936
+ \`\`\`
2937
+ ${rows.join("\n")}
2756
2938
  \`\`\`
2757
2939
 
2758
- _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
2940
+ _"observed" means the tool watched it happen; "reported" means the SDK counted it afterwards. Native drops only appear when the SDK runs with debug enabled._`
2941
+ );
2942
+ } catch (e) {
2943
+ return fail3(errMsg3(e));
2944
+ }
2945
+ }
2946
+ );
2947
+ server.registerTool(
2948
+ "sentry_action",
2949
+ {
2950
+ description: "Act on the Sentry inspector. 'clear' drops every captured envelope and drop record \u2014 use it to get a clean window before driving a flow you want to measure.",
2951
+ inputSchema: {
2952
+ action: import_zod13.z.enum(["clear"]).describe("The action to perform."),
2953
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2954
+ }
2955
+ },
2956
+ async (args) => {
2957
+ try {
2958
+ const target = sync.requireProDevice(args.deviceId);
2959
+ await sync.callAction(SENTRY_TOOL_ID, "clearEnvelopes", {}, target);
2960
+ return text2(
2961
+ "Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost."
2962
+ );
2759
2963
  } catch (e) {
2760
- return fail11(errMsg11(e));
2964
+ return fail3(errMsg3(e));
2761
2965
  }
2762
2966
  }
2763
2967
  );
2764
2968
  }
2765
2969
 
2766
2970
  // src/tools/packs/timeMachine.ts
2767
- var import_zod12 = require("zod");
2971
+ var import_zod14 = require("zod");
2768
2972
  var TIME_MACHINE_TOOL_ID = "time-machine";
2769
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2770
- var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2771
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2772
- function kb(bytes) {
2773
- return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
2973
+ function kb(bytes2) {
2974
+ return bytes2 < 1024 ? `${bytes2} B` : `${(bytes2 / 1024).toFixed(1)} KB`;
2774
2975
  }
2775
2976
  function formatOutcome(outcome) {
2776
2977
  const lines = Object.entries(outcome.results).map(([sid, r2]) => {
@@ -2795,7 +2996,7 @@ function registerTimeMachineTools(server, sync) {
2795
2996
  {
2796
2997
  description: "List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore it to re-run the flow from the same state.",
2797
2998
  inputSchema: {
2798
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2999
+ deviceId: deviceIdInput
2799
3000
  }
2800
3001
  },
2801
3002
  async (args) => {
@@ -2828,9 +3029,9 @@ ${providerLines.join("\n") || "_no providers registered_"}`
2828
3029
  ];
2829
3030
  if (p.lastOutcome) parts.push(`**Last restore:**
2830
3031
  ${formatOutcome(p.lastOutcome)}`);
2831
- return text11(parts.join("\n\n"));
3032
+ return text(parts.join("\n\n"));
2832
3033
  } catch (e) {
2833
- return fail12(errMsg12(e));
3034
+ return fail2(errMsg2(e));
2834
3035
  }
2835
3036
  }
2836
3037
  );
@@ -2839,14 +3040,14 @@ ${formatOutcome(p.lastOutcome)}`);
2839
3040
  {
2840
3041
  description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, preview what restoring one would change (per-item added/removed/changed/won't-apply verdicts \u2014 check the blast radius BEFORE restoring), restore one (mode 'live' replaces state in place; 'reload' restores persisted storage then reloads the JS bundle \u2014 baselines always reload; pass restoreRoute:true to also return to the screen the snapshot was captured on), capture_baseline (an empty wipe-to-fresh-install restore point), delete, or rename. The restore result reports per-source applied/skipped counts \u2014 check them rather than assuming success.",
2841
3042
  inputSchema: {
2842
- action: import_zod12.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
2843
- id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
2844
- name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
2845
- mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
2846
- excludeKeys: import_zod12.z.array(import_zod12.z.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),
2847
- restoreRoute: import_zod12.z.boolean().optional().describe("Restore only: also navigate back to the route the snapshot was captured on (shown as `route` by get_time_machine). With mode 'reload' the navigation happens automatically on the next boot. Default false \u2014 state is restored but the app stays on the current screen."),
2848
- sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
2849
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3043
+ action: import_zod14.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
3044
+ id: import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
3045
+ name: import_zod14.z.string().optional().describe("Snapshot name (for capture and rename)."),
3046
+ mode: import_zod14.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
3047
+ excludeKeys: import_zod14.z.array(import_zod14.z.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),
3048
+ restoreRoute: import_zod14.z.boolean().optional().describe("Restore only: also navigate back to the route the snapshot was captured on (shown as `route` by get_time_machine). With mode 'reload' the navigation happens automatically on the next boot. Default false \u2014 state is restored but the app stays on the current screen."),
3049
+ sourceIds: import_zod14.z.array(import_zod14.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
3050
+ deviceId: deviceIdInput
2850
3051
  }
2851
3052
  },
2852
3053
  async (args) => {
@@ -2859,14 +3060,14 @@ ${formatOutcome(p.lastOutcome)}`);
2859
3060
  const s = result.snapshot;
2860
3061
  const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
2861
3062
  \u26A0 ${sid}: ${w}`)).join("");
2862
- return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
3063
+ return text(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
2863
3064
  }
2864
3065
  case "capture_baseline": {
2865
3066
  const result = await call("captureBaseline", {});
2866
- return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
3067
+ return text(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
2867
3068
  }
2868
3069
  case "preview": {
2869
- if (!args.id) return fail12("preview requires `id` (from get_time_machine).");
3070
+ if (!args.id) return fail2("preview requires `id` (from get_time_machine).");
2870
3071
  const result = await call("preview", { id: args.id });
2871
3072
  const lines = [];
2872
3073
  let totalApplies = 0;
@@ -2885,46 +3086,46 @@ ${formatOutcome(p.lastOutcome)}`);
2885
3086
  for (const note of src.notes) lines.push(` \xB7 ${note}`);
2886
3087
  }
2887
3088
  const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
2888
- return text11(`${head}
3089
+ return text(`${head}
2889
3090
  ${lines.join("\n")}`);
2890
3091
  }
2891
3092
  case "restore": {
2892
- if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
3093
+ if (!args.id) return fail2("restore requires `id` (from get_time_machine).");
2893
3094
  const outcome = await call("restore", {
2894
3095
  id: args.id,
2895
3096
  mode: args.mode ?? "live",
2896
3097
  excludeKeys: args.excludeKeys,
2897
3098
  restoreRoute: args.restoreRoute
2898
3099
  });
2899
- return text11(formatOutcome(outcome));
3100
+ return text(formatOutcome(outcome));
2900
3101
  }
2901
3102
  case "delete": {
2902
- if (!args.id) return fail12("delete requires `id`.");
3103
+ if (!args.id) return fail2("delete requires `id`.");
2903
3104
  await call("delete", { id: args.id });
2904
- return text11(`Deleted snapshot \`${args.id}\`.`);
3105
+ return text(`Deleted snapshot \`${args.id}\`.`);
2905
3106
  }
2906
3107
  case "rename": {
2907
- if (!args.id || !args.name) return fail12("rename requires `id` and `name`.");
3108
+ if (!args.id || !args.name) return fail2("rename requires `id` and `name`.");
2908
3109
  await call("rename", { id: args.id, name: args.name });
2909
- return text11(`Renamed \`${args.id}\` to "${args.name}".`);
3110
+ return text(`Renamed \`${args.id}\` to "${args.name}".`);
2910
3111
  }
2911
3112
  }
2912
3113
  } catch (e) {
2913
- return fail12(errMsg12(e));
3114
+ return fail2(errMsg2(e));
2914
3115
  }
2915
3116
  }
2916
3117
  );
2917
3118
  }
2918
3119
 
2919
3120
  // src/tools/packs/scenarios.ts
2920
- var import_zod13 = require("zod");
3121
+ var import_zod15 = require("zod");
2921
3122
  var SCENARIOS_TOOL_ID = "scenarios";
2922
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2923
- var fail13 = (message) => ({
3123
+ var text3 = (s) => ({ content: [{ type: "text", text: s }] });
3124
+ var fail4 = (message) => ({
2924
3125
  isError: true,
2925
3126
  content: [{ type: "text", text: message }]
2926
3127
  });
2927
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
3128
+ var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
2928
3129
  function varSpec(v) {
2929
3130
  const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
2930
3131
  const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
@@ -2986,7 +3187,7 @@ function registerScenariosTools(server, sync) {
2986
3187
  {
2987
3188
  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.",
2988
3189
  inputSchema: {
2989
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3190
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2990
3191
  }
2991
3192
  },
2992
3193
  async (args) => {
@@ -3016,9 +3217,9 @@ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing r
3016
3217
  ${drafts.map(summarize).join("\n")}`
3017
3218
  );
3018
3219
  }
3019
- return text12(parts.join("\n\n"));
3220
+ return text3(parts.join("\n\n"));
3020
3221
  } catch (e) {
3021
- return fail13(errMsg13(e));
3222
+ return fail4(errMsg4(e));
3022
3223
  }
3023
3224
  }
3024
3225
  );
@@ -3027,66 +3228,66 @@ ${drafts.map(summarize).join("\n")}`
3027
3228
  {
3028
3229
  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.`,
3029
3230
  inputSchema: {
3030
- action: import_zod13.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3031
- folder: import_zod13.z.string().optional().describe(
3231
+ action: import_zod15.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3232
+ folder: import_zod15.z.string().optional().describe(
3032
3233
  "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."
3033
3234
  ),
3034
- id: import_zod13.z.string().optional().describe(
3235
+ id: import_zod15.z.string().optional().describe(
3035
3236
  "Scenario id (required for preview/run/delete; from get_scenarios)."
3036
3237
  ),
3037
- params: import_zod13.z.record(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional().describe(
3238
+ params: import_zod15.z.record(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional().describe(
3038
3239
  "Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
3039
3240
  ),
3040
- scenario: import_zod13.z.object({
3041
- id: import_zod13.z.string().describe("Stable kebab-case id."),
3042
- name: import_zod13.z.string().describe("Human name; may contain {{variable}} placeholders."),
3043
- description: import_zod13.z.string().optional(),
3044
- expectedOutcome: import_zod13.z.string().optional().describe(
3241
+ scenario: import_zod15.z.object({
3242
+ id: import_zod15.z.string().describe("Stable kebab-case id."),
3243
+ name: import_zod15.z.string().describe("Human name; may contain {{variable}} placeholders."),
3244
+ description: import_zod15.z.string().optional(),
3245
+ expectedOutcome: import_zod15.z.string().optional().describe(
3045
3246
  "What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
3046
3247
  ),
3047
- author: import_zod13.z.string().optional(),
3048
- authoredAgainst: import_zod13.z.string().optional().describe("App version this was written against."),
3049
- folder: import_zod13.z.string().optional().describe(
3248
+ author: import_zod15.z.string().optional(),
3249
+ authoredAgainst: import_zod15.z.string().optional().describe("App version this was written against."),
3250
+ folder: import_zod15.z.string().optional().describe(
3050
3251
  'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
3051
3252
  ),
3052
- tags: import_zod13.z.array(import_zod13.z.string()).optional(),
3053
- vars: import_zod13.z.array(
3054
- import_zod13.z.object({
3055
- key: import_zod13.z.string(),
3056
- label: import_zod13.z.string().optional(),
3057
- type: import_zod13.z.enum(["string", "number", "boolean", "enum"]),
3058
- options: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional(),
3059
- default: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]).optional()
3253
+ tags: import_zod15.z.array(import_zod15.z.string()).optional(),
3254
+ vars: import_zod15.z.array(
3255
+ import_zod15.z.object({
3256
+ key: import_zod15.z.string(),
3257
+ label: import_zod15.z.string().optional(),
3258
+ type: import_zod15.z.enum(["string", "number", "boolean", "enum"]),
3259
+ options: import_zod15.z.array(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional(),
3260
+ default: import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()]).optional()
3060
3261
  })
3061
3262
  ).optional().describe(
3062
3263
  "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."
3063
3264
  ),
3064
- steps: import_zod13.z.array(
3065
- import_zod13.z.object({
3066
- id: import_zod13.z.string().optional(),
3067
- tool: import_zod13.z.string().describe(
3265
+ steps: import_zod15.z.array(
3266
+ import_zod15.z.object({
3267
+ id: import_zod15.z.string().optional(),
3268
+ tool: import_zod15.z.string().describe(
3068
3269
  "Tool id: network | storage | impersonate | route-events | query | time-machine"
3069
3270
  ),
3070
- action: import_zod13.z.string().describe(
3271
+ action: import_zod15.z.string().describe(
3071
3272
  "Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
3072
3273
  ),
3073
- params: import_zod13.z.unknown().optional(),
3074
- label: import_zod13.z.string().optional()
3274
+ params: import_zod15.z.unknown().optional(),
3275
+ label: import_zod15.z.string().optional()
3075
3276
  })
3076
3277
  ).describe("Steps, in order."),
3077
- undoSteps: import_zod13.z.array(
3078
- import_zod13.z.object({
3079
- id: import_zod13.z.string().optional(),
3080
- tool: import_zod13.z.string(),
3081
- action: import_zod13.z.string(),
3082
- params: import_zod13.z.unknown().optional(),
3083
- label: import_zod13.z.string().optional()
3278
+ undoSteps: import_zod15.z.array(
3279
+ import_zod15.z.object({
3280
+ id: import_zod15.z.string().optional(),
3281
+ tool: import_zod15.z.string(),
3282
+ action: import_zod15.z.string(),
3283
+ params: import_zod15.z.unknown().optional(),
3284
+ label: import_zod15.z.string().optional()
3084
3285
  })
3085
3286
  ).optional().describe(
3086
3287
  "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."
3087
3288
  )
3088
3289
  }).optional().describe("The scenario to save (action 'save')."),
3089
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3290
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3090
3291
  }
3091
3292
  },
3092
3293
  async (args) => {
@@ -3095,7 +3296,7 @@ ${drafts.map(summarize).join("\n")}`
3095
3296
  const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
3096
3297
  switch (args.action) {
3097
3298
  case "preview": {
3098
- if (!args.id) return fail13("preview requires `id` (from get_scenarios).");
3299
+ if (!args.id) return fail4("preview requires `id` (from get_scenarios).");
3099
3300
  const r = await call("preview", {
3100
3301
  id: args.id,
3101
3302
  params: args.params ?? {}
@@ -3118,16 +3319,16 @@ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
3118
3319
  r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
3119
3320
  ${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
3120
3321
  );
3121
- return text12(lines.join("\n\n"));
3322
+ return text3(lines.join("\n\n"));
3122
3323
  }
3123
3324
  case "run": {
3124
- if (!args.id) return fail13("run requires `id` (from get_scenarios).");
3325
+ if (!args.id) return fail4("run requires `id` (from get_scenarios).");
3125
3326
  const r = await call("run", {
3126
3327
  id: args.id,
3127
3328
  params: args.params ?? {}
3128
3329
  });
3129
3330
  if (r.preflightErrors?.length) {
3130
- return fail13(
3331
+ return fail4(
3131
3332
  `Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
3132
3333
  ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3133
3334
  );
@@ -3139,14 +3340,14 @@ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3139
3340
  return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
3140
3341
  }).join("\n");
3141
3342
  if (!r.ok) {
3142
- return fail13(
3343
+ return fail4(
3143
3344
  `Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
3144
3345
  ${steps}
3145
3346
 
3146
3347
  The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
3147
3348
  );
3148
3349
  }
3149
- return text12(
3350
+ return text3(
3150
3351
  [
3151
3352
  `\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
3152
3353
  steps,
@@ -3170,49 +3371,49 @@ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
3170
3371
  const errs = rep.errors.length ? `
3171
3372
 
3172
3373
  Errors: ${rep.errors.join("; ")}` : "";
3173
- return text12(`**Deactivated.**
3374
+ return text3(`**Deactivated.**
3174
3375
  ${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
3175
3376
  }
3176
3377
  case "save": {
3177
- if (!args.scenario) return fail13("save requires `scenario`.");
3378
+ if (!args.scenario) return fail4("save requires `scenario`.");
3178
3379
  const r = await call("save", { scenario: args.scenario });
3179
3380
  if (!r.ok) {
3180
- return fail13(
3381
+ return fail4(
3181
3382
  `Save rejected:
3182
3383
  ${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
3183
3384
  );
3184
3385
  }
3185
- return text12(
3386
+ return text3(
3186
3387
  `Saved \`${r.id}\` **as a draft**.
3187
3388
 
3188
3389
  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.`
3189
3390
  );
3190
3391
  }
3191
3392
  case "setFolder": {
3192
- if (!args.id) return fail13("setFolder requires `id` (from get_scenarios).");
3393
+ if (!args.id) return fail4("setFolder requires `id` (from get_scenarios).");
3193
3394
  const r = await call("setFolder", {
3194
3395
  id: args.id,
3195
3396
  folder: args.folder
3196
3397
  });
3197
- return text12(
3398
+ return text3(
3198
3399
  r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
3199
3400
  );
3200
3401
  }
3201
3402
  case "delete": {
3202
- if (!args.id) return fail13("delete requires `id`.");
3403
+ if (!args.id) return fail4("delete requires `id`.");
3203
3404
  await call("delete", { id: args.id });
3204
- return text12(`Deleted \`${args.id}\` from the device library.`);
3405
+ return text3(`Deleted \`${args.id}\` from the device library.`);
3205
3406
  }
3206
3407
  }
3207
3408
  } catch (e) {
3208
- return fail13(errMsg13(e));
3409
+ return fail4(errMsg4(e));
3209
3410
  }
3210
3411
  }
3211
3412
  );
3212
3413
  }
3213
3414
 
3214
3415
  // src/tools/packs/screenshot.ts
3215
- var import_zod14 = require("zod");
3416
+ var import_zod16 = require("zod");
3216
3417
 
3217
3418
  // src/capture/simctl.ts
3218
3419
  var import_node_child_process = require("child_process");
@@ -3326,8 +3527,8 @@ function frameToRect(frame, screen) {
3326
3527
  var TOOL_ID = "highlight-updates";
3327
3528
  var MARGIN_PT = 12;
3328
3529
  var CAPTURE_PADDING_PT = 6;
3329
- var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3330
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3530
+ var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3531
+ var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
3331
3532
  function reasonMessage(res, query) {
3332
3533
  switch (res.reason) {
3333
3534
  case "no-renders":
@@ -3357,10 +3558,10 @@ function registerScreenshotTools(server, sync) {
3357
3558
  {
3358
3559
  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.",
3359
3560
  inputSchema: {
3360
- query: import_zod14.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3361
- nativeTag: import_zod14.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3362
- scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3363
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3561
+ query: import_zod16.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
3562
+ nativeTag: import_zod16.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
3563
+ scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
3564
+ deviceId: deviceIdInput
3364
3565
  }
3365
3566
  },
3366
3567
  async (args) => {
@@ -3402,11 +3603,11 @@ function registerScreenshotTools(server, sync) {
3402
3603
  }
3403
3604
  }
3404
3605
  if (!located.matched || !located.rect || !located.screen) {
3405
- return fail14(reasonMessage(located, args.query) + candidatesText(located.candidates));
3606
+ return fail5(reasonMessage(located, args.query) + candidatesText(located.candidates));
3406
3607
  }
3407
3608
  const shot = await captureIosFramebuffer();
3408
3609
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
3409
- return fail14(shot.error ?? "Screenshot capture failed.");
3610
+ return fail5(shot.error ?? "Screenshot capture failed.");
3410
3611
  }
3411
3612
  const pxScale = shot.width / located.screen.width;
3412
3613
  const r = located.rect;
@@ -3422,7 +3623,7 @@ function registerScreenshotTools(server, sync) {
3422
3623
  }
3423
3624
  );
3424
3625
  if (!crop.ok || !crop.buffer) {
3425
- return fail14(crop.error ?? "Crop failed.");
3626
+ return fail5(crop.error ?? "Crop failed.");
3426
3627
  }
3427
3628
  const label = located.componentName || "component";
3428
3629
  const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
@@ -3435,14 +3636,14 @@ function registerScreenshotTools(server, sync) {
3435
3636
  ]
3436
3637
  };
3437
3638
  } catch (e) {
3438
- return fail14(errMsg14(e));
3639
+ return fail5(errMsg5(e));
3439
3640
  }
3440
3641
  }
3441
3642
  );
3442
3643
  }
3443
3644
 
3444
3645
  // src/tools/packs/camera.ts
3445
- var import_zod15 = require("zod");
3646
+ var import_zod17 = require("zod");
3446
3647
 
3447
3648
  // src/capture/camera.ts
3448
3649
  var import_node_child_process2 = require("child_process");
@@ -3598,9 +3799,9 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
3598
3799
  }
3599
3800
 
3600
3801
  // src/tools/packs/camera.ts
3601
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3602
- var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
3603
- var fail15 = (message) => ({
3802
+ var text4 = (s) => ({ content: [{ type: "text", text: s }] });
3803
+ var gate = (tool) => readEntitlement().isPro ? null : fail6(proRequired(tool));
3804
+ var fail6 = (message) => ({
3604
3805
  isError: true,
3605
3806
  content: [{ type: "text", text: message }]
3606
3807
  });
@@ -3610,11 +3811,11 @@ function registerCameraTools(server) {
3610
3811
  {
3611
3812
  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.",
3612
3813
  inputSchema: {
3613
- kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
3614
- value: import_zod15.z.string().optional().describe(
3814
+ kind: import_zod17.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
3815
+ value: import_zod17.z.string().optional().describe(
3615
3816
  "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`."
3616
3817
  ),
3617
- format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
3818
+ format: import_zod17.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
3618
3819
  }
3619
3820
  },
3620
3821
  async (args) => {
@@ -3625,12 +3826,12 @@ function registerCameraTools(server) {
3625
3826
  if (args.format) cmd.push("--format", args.format);
3626
3827
  const result = await run(cmd);
3627
3828
  if (!result.ok) {
3628
- return fail15(
3829
+ return fail6(
3629
3830
  result.error ?? result.data?.error ?? "could not set the camera source"
3630
3831
  );
3631
3832
  }
3632
3833
  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;
3633
- return text13(
3834
+ return text4(
3634
3835
  `The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
3635
3836
 
3636
3837
  _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._`
@@ -3648,15 +3849,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3648
3849
  if (blocked) return blocked;
3649
3850
  const result = await run(["status"]);
3650
3851
  if (!result.ok || !result.data) {
3651
- return fail15(result.error ?? "could not read the simulator camera status");
3852
+ return fail6(result.error ?? "could not read the simulator camera status");
3652
3853
  }
3653
3854
  const { publishing, fps, codes } = result.data;
3654
3855
  if (!publishing) {
3655
- return text13(
3856
+ return text4(
3656
3857
  "The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
3657
3858
  );
3658
3859
  }
3659
- return text13(
3860
+ return text4(
3660
3861
  [
3661
3862
  `Publishing at ${fps} fps.`,
3662
3863
  codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
@@ -3671,8 +3872,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3671
3872
  {
3672
3873
  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.",
3673
3874
  inputSchema: {
3674
- bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
3675
- udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
3875
+ bundleId: import_zod17.z.string().describe("The app's bundle identifier."),
3876
+ udid: import_zod17.z.string().optional().describe("Simulator udid, if more than one is booted.")
3676
3877
  }
3677
3878
  },
3678
3879
  async (args) => {
@@ -3682,9 +3883,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3682
3883
  if (args.udid) cmd.push("--udid", args.udid);
3683
3884
  const result = await run(cmd);
3684
3885
  if (!result.ok) {
3685
- return fail15(result.error ?? result.data?.error ?? "could not launch the app");
3886
+ return fail6(result.error ?? result.data?.error ?? "could not launch the app");
3686
3887
  }
3687
- return text13(
3888
+ return text4(
3688
3889
  `Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
3689
3890
  );
3690
3891
  }
@@ -3694,7 +3895,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3694
3895
  {
3695
3896
  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.",
3696
3897
  inputSchema: {
3697
- bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
3898
+ bundleId: import_zod17.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
3698
3899
  }
3699
3900
  },
3700
3901
  async (args) => {
@@ -3702,11 +3903,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3702
3903
  if (args.bundleId) cmd.push(args.bundleId);
3703
3904
  const result = await run(cmd);
3704
3905
  if (!result.data?.checks) {
3705
- return fail15(result.error ?? "could not run the simulator camera diagnosis");
3906
+ return fail6(result.error ?? "could not run the simulator camera diagnosis");
3706
3907
  }
3707
3908
  const ent = readEntitlement();
3708
3909
  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._";
3709
- return text13(describeDiagnosis(result.data.checks) + tierNote);
3910
+ return text4(describeDiagnosis(result.data.checks) + tierNote);
3710
3911
  }
3711
3912
  );
3712
3913
  server.registerTool(
@@ -3720,9 +3921,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3720
3921
  if (blocked) return blocked;
3721
3922
  const result = await run(["stop"]);
3722
3923
  if (!result.ok) {
3723
- return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
3924
+ return fail6(result.error ?? result.data?.error ?? "could not stop the camera");
3724
3925
  }
3725
- return text13("The simulator camera is off.");
3926
+ return text4("The simulator camera is off.");
3726
3927
  }
3727
3928
  );
3728
3929
  server.registerTool(
@@ -3730,7 +3931,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3730
3931
  {
3731
3932
  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.",
3732
3933
  inputSchema: {
3733
- udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
3934
+ udid: import_zod17.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
3734
3935
  }
3735
3936
  },
3736
3937
  async (args) => {
@@ -3738,7 +3939,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3738
3939
  if (blocked) return blocked;
3739
3940
  const devicesResult = await run(["devices"]);
3740
3941
  if (!devicesResult.ok || !devicesResult.data) {
3741
- return fail15(devicesResult.error ?? "could not list booted simulators");
3942
+ return fail6(devicesResult.error ?? "could not list booted simulators");
3742
3943
  }
3743
3944
  const devices = devicesResult.data.devices ?? [];
3744
3945
  let apps = null;
@@ -3749,7 +3950,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3749
3950
  ]);
3750
3951
  apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
3751
3952
  }
3752
- return text13(describeDevices(devices, apps));
3953
+ return text4(describeDevices(devices, apps));
3753
3954
  }
3754
3955
  );
3755
3956
  server.registerTool(
@@ -3766,11 +3967,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3766
3967
  run(["windows"])
3767
3968
  ]);
3768
3969
  if (!cams.ok && !wins.ok) {
3769
- return fail15(
3970
+ return fail6(
3770
3971
  cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
3771
3972
  );
3772
3973
  }
3773
- return text13(
3974
+ return text4(
3774
3975
  describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
3775
3976
  );
3776
3977
  }
@@ -3780,8 +3981,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3780
3981
  {
3781
3982
  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.",
3782
3983
  inputSchema: {
3783
- action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
3784
- udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
3984
+ action: import_zod17.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
3985
+ udid: import_zod17.z.string().optional().describe("Which simulator. Only needed when several are booted.")
3785
3986
  }
3786
3987
  },
3787
3988
  async (args) => {
@@ -3789,36 +3990,36 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3789
3990
  if (blocked) return blocked;
3790
3991
  const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
3791
3992
  if (!result.ok) {
3792
- return fail15(
3993
+ return fail6(
3793
3994
  result.error ?? result.data?.error ?? "could not change zero-setup mode"
3794
3995
  );
3795
3996
  }
3796
3997
  const d = result.data ?? {};
3797
3998
  if (args.action === "status") {
3798
- if (!d.enabled) return text13("Zero-setup mode is **off**.");
3799
- return text13(
3999
+ if (!d.enabled) return text4("Zero-setup mode is **off**.");
4000
+ return text4(
3800
4001
  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."
3801
4002
  );
3802
4003
  }
3803
4004
  if (args.action === "on") {
3804
- return text13(
4005
+ return text4(
3805
4006
  "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._"
3806
4007
  );
3807
4008
  }
3808
- return text13("Zero-setup mode is **off**.");
4009
+ return text4("Zero-setup mode is **off**.");
3809
4010
  }
3810
4011
  );
3811
4012
  }
3812
4013
 
3813
4014
  // src/tools/packs/interact.ts
3814
- var import_zod16 = require("zod");
4015
+ var import_zod18 = require("zod");
3815
4016
  var TOOL_ID2 = "highlight-updates";
3816
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3817
- var fail16 = (message) => ({
4017
+ var text5 = (s) => ({ content: [{ type: "text", text: s }] });
4018
+ var fail7 = (message) => ({
3818
4019
  isError: true,
3819
4020
  content: [{ type: "text", text: message }]
3820
4021
  });
3821
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
4022
+ var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
3822
4023
  function formatElement(e) {
3823
4024
  let line = e.interactive ? "\u2022 " : " ";
3824
4025
  line += e.name;
@@ -3841,8 +4042,8 @@ function registerInteractTools(server, sync) {
3841
4042
  {
3842
4043
  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.",
3843
4044
  inputSchema: {
3844
- interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3845
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4045
+ interactiveOnly: import_zod18.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
4046
+ deviceId: deviceIdInput
3846
4047
  }
3847
4048
  },
3848
4049
  async (args) => {
@@ -3852,16 +4053,16 @@ function registerInteractTools(server, sync) {
3852
4053
  const all = res?.elements ?? [];
3853
4054
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3854
4055
  if (elements.length === 0) {
3855
- return text14(
4056
+ return text5(
3856
4057
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
3857
4058
  );
3858
4059
  }
3859
4060
  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 }).`;
3860
- return text14(`${header}
4061
+ return text5(`${header}
3861
4062
 
3862
4063
  ${elements.map(formatElement).join("\n")}`);
3863
4064
  } catch (e) {
3864
- return fail16(errMsg15(e));
4065
+ return fail7(errMsg6(e));
3865
4066
  }
3866
4067
  }
3867
4068
  );
@@ -3870,20 +4071,20 @@ ${elements.map(formatElement).join("\n")}`);
3870
4071
  {
3871
4072
  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.",
3872
4073
  inputSchema: {
3873
- nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3874
- testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
3875
- query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3876
- 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)."),
3877
- text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
3878
- 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."),
3879
- scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3880
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4074
+ nativeTag: import_zod18.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
4075
+ testID: import_zod18.z.string().optional().describe("Exact testID of the element."),
4076
+ query: import_zod18.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
4077
+ value: import_zod18.z.union([import_zod18.z.number(), import_zod18.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)."),
4078
+ text: import_zod18.z.string().optional().describe("For a text input: the string to set via onChangeText."),
4079
+ longPress: import_zod18.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
4080
+ scrollIntoView: import_zod18.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
4081
+ deviceId: deviceIdInput
3881
4082
  }
3882
4083
  },
3883
4084
  async (args) => {
3884
4085
  try {
3885
4086
  if (args.nativeTag == null && !args.testID && !args.query) {
3886
- return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
4087
+ return fail7("Provide one of: nativeTag, testID, or query (from describe_screen).");
3887
4088
  }
3888
4089
  const target = sync.requireProDevice(args.deviceId);
3889
4090
  const res = await sync.callAction(
@@ -3908,26 +4109,23 @@ ${elements.map(formatElement).join("\n")}`);
3908
4109
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3909
4110
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3910
4111
  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}`;
3911
- return text14(`${verb}${scrolledNote}.${flowTip}`);
4112
+ return text5(`${verb}${scrolledNote}.${flowTip}`);
3912
4113
  }
3913
4114
  let msg = res?.reason ?? "Tap failed.";
3914
4115
  if (res?.candidates?.length) {
3915
4116
  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");
3916
4117
  }
3917
- return fail16(msg);
4118
+ return fail7(msg);
3918
4119
  } catch (e) {
3919
- return fail16(errMsg15(e));
4120
+ return fail7(errMsg6(e));
3920
4121
  }
3921
4122
  }
3922
4123
  );
3923
4124
  }
3924
4125
 
3925
4126
  // src/tools/packs/jsThread.ts
3926
- var import_zod17 = require("zod");
4127
+ var import_zod19 = require("zod");
3927
4128
  var JS_TOP_TOOL_ID = "js-top";
3928
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3929
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3930
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3931
4129
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3932
4130
  function sparkline(values) {
3933
4131
  const max = Math.max(1, ...values);
@@ -3998,9 +4196,9 @@ function registerJsThreadTools(server, sync) {
3998
4196
  {
3999
4197
  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.',
4000
4198
  inputSchema: {
4001
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4002
- durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
4003
- clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
4199
+ deviceId: deviceIdInput,
4200
+ durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
4201
+ clearFirst: import_zod19.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
4004
4202
  }
4005
4203
  },
4006
4204
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -4013,7 +4211,7 @@ function registerJsThreadTools(server, sync) {
4013
4211
  { durationMs: duration, clearFirst: clearFirst === true },
4014
4212
  target
4015
4213
  );
4016
- if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
4214
+ if (!snap) return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
4017
4215
  const md = formatTopMarkdown(snap, duration);
4018
4216
  const slim = {
4019
4217
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -4032,13 +4230,13 @@ function registerJsThreadTools(server, sync) {
4032
4230
  worstFreezeMs: r.worstFreezeMs
4033
4231
  }))
4034
4232
  };
4035
- return text15(`${md}
4233
+ return text(`${md}
4036
4234
 
4037
4235
  \`\`\`json
4038
4236
  ${JSON.stringify(slim)}
4039
4237
  \`\`\``);
4040
4238
  } catch (e) {
4041
- return fail17(errMsg16(e));
4239
+ return fail2(errMsg2(e));
4042
4240
  }
4043
4241
  }
4044
4242
  );
@@ -4047,8 +4245,8 @@ ${JSON.stringify(slim)}
4047
4245
  {
4048
4246
  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).',
4049
4247
  inputSchema: {
4050
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4051
- key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
4248
+ deviceId: deviceIdInput,
4249
+ key: import_zod19.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
4052
4250
  }
4053
4251
  },
4054
4252
  async ({ deviceId, key }) => {
@@ -4061,18 +4259,18 @@ ${JSON.stringify(slim)}
4061
4259
  target
4062
4260
  );
4063
4261
  if (!detail) {
4064
- return fail17(
4262
+ return fail2(
4065
4263
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
4066
4264
  );
4067
4265
  }
4068
4266
  const md = formatDetailMarkdown(detail);
4069
- return text15(`${md}
4267
+ return text(`${md}
4070
4268
 
4071
4269
  \`\`\`json
4072
4270
  ${JSON.stringify(detail)}
4073
4271
  \`\`\``);
4074
4272
  } catch (e) {
4075
- return fail17(errMsg16(e));
4273
+ return fail2(errMsg2(e));
4076
4274
  }
4077
4275
  }
4078
4276
  );
@@ -4081,27 +4279,24 @@ ${JSON.stringify(detail)}
4081
4279
  {
4082
4280
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
4083
4281
  inputSchema: {
4084
- deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4282
+ deviceId: deviceIdInput
4085
4283
  }
4086
4284
  },
4087
4285
  async ({ deviceId }) => {
4088
4286
  try {
4089
4287
  const target = sync.requireProDevice(deviceId);
4090
4288
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
4091
- return text15("Cleared js-top stats.");
4289
+ return text("Cleared js-top stats.");
4092
4290
  } catch (e) {
4093
- return fail17(errMsg16(e));
4291
+ return fail2(errMsg2(e));
4094
4292
  }
4095
4293
  }
4096
4294
  );
4097
4295
  }
4098
4296
 
4099
4297
  // src/tools/packs/images.ts
4100
- var import_zod18 = require("zod");
4298
+ var import_zod20 = require("zod");
4101
4299
  var IMAGES_TOOL_ID = "images";
4102
- var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4103
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4104
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
4105
4300
  function uriTail(uri) {
4106
4301
  if (!uri) return "(no source)";
4107
4302
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -4121,8 +4316,8 @@ function cacheLabel(r) {
4121
4316
  if (r.progressSeen) return "network";
4122
4317
  return "?";
4123
4318
  }
4124
- function mb(bytes) {
4125
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
4319
+ function mb(bytes2) {
4320
+ return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`;
4126
4321
  }
4127
4322
  function formatListMarkdown(list, limit) {
4128
4323
  const lines = [];
@@ -4189,9 +4384,9 @@ function registerImagesTools(server, sync) {
4189
4384
  {
4190
4385
  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.",
4191
4386
  inputSchema: {
4192
- deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4193
- limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
4194
- status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
4387
+ deviceId: deviceIdInput,
4388
+ limit: import_zod20.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
4389
+ status: import_zod20.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
4195
4390
  }
4196
4391
  },
4197
4392
  async ({ deviceId, limit, status }) => {
@@ -4204,7 +4399,7 @@ function registerImagesTools(server, sync) {
4204
4399
  { limit: capped, status },
4205
4400
  target
4206
4401
  );
4207
- if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
4402
+ if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
4208
4403
  const md = formatListMarkdown(list, capped);
4209
4404
  const slim = {
4210
4405
  stats: list.stats,
@@ -4231,13 +4426,13 @@ function registerImagesTools(server, sync) {
4231
4426
  layoutShifts: r.layoutShifts
4232
4427
  }))
4233
4428
  };
4234
- return text16(`${md}
4429
+ return text(`${md}
4235
4430
 
4236
4431
  \`\`\`json
4237
4432
  ${JSON.stringify(slim)}
4238
4433
  \`\`\``);
4239
4434
  } catch (e) {
4240
- return fail18(errMsg17(e));
4435
+ return fail2(errMsg2(e));
4241
4436
  }
4242
4437
  }
4243
4438
  );
@@ -4246,9 +4441,9 @@ ${JSON.stringify(slim)}
4246
4441
  {
4247
4442
  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.",
4248
4443
  inputSchema: {
4249
- deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4250
- id: import_zod18.z.number().describe("Record id from get_images."),
4251
- action: import_zod18.z.enum([
4444
+ deviceId: deviceIdInput,
4445
+ id: import_zod20.z.number().describe("Record id from get_images."),
4446
+ action: import_zod20.z.enum([
4252
4447
  "hard_reload",
4253
4448
  "retry",
4254
4449
  "evict_disk",
@@ -4260,7 +4455,7 @@ ${JSON.stringify(slim)}
4260
4455
  "swap_url",
4261
4456
  "restore"
4262
4457
  ]).describe("What to do."),
4263
- url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
4458
+ url: import_zod20.z.string().optional().describe("Replacement URL (swap_url only).")
4264
4459
  }
4265
4460
  },
4266
4461
  async ({ deviceId, id, action, url }) => {
@@ -4268,8 +4463,8 @@ ${JSON.stringify(slim)}
4268
4463
  const target = sync.requireProDevice(deviceId);
4269
4464
  if (action === "prove_savings") {
4270
4465
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
4271
- if (!savings) return fail18("Device returned nothing for proveSavings.");
4272
- if (!savings.ok) return fail18(savings.message);
4466
+ if (!savings) return fail2("Device returned nothing for proveSavings.");
4467
+ if (!savings.ok) return fail2(savings.message);
4273
4468
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
4274
4469
  if (savings.originalBytes != null)
4275
4470
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -4281,11 +4476,11 @@ ${JSON.stringify(slim)}
4281
4476
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
4282
4477
  if (savings.optimizedUri)
4283
4478
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
4284
- return text16(parts.join("\n"));
4479
+ return text(parts.join("\n"));
4285
4480
  }
4286
4481
  const entry = ACTION_MAP[action];
4287
- if (!entry) return fail18(`Unknown action: ${action}`);
4288
- if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
4482
+ if (!entry) return fail2(`Unknown action: ${action}`);
4483
+ if (action === "swap_url" && !url) return fail2("swap_url requires the `url` param.");
4289
4484
  const result = await sync.callAction(
4290
4485
  IMAGES_TOOL_ID,
4291
4486
  entry.action,
@@ -4305,9 +4500,9 @@ ${JSON.stringify(slim)}
4305
4500
  );
4306
4501
  if (detail.error) lines.push(`Error: ${detail.error}`);
4307
4502
  }
4308
- return text16(lines.join("\n"));
4503
+ return text(lines.join("\n"));
4309
4504
  } catch (e) {
4310
- return fail18(errMsg17(e));
4505
+ return fail2(errMsg2(e));
4311
4506
  }
4312
4507
  }
4313
4508
  );
@@ -4316,12 +4511,12 @@ ${JSON.stringify(slim)}
4316
4511
  {
4317
4512
  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.",
4318
4513
  inputSchema: {
4319
- deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4320
- network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
4321
- blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
4322
- 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."),
4323
- clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
4324
- clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
4514
+ deviceId: deviceIdInput,
4515
+ network: import_zod20.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
4516
+ blankImages: import_zod20.z.boolean().optional().describe("Render all images without a source."),
4517
+ massAction: import_zod20.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."),
4518
+ clearRegistry: import_zod20.z.boolean().optional().describe("Clear the captured image log."),
4519
+ clearExpoCaches: import_zod20.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
4325
4520
  }
4326
4521
  },
4327
4522
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -4348,17 +4543,17 @@ ${JSON.stringify(slim)}
4348
4543
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
4349
4544
  done.push(r?.message ?? "expo caches cleared");
4350
4545
  }
4351
- if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4352
- return text16(`Applied: ${done.join(" \xB7 ")}`);
4546
+ if (done.length === 0) return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4547
+ return text(`Applied: ${done.join(" \xB7 ")}`);
4353
4548
  } catch (e) {
4354
- return fail18(errMsg17(e));
4549
+ return fail2(errMsg2(e));
4355
4550
  }
4356
4551
  }
4357
4552
  );
4358
4553
  }
4359
4554
 
4360
4555
  // src/tools/packs/assets.ts
4361
- var import_zod19 = require("zod");
4556
+ var import_zod21 = require("zod");
4362
4557
 
4363
4558
  // src/tools/packs/assetsGraphNote.ts
4364
4559
  function graphUnavailableNote(ss) {
@@ -4373,14 +4568,11 @@ function graphUnavailableNote(ss) {
4373
4568
 
4374
4569
  // src/tools/packs/assets.ts
4375
4570
  var ASSETS_TOOL_ID = "assets";
4376
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4377
- var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4378
- var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
4379
- function kb2(bytes) {
4380
- if (bytes == null || bytes <= 0) return "\u2013";
4381
- if (bytes < 1024) return `${bytes}B`;
4382
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
4383
- return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
4571
+ function kb2(bytes2) {
4572
+ if (bytes2 == null || bytes2 <= 0) return "\u2013";
4573
+ if (bytes2 < 1024) return `${bytes2}B`;
4574
+ if (bytes2 < 1024 * 1024) return `${(bytes2 / 1024).toFixed(1)}KB`;
4575
+ return `${(bytes2 / (1024 * 1024)).toFixed(2)}MB`;
4384
4576
  }
4385
4577
  function sizeLabel(r) {
4386
4578
  if (r.sizeBytes == null) return "\u2013";
@@ -4463,10 +4655,10 @@ function registerAssetsTools(server, sync) {
4463
4655
  {
4464
4656
  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.",
4465
4657
  inputSchema: {
4466
- deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4467
- limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4468
- kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4469
- unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4658
+ deviceId: deviceIdInput,
4659
+ limit: import_zod21.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4660
+ kind: import_zod21.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4661
+ unusedOnly: import_zod21.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4470
4662
  }
4471
4663
  },
4472
4664
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -4479,7 +4671,7 @@ function registerAssetsTools(server, sync) {
4479
4671
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
4480
4672
  target
4481
4673
  );
4482
- if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4674
+ if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4483
4675
  if (unusedOnly) {
4484
4676
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
4485
4677
  }
@@ -4503,13 +4695,13 @@ function registerAssetsTools(server, sync) {
4503
4695
  sizeSource: r.sizeSource
4504
4696
  }))
4505
4697
  };
4506
- return text17(`${md}
4698
+ return text(`${md}
4507
4699
 
4508
4700
  \`\`\`json
4509
4701
  ${JSON.stringify(slim)}
4510
4702
  \`\`\``);
4511
4703
  } catch (e) {
4512
- return fail19(errMsg18(e));
4704
+ return fail2(errMsg2(e));
4513
4705
  }
4514
4706
  }
4515
4707
  );
@@ -4518,9 +4710,9 @@ ${JSON.stringify(slim)}
4518
4710
  {
4519
4711
  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).",
4520
4712
  inputSchema: {
4521
- deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
4522
- action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4523
- id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
4713
+ deviceId: deviceIdInput,
4714
+ action: import_zod21.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4715
+ id: import_zod21.z.number().optional().describe("Asset id from get_assets (detail only).")
4524
4716
  }
4525
4717
  },
4526
4718
  async ({ deviceId, action, id }) => {
@@ -4530,19 +4722,19 @@ ${JSON.stringify(slim)}
4530
4722
  case "rescan": {
4531
4723
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
4532
4724
  const ss = result?.scanStatus;
4533
- if (!ss) return fail19("Device returned nothing for rescan.");
4534
- return text17(
4725
+ if (!ss) return fail2("Device returned nothing for rescan.");
4726
+ return text(
4535
4727
  `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" : "")
4536
4728
  );
4537
4729
  }
4538
4730
  case "measure_sizes": {
4539
4731
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
4540
- return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4732
+ return text(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4541
4733
  }
4542
4734
  case "detail": {
4543
- if (id == null) return fail19("detail requires the `id` param (from get_assets).");
4735
+ if (id == null) return fail2("detail requires the `id` param (from get_assets).");
4544
4736
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
4545
- if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4737
+ if (!detail) return fail2(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4546
4738
  const lines = [
4547
4739
  `## ${detail.name}.${detail.type} (#${detail.id})`,
4548
4740
  "",
@@ -4550,8 +4742,8 @@ ${JSON.stringify(slim)}
4550
4742
  ];
4551
4743
  if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
4552
4744
  if (detail.sizesByScale) {
4553
- for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
4554
- lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
4745
+ for (const [scale, bytes2] of Object.entries(detail.sizesByScale)) {
4746
+ lines.push(`- Size @${scale}x: ${kb2(bytes2)}`);
4555
4747
  }
4556
4748
  }
4557
4749
  if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
@@ -4560,32 +4752,32 @@ ${JSON.stringify(slim)}
4560
4752
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
4561
4753
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
4562
4754
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
4563
- return text17(lines.join("\n"));
4755
+ return text(lines.join("\n"));
4564
4756
  }
4565
4757
  case "save_baseline": {
4566
4758
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
4567
- return text17(
4759
+ return text(
4568
4760
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
4569
4761
  );
4570
4762
  }
4571
4763
  case "clear_baseline": {
4572
4764
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
4573
- return text17("Baseline cleared.");
4765
+ return text("Baseline cleared.");
4574
4766
  }
4575
4767
  case "clear_records": {
4576
4768
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
4577
- return text17(result?.message ?? "Inventory cleared.");
4769
+ return text(result?.message ?? "Inventory cleared.");
4578
4770
  }
4579
4771
  }
4580
4772
  } catch (e) {
4581
- return fail19(errMsg18(e));
4773
+ return fail2(errMsg2(e));
4582
4774
  }
4583
4775
  }
4584
4776
  );
4585
4777
  }
4586
4778
 
4587
4779
  // src/tools/packs/reload.ts
4588
- var import_zod20 = require("zod");
4780
+ var import_zod22 = require("zod");
4589
4781
 
4590
4782
  // src/capture/devServerReload.ts
4591
4783
  var import_ws = require("ws");
@@ -4648,9 +4840,6 @@ var DEFAULT_WAIT_MS = 3e4;
4648
4840
  var TEARDOWN_GRACE_MS = 1200;
4649
4841
  var PROBE_TIMEOUT_MS = 1500;
4650
4842
  var PROBE_INTERVAL_MS = 400;
4651
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4652
- var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4653
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4654
4843
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
4655
4844
  async function ping(sync, deviceId, timeoutMs) {
4656
4845
  try {
@@ -4689,12 +4878,12 @@ function registerReloadTools(server, sync) {
4689
4878
  {
4690
4879
  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.",
4691
4880
  inputSchema: {
4692
- strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4881
+ strategy: import_zod22.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4693
4882
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
4694
4883
  ),
4695
- 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."),
4696
- timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4697
- deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4884
+ wait: import_zod22.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4885
+ timeoutMs: import_zod22.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4886
+ deviceId: import_zod22.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4698
4887
  }
4699
4888
  },
4700
4889
  async (args) => {
@@ -4736,39 +4925,39 @@ function registerReloadTools(server, sync) {
4736
4925
  }
4737
4926
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
4738
4927
  if (args.wait === false) {
4739
- return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4928
+ return text(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4740
4929
  }
4741
4930
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4742
4931
  if (elapsed == null) {
4743
- return fail20(
4932
+ return fail2(
4744
4933
  `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.`
4745
4934
  );
4746
4935
  }
4747
- return text18(
4936
+ return text(
4748
4937
  `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.`
4749
4938
  );
4750
4939
  } catch (e) {
4751
- return fail20(errMsg19(e));
4940
+ return fail2(errMsg2(e));
4752
4941
  }
4753
4942
  }
4754
4943
  );
4755
4944
  }
4756
4945
 
4757
4946
  // src/tools/packs/triage.ts
4758
- var import_zod21 = require("zod");
4759
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4760
- var fail21 = (message) => ({
4947
+ var import_zod23 = require("zod");
4948
+ var text6 = (s) => ({ content: [{ type: "text", text: s }] });
4949
+ var fail8 = (message) => ({
4761
4950
  isError: true,
4762
4951
  content: [{ type: "text", text: message }]
4763
4952
  });
4764
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4953
+ var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
4765
4954
  var PROBE_TIMEOUT_MS2 = 5e3;
4766
4955
  var MAX_STORAGE_KEYS = 20;
4767
4956
  var MAX_STORAGE_VALUE = 400;
4768
4957
  var STORAGE_VALUE_BUDGET = 1600;
4769
4958
  var MAX_LINE = 160;
4770
4959
  var cap = (s, n = MAX_LINE) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4771
- function hhmmss2(ts) {
4960
+ function hhmmss3(ts) {
4772
4961
  if (!ts) return "--:--:--";
4773
4962
  const d = new Date(ts);
4774
4963
  const p = (n) => String(n).padStart(2, "0");
@@ -4798,7 +4987,7 @@ async function section(title, run2) {
4798
4987
  return body ? `${title}
4799
4988
  ${body}` : null;
4800
4989
  } catch (e) {
4801
- const msg = errMsg20(e);
4990
+ const msg = errMsg7(e);
4802
4991
  if (/not responding|timed out|got no answer/i.test(msg)) {
4803
4992
  return `${title}
4804
4993
  (no answer \u2014 see the note above)`;
@@ -4813,7 +5002,7 @@ function registerTriageTools(server, sync) {
4813
5002
  {
4814
5003
  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.",
4815
5004
  inputSchema: {
4816
- deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
5005
+ deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
4817
5006
  }
4818
5007
  },
4819
5008
  async (args) => {
@@ -4837,7 +5026,7 @@ function registerTriageTools(server, sync) {
4837
5026
  return `(none \u2014 ${entries.length} entries captured)`;
4838
5027
  }
4839
5028
  return shown.map(
4840
- (e) => `${hhmmss2(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
5029
+ (e) => `${hhmmss3(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
4841
5030
  (e.message ?? "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).join(" \u23CE "),
4842
5031
  220
4843
5032
  )}`
@@ -4920,7 +5109,7 @@ _Values: get_storage includeValues:true_`;
4920
5109
 
4921
5110
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4922
5111
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4923
- return text19(
5112
+ return text6(
4924
5113
  `${head}${banner}
4925
5114
 
4926
5115
  ${body}
@@ -4928,22 +5117,22 @@ ${body}
4928
5117
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4929
5118
  );
4930
5119
  } catch (e) {
4931
- return fail21(errMsg20(e));
5120
+ return fail8(errMsg7(e));
4932
5121
  }
4933
5122
  }
4934
5123
  );
4935
5124
  }
4936
5125
 
4937
5126
  // src/tools/packs/flow.ts
4938
- var import_zod22 = require("zod");
5127
+ var import_zod24 = require("zod");
4939
5128
  var UI_TOOL_ID = "highlight-updates";
4940
5129
  var APP_TOOL_ID2 = "app";
4941
- var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4942
- var fail22 = (message) => ({
5130
+ var text7 = (s) => ({ content: [{ type: "text", text: s }] });
5131
+ var fail9 = (message) => ({
4943
5132
  isError: true,
4944
5133
  content: [{ type: "text", text: message }]
4945
5134
  });
4946
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
5135
+ var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
4947
5136
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4948
5137
  var POLL_MS = 250;
4949
5138
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4960,19 +5149,19 @@ function formatElement2(e) {
4960
5149
  const display = e.text ?? e.label;
4961
5150
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4962
5151
  }
4963
- var stepSchema = import_zod22.z.object({
4964
- tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4965
- longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
4966
- type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
4967
- into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4968
- 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."),
4969
- reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
4970
- waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4971
- waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4972
- waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
4973
- 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."),
4974
- 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."),
4975
- 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).")
5152
+ var stepSchema = import_zod24.z.object({
5153
+ tap: import_zod24.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5154
+ longPress: import_zod24.z.boolean().optional().describe("Make `tap` a long-press instead."),
5155
+ type: import_zod24.z.string().optional().describe("Text to type (needs `into`)."),
5156
+ into: import_zod24.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5157
+ set: import_zod24.z.union([import_zod24.z.number(), import_zod24.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
5158
+ reload: import_zod24.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
5159
+ waitMs: import_zod24.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
5160
+ waitFor: import_zod24.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
5161
+ waitGone: import_zod24.z.string().optional().describe("Wait until no element matches this (default 5s)."),
5162
+ expect: import_zod24.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
5163
+ expectGone: import_zod24.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
5164
+ timeoutMs: import_zod24.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
4976
5165
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4977
5166
  function registerFlowTools(server, sync) {
4978
5167
  server.registerTool(
@@ -4980,10 +5169,10 @@ function registerFlowTools(server, sync) {
4980
5169
  {
4981
5170
  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.",
4982
5171
  inputSchema: {
4983
- steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
4984
- 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."),
4985
- continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4986
- deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
5172
+ steps: import_zod24.z.array(stepSchema).min(1).describe("Steps to run, in order."),
5173
+ include: import_zod24.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."),
5174
+ continueOnFailure: import_zod24.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
5175
+ deviceId: deviceIdInput
4987
5176
  }
4988
5177
  },
4989
5178
  async (args) => {
@@ -5155,7 +5344,7 @@ function registerFlowTools(server, sync) {
5155
5344
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
5156
5345
  }
5157
5346
  } catch (e) {
5158
- stepErrors.push(errMsg21(e));
5347
+ stepErrors.push(errMsg8(e));
5159
5348
  }
5160
5349
  if (stepErrors.length) {
5161
5350
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -5178,7 +5367,7 @@ function registerFlowTools(server, sync) {
5178
5367
  ${shown.map(formatElement2).join("\n")}`
5179
5368
  );
5180
5369
  } catch (e) {
5181
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
5370
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(e))}`);
5182
5371
  }
5183
5372
  }
5184
5373
  if (include === "all") {
@@ -5207,9 +5396,9 @@ ${shown.map(formatElement2).join("\n")}`
5207
5396
  }
5208
5397
  }
5209
5398
  }
5210
- return text20(parts.join("\n\n"));
5399
+ return text7(parts.join("\n\n"));
5211
5400
  } catch (e) {
5212
- return fail22(errMsg21(e));
5401
+ return fail9(errMsg8(e));
5213
5402
  }
5214
5403
  }
5215
5404
  );
@@ -5230,25 +5419,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
5230
5419
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
5231
5420
 
5232
5421
  // src/tools/packs/renders.ts
5233
- var import_zod23 = require("zod");
5422
+ var import_zod25 = require("zod");
5234
5423
  var UI_TOOL_ID2 = "highlight-updates";
5235
- var text21 = (s) => ({ content: [{ type: "text", text: s }] });
5236
- var fail23 = (message) => ({
5424
+ var text8 = (s) => ({ content: [{ type: "text", text: s }] });
5425
+ var fail10 = (message) => ({
5237
5426
  isError: true,
5238
5427
  content: [{ type: "text", text: message }]
5239
5428
  });
5240
- var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
5429
+ var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
5241
5430
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
5242
5431
  var DEFAULT_IDLE_MS = 3e3;
5243
5432
  var SETTLE_MS = 400;
5244
5433
  var DEFAULT_TOP = 12;
5245
5434
  var lastCapture = /* @__PURE__ */ new Map();
5246
- var stepSchema2 = import_zod23.z.object({
5247
- tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5248
- type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
5249
- into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5250
- waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
5251
- repeat: import_zod23.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
5435
+ var stepSchema2 = import_zod25.z.object({
5436
+ tap: import_zod25.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5437
+ type: import_zod25.z.string().optional().describe("Text to type (needs `into`)."),
5438
+ into: import_zod25.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5439
+ waitMs: import_zod25.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
5440
+ repeat: import_zod25.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
5252
5441
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
5253
5442
  function selector2(needle) {
5254
5443
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -5341,14 +5530,14 @@ function registerRenderTools(server, sync) {
5341
5530
  {
5342
5531
  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.",
5343
5532
  inputSchema: {
5344
- steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
5533
+ steps: import_zod25.z.array(stepSchema2).max(20).optional().describe(
5345
5534
  "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."
5346
5535
  ),
5347
- idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5348
- label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5349
- compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5350
- top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5351
- deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
5536
+ idleMs: import_zod25.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5537
+ label: import_zod25.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5538
+ compareToPrevious: import_zod25.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5539
+ top: import_zod25.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5540
+ deviceId: deviceIdInput
5352
5541
  }
5353
5542
  },
5354
5543
  async (args) => {
@@ -5357,18 +5546,18 @@ function registerRenderTools(server, sync) {
5357
5546
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
5358
5547
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
5359
5548
  if (!uiTool) {
5360
- return fail23(
5549
+ return fail10(
5361
5550
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
5362
5551
  );
5363
5552
  }
5364
5553
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
5365
- return fail23(
5554
+ return fail10(
5366
5555
  "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."
5367
5556
  );
5368
5557
  }
5369
5558
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
5370
5559
  if (began && began.ok === false) {
5371
- return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5560
+ return fail10(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5372
5561
  }
5373
5562
  await delay5(SETTLE_MS);
5374
5563
  const startedAt = Date.now();
@@ -5413,7 +5602,7 @@ function registerRenderTools(server, sync) {
5413
5602
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
5414
5603
  const summary = ended?.summary;
5415
5604
  if (!summary) {
5416
- return fail23(
5605
+ return fail10(
5417
5606
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
5418
5607
  );
5419
5608
  }
@@ -5438,9 +5627,9 @@ function registerRenderTools(server, sync) {
5438
5627
  } else if (!args.compareToPrevious) {
5439
5628
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
5440
5629
  }
5441
- return text21(parts.join("\n"));
5630
+ return text8(parts.join("\n"));
5442
5631
  } catch (e) {
5443
- return fail23(errMsg22(e));
5632
+ return fail10(errMsg9(e));
5444
5633
  }
5445
5634
  }
5446
5635
  );
@@ -5724,6 +5913,7 @@ function createMcpServer(sync) {
5724
5913
  registerStorageTools(server, sync);
5725
5914
  registerRoutesTools(server, sync);
5726
5915
  registerConsoleTools(server, sync);
5916
+ registerSentryTools(server, sync);
5727
5917
  registerTimeMachineTools(server, sync);
5728
5918
  registerScenariosTools(server, sync);
5729
5919
  registerScreenshotTools(server, sync);
@@ -5736,7 +5926,7 @@ function createMcpServer(sync) {
5736
5926
  return server;
5737
5927
  }
5738
5928
  var deviceIdField = {
5739
- deviceId: import_zod24.z.string().optional().describe(
5929
+ deviceId: import_zod26.z.string().optional().describe(
5740
5930
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5741
5931
  )
5742
5932
  };