@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.
@@ -614,7 +614,7 @@ async function waitForBroker(url, attempts = 20) {
614
614
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
615
615
 
616
616
  // src/version.ts
617
- var VERSION = "7.0.33" ? "7.0.33" : "0.0.0-dev";
617
+ var VERSION = "7.0.35" ? "7.0.35" : "0.0.0-dev";
618
618
  var PACKAGE_NAME = "@buoy-gg/mcp";
619
619
 
620
620
  // src/update/checkForUpdate.ts
@@ -1101,7 +1101,40 @@ function round(n, d) {
1101
1101
  }
1102
1102
 
1103
1103
  // src/tools/packs/perfMonitor.ts
1104
+ import { z as z3 } from "zod";
1105
+
1106
+ // src/tools/packKit.ts
1104
1107
  import { z as z2 } from "zod";
1108
+ var text = (s) => ({
1109
+ content: [{ type: "text", text: s }]
1110
+ });
1111
+ var fail2 = (message) => ({
1112
+ isError: true,
1113
+ content: [{ type: "text", text: message }]
1114
+ });
1115
+ var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
1116
+ var deviceIdInput = z2.string().optional().describe("Target device id (auto-selected if one is connected).");
1117
+ function clampLimit(value, fallback) {
1118
+ return typeof value === "number" && value > 0 ? value : fallback;
1119
+ }
1120
+ function staleHeader(stale) {
1121
+ return stale ? `${stale}
1122
+
1123
+ ` : "";
1124
+ }
1125
+ function inactiveToolMessage(options) {
1126
+ 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).`;
1127
+ }
1128
+ function withDevice(sync, run2) {
1129
+ return async (args) => {
1130
+ try {
1131
+ const target = sync.requireProDevice(args.deviceId);
1132
+ return await run2(args, target);
1133
+ } catch (e) {
1134
+ return fail2(errMsg2(e));
1135
+ }
1136
+ };
1137
+ }
1105
1138
 
1106
1139
  // src/tools/packs/perfMonitorTypes.ts
1107
1140
  var PERF_TOOL_ID = "perf-monitor";
@@ -1110,13 +1143,10 @@ var PERF_TOOL_ID = "perf-monitor";
1110
1143
  var POLL_INTERVAL_MS = 1500;
1111
1144
  var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
1112
1145
  var json2 = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
1113
- var text = (s) => ({ content: [{ type: "text", text: s }] });
1114
- var fail2 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1115
- var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
1116
- var caseSchema = z2.object({
1117
- name: z2.string().describe("Display name for this variant (becomes the saved run's name)."),
1118
- params: z2.record(z2.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
1119
- route: z2.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
1146
+ var caseSchema = z3.object({
1147
+ name: z3.string().describe("Display name for this variant (becomes the saved run's name)."),
1148
+ params: z3.record(z3.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
1149
+ route: z3.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
1120
1150
  });
1121
1151
  async function readDeviceSettings(sync, deviceId) {
1122
1152
  try {
@@ -1235,22 +1265,22 @@ function registerPerfMonitorTools(server, sync) {
1235
1265
  {
1236
1266
  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.",
1237
1267
  inputSchema: {
1238
- targetRoute: z2.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
1239
- cases: z2.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
1240
- bounceRoute: z2.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
1241
- runsPerCase: z2.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
1242
- perCaseDurationMs: z2.number().optional().describe("How long to record each run. Default 5000."),
1243
- settleMs: z2.number().optional().describe("Idle gap after navigation before recording. Default 600."),
1244
- coolDownMs: z2.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
1245
- navTimeoutMs: z2.number().optional().describe("Max wait for navigation per run. Default 5000."),
1246
- discardWarmupRuns: z2.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
1247
- shuffleCases: z2.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
1248
- reloadBetweenCases: z2.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
1249
- captureRenders: z2.boolean().optional().describe(
1268
+ targetRoute: z3.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
1269
+ cases: z3.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
1270
+ bounceRoute: z3.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
1271
+ runsPerCase: z3.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
1272
+ perCaseDurationMs: z3.number().optional().describe("How long to record each run. Default 5000."),
1273
+ settleMs: z3.number().optional().describe("Idle gap after navigation before recording. Default 600."),
1274
+ coolDownMs: z3.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
1275
+ navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run. Default 5000."),
1276
+ discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
1277
+ shuffleCases: z3.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
1278
+ reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
1279
+ captureRenders: z3.boolean().optional().describe(
1250
1280
  "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."
1251
1281
  ),
1252
- deviceId: z2.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
1253
- maxWaitMs: z2.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
1282
+ deviceId: z3.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
1283
+ maxWaitMs: z3.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
1254
1284
  }
1255
1285
  },
1256
1286
  async (args) => {
@@ -1320,7 +1350,7 @@ ${JSON.stringify(summary, null, 2)}
1320
1350
  "get_automation_status",
1321
1351
  {
1322
1352
  description: "Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",
1323
- inputSchema: { deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected).") }
1353
+ inputSchema: { deviceId: z3.string().optional().describe("Target device id (auto-selected if one is connected).") }
1324
1354
  },
1325
1355
  async ({ deviceId }) => {
1326
1356
  try {
@@ -1341,8 +1371,8 @@ ${JSON.stringify(summary, null, 2)}
1341
1371
  {
1342
1372
  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).",
1343
1373
  inputSchema: {
1344
- deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected)."),
1345
- maxWaitMs: z2.number().optional().describe("Max time to wait. Default 600000 (10 min).")
1374
+ deviceId: deviceIdInput,
1375
+ maxWaitMs: z3.number().optional().describe("Max time to wait. Default 600000 (10 min).")
1346
1376
  }
1347
1377
  },
1348
1378
  async ({ deviceId, maxWaitMs }) => {
@@ -1415,8 +1445,8 @@ ${JSON.stringify(ranking, null, 2)}
1415
1445
  {
1416
1446
  description: "Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",
1417
1447
  inputSchema: {
1418
- batchId: z2.string().describe("The batch id to report on."),
1419
- deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected).")
1448
+ batchId: z3.string().describe("The batch id to report on."),
1449
+ deviceId: deviceIdInput
1420
1450
  }
1421
1451
  },
1422
1452
  async ({ batchId, deviceId }) => {
@@ -1443,9 +1473,9 @@ ${JSON.stringify(ranking, null, 2)}
1443
1473
  {
1444
1474
  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.",
1445
1475
  inputSchema: {
1446
- reportIdA: z2.string().describe("First (baseline) report id."),
1447
- reportIdB: z2.string().describe("Second report id to compare against the baseline."),
1448
- deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected).")
1476
+ reportIdA: z3.string().describe("First (baseline) report id."),
1477
+ reportIdB: z3.string().describe("Second report id to compare against the baseline."),
1478
+ deviceId: deviceIdInput
1449
1479
  }
1450
1480
  },
1451
1481
  async ({ reportIdA, reportIdB, deviceId }) => {
@@ -1473,8 +1503,8 @@ ${JSON.stringify(ranking, null, 2)}
1473
1503
  {
1474
1504
  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.",
1475
1505
  inputSchema: {
1476
- pages: z2.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
1477
- deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected).")
1506
+ pages: z3.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
1507
+ deviceId: deviceIdInput
1478
1508
  }
1479
1509
  },
1480
1510
  async ({ pages, deviceId }) => {
@@ -1504,16 +1534,16 @@ ${JSON.stringify(settings, null, 2)}
1504
1534
  {
1505
1535
  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.`,
1506
1536
  inputSchema: {
1507
- perCaseDurationMs: z2.number().optional().describe("Recording length per page (ms)."),
1508
- settleMs: z2.number().optional().describe("Idle after navigation before recording (ms)."),
1509
- coolDownMs: z2.number().optional().describe("Idle between runs/cases (ms)."),
1510
- navTimeoutMs: z2.number().optional().describe("Max wait for navigation per run (ms)."),
1511
- runsPerCase: z2.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
1512
- discardWarmupRuns: z2.number().optional().describe("Discard first N runs of each page before the median."),
1513
- shuffleCases: z2.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
1514
- reloadBetweenCases: z2.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
1515
- captureRenders: z2.boolean().optional().describe("Capture per-component render counts/durations during each run."),
1516
- deviceId: z2.string().optional().describe("Target device id (auto-selected if one is connected).")
1537
+ perCaseDurationMs: z3.number().optional().describe("Recording length per page (ms)."),
1538
+ settleMs: z3.number().optional().describe("Idle after navigation before recording (ms)."),
1539
+ coolDownMs: z3.number().optional().describe("Idle between runs/cases (ms)."),
1540
+ navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run (ms)."),
1541
+ runsPerCase: z3.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
1542
+ discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each page before the median."),
1543
+ shuffleCases: z3.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
1544
+ reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
1545
+ captureRenders: z3.boolean().optional().describe("Capture per-component render counts/durations during each run."),
1546
+ deviceId: deviceIdInput
1517
1547
  }
1518
1548
  },
1519
1549
  async (args) => {
@@ -1561,11 +1591,8 @@ ${JSON.stringify(saved ?? settings, null, 2)}
1561
1591
  }
1562
1592
 
1563
1593
  // src/tools/packs/events.ts
1564
- import { z as z3 } from "zod";
1594
+ import { z as z4 } from "zod";
1565
1595
  var EVENTS_TOOL_ID = "events";
1566
- var text2 = (s) => ({ content: [{ type: "text", text: s }] });
1567
- var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1568
- var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
1569
1596
  var SOURCE_ALIASES = {
1570
1597
  storage: ["storage-async", "storage-mmkv"],
1571
1598
  "react-query": ["react-query", "react-query-query", "react-query-mutation"]
@@ -1580,13 +1607,13 @@ function registerEventsTools(server, sync) {
1580
1607
  {
1581
1608
  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.",
1582
1609
  inputSchema: {
1583
- limit: z3.number().optional().describe("Most-recent N events to include. Default 25."),
1584
- sources: z3.array(z3.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
1585
- status: z3.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
1586
- includeData: z3.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
1587
- format: z3.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
1588
- preset: z3.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
1589
- deviceId: z3.string().optional().describe("Target device id (auto-selected if one is connected).")
1610
+ limit: z4.number().optional().describe("Most-recent N events to include. Default 25."),
1611
+ sources: z4.array(z4.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
1612
+ status: z4.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
1613
+ includeData: z4.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
1614
+ format: z4.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
1615
+ preset: z4.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
1616
+ deviceId: deviceIdInput
1590
1617
  }
1591
1618
  },
1592
1619
  async (args) => {
@@ -1605,7 +1632,7 @@ function registerEventsTools(server, sync) {
1605
1632
  target
1606
1633
  );
1607
1634
  if (!res || typeof res.output !== "string") {
1608
- return fail3(
1635
+ return fail2(
1609
1636
  "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)."
1610
1637
  );
1611
1638
  }
@@ -1613,26 +1640,23 @@ function registerEventsTools(server, sync) {
1613
1640
  const note = `
1614
1641
 
1615
1642
  _Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
1616
- return text2((res.output || "(no events captured)") + note);
1643
+ return text((res.output || "(no events captured)") + note);
1617
1644
  } catch (e) {
1618
- return fail3(errMsg3(e));
1645
+ return fail2(errMsg2(e));
1619
1646
  }
1620
1647
  }
1621
1648
  );
1622
1649
  }
1623
1650
 
1624
1651
  // src/tools/packs/reactQuery.ts
1625
- import { z as z4 } from "zod";
1652
+ import { z as z5 } from "zod";
1626
1653
  var RQ_TOOL_ID = "query";
1627
1654
  var RQ_TOOL_ID_LEGACY = "react-query";
1628
- var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1629
- var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1630
- var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1631
1655
  async function callQueryAction(sync, action, params, target) {
1632
1656
  try {
1633
1657
  return await sync.callAction(RQ_TOOL_ID, action, params, target);
1634
1658
  } catch (e) {
1635
- const msg = errMsg4(e);
1659
+ const msg = errMsg2(e);
1636
1660
  if (!/unknown tool/i.test(msg)) throw e;
1637
1661
  return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
1638
1662
  }
@@ -1660,10 +1684,10 @@ function registerReactQueryTools(server, sync) {
1660
1684
  {
1661
1685
  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.",
1662
1686
  inputSchema: {
1663
- includeData: z4.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
1664
- staleOnly: z4.boolean().optional().describe("Only stale queries. Default false."),
1665
- limit: z4.number().optional().describe("Most-recently-updated N queries. Default 25."),
1666
- deviceId: z4.string().optional().describe("Target device id (auto-selected if one is connected).")
1687
+ includeData: z5.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
1688
+ staleOnly: z5.boolean().optional().describe("Only stale queries. Default false."),
1689
+ limit: z5.number().optional().describe("Most-recently-updated N queries. Default 25."),
1690
+ deviceId: deviceIdInput
1667
1691
  }
1668
1692
  },
1669
1693
  async (args) => {
@@ -1676,12 +1700,12 @@ function registerReactQueryTools(server, sync) {
1676
1700
  target
1677
1701
  );
1678
1702
  if (!res || !Array.isArray(res.queries)) {
1679
- return fail4(
1703
+ return fail2(
1680
1704
  "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)."
1681
1705
  );
1682
1706
  }
1683
1707
  if (res.queries.length === 0) {
1684
- return text3(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
1708
+ return text(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
1685
1709
  }
1686
1710
  const lines = res.queries.map((q, i) => {
1687
1711
  const icon = STATUS_ICON[q.status] ?? "\u2022";
@@ -1694,11 +1718,11 @@ function registerReactQueryTools(server, sync) {
1694
1718
  const staleCount = res.queries.filter((q) => q.isStale).length;
1695
1719
  const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
1696
1720
  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._";
1697
- return text3(`${header}
1721
+ return text(`${header}
1698
1722
 
1699
1723
  ${lines.join("\n")}${note}`);
1700
1724
  } catch (e) {
1701
- return fail4(errMsg4(e));
1725
+ return fail2(errMsg2(e));
1702
1726
  }
1703
1727
  }
1704
1728
  );
@@ -1707,9 +1731,9 @@ ${lines.join("\n")}${note}`);
1707
1731
  {
1708
1732
  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.",
1709
1733
  inputSchema: {
1710
- action: z4.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
1711
- queryHash: z4.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
1712
- deviceId: z4.string().optional().describe("Target device id (auto-selected if one is connected).")
1734
+ action: z5.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
1735
+ queryHash: z5.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
1736
+ deviceId: deviceIdInput
1713
1737
  }
1714
1738
  },
1715
1739
  async (args) => {
@@ -1717,174 +1741,145 @@ ${lines.join("\n")}${note}`);
1717
1741
  const target = sync.requireProDevice(args.deviceId);
1718
1742
  if (args.action === "clear") {
1719
1743
  await callQueryAction(sync, "clearQueryCache", {}, target);
1720
- return text3("Cleared the entire React Query cache.");
1744
+ return text("Cleared the entire React Query cache.");
1721
1745
  }
1722
1746
  if (!args.queryHash) {
1723
- return fail4(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
1747
+ return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
1724
1748
  }
1725
1749
  await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
1726
- return text3(`Ran ${args.action} on query \`${args.queryHash}\`.`);
1750
+ return text(`Ran ${args.action} on query \`${args.queryHash}\`.`);
1727
1751
  } catch (e) {
1728
- return fail4(errMsg4(e));
1752
+ return fail2(errMsg2(e));
1729
1753
  }
1730
1754
  }
1731
1755
  );
1732
1756
  }
1733
1757
 
1734
1758
  // src/tools/packs/zustand.ts
1735
- import { z as z5 } from "zod";
1759
+ import { z as z6 } from "zod";
1736
1760
  var ZUSTAND_TOOL_ID = "zustand";
1737
- var text4 = (s) => ({ content: [{ type: "text", text: s }] });
1738
- var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1739
- var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
1740
1761
  function registerZustandTools(server, sync) {
1741
1762
  server.registerTool(
1742
1763
  "get_zustand_state",
1743
1764
  {
1744
1765
  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'].)",
1745
1766
  inputSchema: {
1746
- includeValues: z5.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
1747
- limit: z5.number().optional().describe("First N stores. Default all."),
1748
- deviceId: z5.string().optional().describe("Target device id (auto-selected if one is connected).")
1767
+ includeValues: z6.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
1768
+ limit: z6.number().optional().describe("First N stores. Default all."),
1769
+ deviceId: deviceIdInput
1749
1770
  }
1750
1771
  },
1751
- async (args) => {
1752
- try {
1753
- const target = sync.requireProDevice(args.deviceId);
1754
- const res = await sync.callAction(
1755
- ZUSTAND_TOOL_ID,
1756
- "listStores",
1757
- { includeValues: args.includeValues ?? false, limit: args.limit },
1758
- target
1772
+ withDevice(sync, async (args, target) => {
1773
+ const res = await sync.callAction(
1774
+ ZUSTAND_TOOL_ID,
1775
+ "listStores",
1776
+ { includeValues: args.includeValues ?? false, limit: args.limit },
1777
+ target
1778
+ );
1779
+ if (!res || !Array.isArray(res.stores)) {
1780
+ return fail2(
1781
+ inactiveToolMessage({ toolId: ZUSTAND_TOOL_ID, mcpTool: "get_zustand_state", action: "listStores" })
1759
1782
  );
1760
- if (!res || !Array.isArray(res.stores)) {
1761
- return fail5(
1762
- "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)."
1763
- );
1764
- }
1765
- if (res.stores.length === 0) {
1766
- return text4("No Zustand stores are registered (call watchStores to register them).");
1767
- }
1768
- const lines = res.stores.map((s, i) => {
1769
- const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
1770
- const body = res.includedValues ? `
1783
+ }
1784
+ if (res.stores.length === 0) {
1785
+ return text("No Zustand stores are registered (call watchStores to register them).");
1786
+ }
1787
+ const lines = res.stores.map((s, i) => {
1788
+ const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
1789
+ const body = res.includedValues ? `
1771
1790
  ${JSON.stringify(s.currentState)}` : ` \xB7 keys: ${(s.keys ?? []).join(", ") || "\u2014"}`;
1772
- return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
1773
- });
1774
- const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
1775
- return text4(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
1791
+ return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
1792
+ });
1793
+ const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
1794
+ return text(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
1776
1795
 
1777
1796
  ${lines.join("\n")}${note}`);
1778
- } catch (e) {
1779
- return fail5(errMsg5(e));
1780
- }
1781
- }
1797
+ })
1782
1798
  );
1783
1799
  server.registerTool(
1784
1800
  "zustand_action",
1785
1801
  {
1786
1802
  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.",
1787
1803
  inputSchema: {
1788
- store: z5.string().describe("Store name to target (from get_zustand_state)."),
1789
- state: z5.record(z5.string(), z5.unknown()).describe("The new state object."),
1790
- replace: z5.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
1791
- deviceId: z5.string().optional().describe("Target device id (auto-selected if one is connected).")
1804
+ store: z6.string().describe("Store name to target (from get_zustand_state)."),
1805
+ state: z6.record(z6.string(), z6.unknown()).describe("The new state object."),
1806
+ replace: z6.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
1807
+ deviceId: deviceIdInput
1792
1808
  }
1793
1809
  },
1794
- async (args) => {
1795
- try {
1796
- const target = sync.requireProDevice(args.deviceId);
1797
- await sync.callAction(
1798
- ZUSTAND_TOOL_ID,
1799
- "setState",
1800
- { storeName: args.store, state: args.state, replace: args.replace ?? true },
1801
- target
1802
- );
1803
- return text4(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
1804
- } catch (e) {
1805
- return fail5(errMsg5(e));
1806
- }
1807
- }
1810
+ withDevice(sync, async (args, target) => {
1811
+ await sync.callAction(
1812
+ ZUSTAND_TOOL_ID,
1813
+ "setState",
1814
+ { storeName: args.store, state: args.state, replace: args.replace ?? true },
1815
+ target
1816
+ );
1817
+ return text(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
1818
+ })
1808
1819
  );
1809
1820
  }
1810
1821
 
1811
1822
  // src/tools/packs/jotai.ts
1812
- import { z as z6 } from "zod";
1823
+ import { z as z7 } from "zod";
1813
1824
  var JOTAI_TOOL_ID = "jotai";
1814
- var text5 = (s) => ({ content: [{ type: "text", text: s }] });
1815
- var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1816
- var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
1817
1825
  function registerJotaiTools(server, sync) {
1818
1826
  server.registerTool(
1819
1827
  "get_jotai_state",
1820
1828
  {
1821
1829
  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'].)",
1822
1830
  inputSchema: {
1823
- includeValues: z6.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
1824
- limit: z6.number().optional().describe("First N atoms. Default all."),
1825
- deviceId: z6.string().optional().describe("Target device id (auto-selected if one is connected).")
1831
+ includeValues: z7.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
1832
+ limit: z7.number().optional().describe("First N atoms. Default all."),
1833
+ deviceId: deviceIdInput
1826
1834
  }
1827
1835
  },
1828
- async (args) => {
1829
- try {
1830
- const target = sync.requireProDevice(args.deviceId);
1831
- const res = await sync.callAction(
1832
- JOTAI_TOOL_ID,
1833
- "listAtoms",
1834
- { includeValues: args.includeValues ?? false, limit: args.limit },
1835
- target
1836
+ withDevice(sync, async (args, target) => {
1837
+ const res = await sync.callAction(
1838
+ JOTAI_TOOL_ID,
1839
+ "listAtoms",
1840
+ { includeValues: args.includeValues ?? false, limit: args.limit },
1841
+ target
1842
+ );
1843
+ if (!res || !Array.isArray(res.atoms)) {
1844
+ return fail2(
1845
+ inactiveToolMessage({ toolId: JOTAI_TOOL_ID, mcpTool: "get_jotai_state", action: "listAtoms" })
1836
1846
  );
1837
- if (!res || !Array.isArray(res.atoms)) {
1838
- return fail6(
1839
- "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)."
1840
- );
1841
- }
1842
- if (res.atoms.length === 0) {
1843
- return text5("No Jotai atoms are being watched (call watchAtoms to register them).");
1844
- }
1845
- const lines = res.atoms.map((a, i) => {
1846
- const kind = a.writable ? "writable" : "read-only";
1847
- const value = res.includedValues ? `
1847
+ }
1848
+ if (res.atoms.length === 0) {
1849
+ return text("No Jotai atoms are being watched (call watchAtoms to register them).");
1850
+ }
1851
+ const lines = res.atoms.map((a, i) => {
1852
+ const kind = a.writable ? "writable" : "read-only";
1853
+ const value = res.includedValues ? `
1848
1854
  ${JSON.stringify(a.currentValue)}` : "";
1849
- return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
1850
- });
1851
- const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
1852
- return text5(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
1855
+ return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
1856
+ });
1857
+ const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
1858
+ return text(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
1853
1859
 
1854
1860
  ${lines.join("\n")}${note}`);
1855
- } catch (e) {
1856
- return fail6(errMsg6(e));
1857
- }
1858
- }
1861
+ })
1859
1862
  );
1860
1863
  server.registerTool(
1861
1864
  "jotai_action",
1862
1865
  {
1863
1866
  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.",
1864
1867
  inputSchema: {
1865
- atom: z6.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
1866
- value: z6.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
1867
- deviceId: z6.string().optional().describe("Target device id (auto-selected if one is connected).")
1868
+ atom: z7.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
1869
+ value: z7.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
1870
+ deviceId: deviceIdInput
1868
1871
  }
1869
1872
  },
1870
- async (args) => {
1871
- try {
1872
- const target = sync.requireProDevice(args.deviceId);
1873
- await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
1874
- return text5(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
1875
- } catch (e) {
1876
- return fail6(errMsg6(e));
1877
- }
1878
- }
1873
+ withDevice(sync, async (args, target) => {
1874
+ await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
1875
+ return text(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
1876
+ })
1879
1877
  );
1880
1878
  }
1881
1879
 
1882
1880
  // src/tools/packs/redux.ts
1883
- import { z as z7 } from "zod";
1881
+ import { z as z8 } from "zod";
1884
1882
  var REDUX_TOOL_ID = "redux";
1885
- var text6 = (s) => ({ content: [{ type: "text", text: s }] });
1886
- var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1887
- var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
1888
1883
  function noStoreMessage(reason) {
1889
1884
  switch (reason) {
1890
1885
  case "no-react-redux":
@@ -1903,8 +1898,8 @@ function registerReduxTools(server, sync) {
1903
1898
  {
1904
1899
  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'].)",
1905
1900
  inputSchema: {
1906
- includeValues: z7.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
1907
- deviceId: z7.string().optional().describe("Target device id (auto-selected if one is connected).")
1901
+ includeValues: z8.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
1902
+ deviceId: deviceIdInput
1908
1903
  }
1909
1904
  },
1910
1905
  async (args) => {
@@ -1917,15 +1912,15 @@ function registerReduxTools(server, sync) {
1917
1912
  target
1918
1913
  );
1919
1914
  if (!res) {
1920
- return fail7(
1915
+ return fail2(
1921
1916
  "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)."
1922
1917
  );
1923
1918
  }
1924
- if (!res.available) return fail7(noStoreMessage(res.reason));
1919
+ if (!res.available) return fail2(noStoreMessage(res.reason));
1925
1920
  const slices = res.slices.length ? res.slices.join(", ") : "(none)";
1926
1921
  const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
1927
1922
  if (args.includeValues) {
1928
- return text6(
1923
+ return text(
1929
1924
  `**Redux store \u2014 slices: ${slices}**
1930
1925
 
1931
1926
  \`\`\`json
@@ -1933,13 +1928,13 @@ ${JSON.stringify(res.state, null, 2)}
1933
1928
  \`\`\`${captureNote}`
1934
1929
  );
1935
1930
  }
1936
- return text6(
1931
+ return text(
1937
1932
  `**Redux store \u2014 slices: ${slices}**
1938
1933
 
1939
1934
  _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
1940
1935
  );
1941
1936
  } catch (e) {
1942
- return fail7(errMsg7(e));
1937
+ return fail2(errMsg2(e));
1943
1938
  }
1944
1939
  }
1945
1940
  );
@@ -1948,9 +1943,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
1948
1943
  {
1949
1944
  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 }.",
1950
1945
  inputSchema: {
1951
- type: z7.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
1952
- payload: z7.unknown().optional().describe("Optional action payload (any JSON)."),
1953
- deviceId: z7.string().optional().describe("Target device id (auto-selected if one is connected).")
1946
+ type: z8.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
1947
+ payload: z8.unknown().optional().describe("Optional action payload (any JSON)."),
1948
+ deviceId: deviceIdInput
1954
1949
  }
1955
1950
  },
1956
1951
  async (args) => {
@@ -1959,21 +1954,18 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
1959
1954
  const action = { type: args.type };
1960
1955
  if (args.payload !== void 0) action.payload = args.payload;
1961
1956
  const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
1962
- if (!res || !res.dispatched) return fail7(noStoreMessage(res?.reason));
1963
- return text6(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
1957
+ if (!res || !res.dispatched) return fail2(noStoreMessage(res?.reason));
1958
+ return text(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
1964
1959
  } catch (e) {
1965
- return fail7(errMsg7(e));
1960
+ return fail2(errMsg2(e));
1966
1961
  }
1967
1962
  }
1968
1963
  );
1969
1964
  }
1970
1965
 
1971
1966
  // src/tools/packs/storage.ts
1972
- import { z as z8 } from "zod";
1967
+ import { z as z9 } from "zod";
1973
1968
  var STORAGE_TOOL_ID = "storage";
1974
- var text7 = (s) => ({ content: [{ type: "text", text: s }] });
1975
- var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1976
- var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
1977
1969
  function truncate(v, n = 200) {
1978
1970
  if (v === null || v === void 0) return "(null)";
1979
1971
  const s = typeof v === "string" ? v : JSON.stringify(v);
@@ -1985,10 +1977,10 @@ function registerStorageTools(server, sync) {
1985
1977
  {
1986
1978
  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.",
1987
1979
  inputSchema: {
1988
- pattern: z8.string().optional().describe("Only keys containing this substring (case-insensitive)."),
1989
- includeValues: z8.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
1990
- limit: z8.number().optional().describe("Max keys per backend section. Default 100."),
1991
- deviceId: z8.string().optional().describe("Target device id (auto-selected if one is connected).")
1980
+ pattern: z9.string().optional().describe("Only keys containing this substring (case-insensitive)."),
1981
+ includeValues: z9.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
1982
+ limit: z9.number().optional().describe("Max keys per backend section. Default 100."),
1983
+ deviceId: deviceIdInput
1992
1984
  }
1993
1985
  },
1994
1986
  async (args) => {
@@ -2000,7 +1992,7 @@ function registerStorageTools(server, sync) {
2000
1992
  const sections = [];
2001
1993
  const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
2002
1994
  if (!Array.isArray(allKeys)) {
2003
- return fail8("No storage result from the device. The storage tool may be inactive.");
1995
+ return fail2("No storage result from the device. The storage tool may be inactive.");
2004
1996
  }
2005
1997
  const asyncKeys = allKeys.filter(matches2).slice(0, limit);
2006
1998
  if (asyncKeys.length > 0) {
@@ -2041,12 +2033,12 @@ ${lines.join("\n")}`);
2041
2033
  ${lines.join("\n")}`);
2042
2034
  }
2043
2035
  if (sections.length === 0) {
2044
- return text7(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
2036
+ return text(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
2045
2037
  }
2046
2038
  const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
2047
- return text7(`${sections.join("\n\n")}${note}`);
2039
+ return text(`${sections.join("\n\n")}${note}`);
2048
2040
  } catch (e) {
2049
- return fail8(errMsg8(e));
2041
+ return fail2(errMsg2(e));
2050
2042
  }
2051
2043
  }
2052
2044
  );
@@ -2055,10 +2047,10 @@ ${lines.join("\n")}`);
2055
2047
  {
2056
2048
  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.",
2057
2049
  inputSchema: {
2058
- action: z8.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
2059
- key: z8.string().optional().describe("The key (required for set and remove)."),
2060
- value: z8.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
2061
- deviceId: z8.string().optional().describe("Target device id (auto-selected if one is connected).")
2050
+ action: z9.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
2051
+ key: z9.string().optional().describe("The key (required for set and remove)."),
2052
+ value: z9.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
2053
+ deviceId: deviceIdInput
2062
2054
  }
2063
2055
  },
2064
2056
  async (args) => {
@@ -2066,33 +2058,30 @@ ${lines.join("\n")}`);
2066
2058
  const target = sync.requireProDevice(args.deviceId);
2067
2059
  if (args.action === "clear") {
2068
2060
  await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
2069
- return text7("Cleared all app storage (Buoy's own devtool keys preserved).");
2061
+ return text("Cleared all app storage (Buoy's own devtool keys preserved).");
2070
2062
  }
2071
2063
  if (!args.key) {
2072
- return fail8(`'${args.action}' needs a key.`);
2064
+ return fail2(`'${args.action}' needs a key.`);
2073
2065
  }
2074
2066
  if (args.action === "remove") {
2075
2067
  await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
2076
- return text7(`Removed storage key "${args.key}".`);
2068
+ return text(`Removed storage key "${args.key}".`);
2077
2069
  }
2078
2070
  if (args.value === void 0) {
2079
- return fail8("'set' needs a value (a string).");
2071
+ return fail2("'set' needs a value (a string).");
2080
2072
  }
2081
2073
  await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
2082
- return text7(`Set storage key "${args.key}".`);
2074
+ return text(`Set storage key "${args.key}".`);
2083
2075
  } catch (e) {
2084
- return fail8(errMsg8(e));
2076
+ return fail2(errMsg2(e));
2085
2077
  }
2086
2078
  }
2087
2079
  );
2088
2080
  }
2089
2081
 
2090
2082
  // src/tools/packs/routes.ts
2091
- import { z as z9 } from "zod";
2083
+ import { z as z10 } from "zod";
2092
2084
  var ROUTES_TOOL_ID = "route-events";
2093
- var text8 = (s) => ({ content: [{ type: "text", text: s }] });
2094
- var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2095
- var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
2096
2085
  function flattenRoutes(routes, acc = []) {
2097
2086
  for (const r of routes ?? []) {
2098
2087
  if (r.path) acc.push(r.path);
@@ -2106,8 +2095,8 @@ function registerRoutesTools(server, sync) {
2106
2095
  {
2107
2096
  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'].)",
2108
2097
  inputSchema: {
2109
- includeSitemap: z9.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
2110
- deviceId: z9.string().optional().describe("Target device id (auto-selected if one is connected).")
2098
+ includeSitemap: z10.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
2099
+ deviceId: deviceIdInput
2111
2100
  }
2112
2101
  },
2113
2102
  async (args) => {
@@ -2140,9 +2129,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2140
2129
  **Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
2141
2130
  }
2142
2131
  parts.push("\n_Navigate with navigate({ path })._");
2143
- return text8(parts.join("\n"));
2132
+ return text(parts.join("\n"));
2144
2133
  } catch (e) {
2145
- return fail9(errMsg9(e));
2134
+ return fail2(errMsg2(e));
2146
2135
  }
2147
2136
  }
2148
2137
  );
@@ -2151,9 +2140,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2151
2140
  {
2152
2141
  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 '/').",
2153
2142
  inputSchema: {
2154
- path: z9.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2155
- replace: z9.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
2156
- deviceId: z9.string().optional().describe("Target device id (auto-selected if one is connected).")
2143
+ path: z10.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2144
+ replace: z10.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
2145
+ deviceId: deviceIdInput
2157
2146
  }
2158
2147
  },
2159
2148
  async (args) => {
@@ -2165,20 +2154,17 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2165
2154
  { path: args.path, replace: args.replace === true },
2166
2155
  target
2167
2156
  );
2168
- return text8(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
2157
+ return text(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
2169
2158
  } catch (e) {
2170
- return fail9(errMsg9(e));
2159
+ return fail2(errMsg2(e));
2171
2160
  }
2172
2161
  }
2173
2162
  );
2174
2163
  }
2175
2164
 
2176
2165
  // src/tools/packs/console.ts
2177
- import { z as z10 } from "zod";
2166
+ import { z as z11 } from "zod";
2178
2167
  var CONSOLE_TOOL_ID = "console";
2179
- var text9 = (s) => ({ content: [{ type: "text", text: s }] });
2180
- var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2181
- var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
2182
2168
  var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
2183
2169
  var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
2184
2170
  function hhmmss(ts) {
@@ -2192,51 +2178,44 @@ function registerConsoleTools(server, sync) {
2192
2178
  {
2193
2179
  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').",
2194
2180
  inputSchema: {
2195
- level: z10.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
2196
- limit: z10.number().optional().describe("Most-recent N entries. Default 50."),
2197
- pattern: z10.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
2198
- deviceId: z10.string().optional().describe("Target device id (auto-selected if one is connected).")
2181
+ level: z11.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
2182
+ limit: z11.number().optional().describe("Most-recent N entries. Default 50."),
2183
+ pattern: z11.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
2184
+ deviceId: deviceIdInput
2199
2185
  }
2200
2186
  },
2201
- async (args) => {
2202
- try {
2203
- const target = sync.requireProDevice(args.deviceId);
2204
- const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
2205
- const staleHeader = stale ? `${stale}
2206
-
2207
- ` : "";
2208
- const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
2209
- const all = Array.isArray(p.entries) ? p.entries : [];
2210
- const minSev = args.level ? SEVERITY[args.level] : 0;
2211
- const pattern = args.pattern?.toLowerCase();
2212
- const filtered = all.filter(
2213
- (e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
2214
- );
2215
- const sorted = [...filtered].sort((a, b) => a.id - b.id);
2216
- const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
2217
- const shown = sorted.slice(-limit);
2218
- if (shown.length === 0) {
2219
- return text9(`${staleHeader}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
2220
- }
2221
- const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
2222
- const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
2223
- const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
2224
- return text9(`${staleHeader}${header}
2187
+ withDevice(sync, async (args, target) => {
2188
+ const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
2189
+ const banner = staleHeader(stale);
2190
+ const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
2191
+ const all = Array.isArray(p.entries) ? p.entries : [];
2192
+ const minSev = args.level ? SEVERITY[args.level] : 0;
2193
+ const pattern = args.pattern?.toLowerCase();
2194
+ const filtered = all.filter(
2195
+ (e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
2196
+ );
2197
+ const sorted = [...filtered].sort((a, b) => a.id - b.id);
2198
+ const limit = clampLimit(args.limit, 50);
2199
+ const shown = sorted.slice(-limit);
2200
+ if (shown.length === 0) {
2201
+ return text(`${banner}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
2202
+ }
2203
+ const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
2204
+ const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
2205
+ const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
2206
+ return text(`${banner}${header}
2225
2207
 
2226
2208
  \`\`\`
2227
2209
  ${lines.join("\n")}
2228
2210
  \`\`\`
2229
2211
 
2230
2212
  _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
2231
- } catch (e) {
2232
- return fail10(errMsg10(e));
2233
- }
2234
- }
2213
+ })
2235
2214
  );
2236
2215
  }
2237
2216
 
2238
2217
  // src/tools/packs/screenshot.ts
2239
- import { z as z11 } from "zod";
2218
+ import { z as z12 } from "zod";
2240
2219
 
2241
2220
  // src/capture/simctl.ts
2242
2221
  import { execFile } from "child_process";
@@ -2350,8 +2329,8 @@ function frameToRect(frame, screen) {
2350
2329
  var TOOL_ID = "highlight-updates";
2351
2330
  var MARGIN_PT = 12;
2352
2331
  var CAPTURE_PADDING_PT = 6;
2353
- var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2354
- var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
2332
+ var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2333
+ var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
2355
2334
  function reasonMessage(res, query) {
2356
2335
  switch (res.reason) {
2357
2336
  case "no-renders":
@@ -2381,10 +2360,10 @@ function registerScreenshotTools(server, sync) {
2381
2360
  {
2382
2361
  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.",
2383
2362
  inputSchema: {
2384
- query: z11.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2385
- nativeTag: z11.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2386
- scrollIntoView: z11.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2387
- deviceId: z11.string().optional().describe("Target device id (auto-selected if one is connected).")
2363
+ query: z12.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2364
+ nativeTag: z12.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2365
+ scrollIntoView: z12.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2366
+ deviceId: deviceIdInput
2388
2367
  }
2389
2368
  },
2390
2369
  async (args) => {
@@ -2426,11 +2405,11 @@ function registerScreenshotTools(server, sync) {
2426
2405
  }
2427
2406
  }
2428
2407
  if (!located.matched || !located.rect || !located.screen) {
2429
- return fail11(reasonMessage(located, args.query) + candidatesText(located.candidates));
2408
+ return fail3(reasonMessage(located, args.query) + candidatesText(located.candidates));
2430
2409
  }
2431
2410
  const shot = await captureIosFramebuffer();
2432
2411
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
2433
- return fail11(shot.error ?? "Screenshot capture failed.");
2412
+ return fail3(shot.error ?? "Screenshot capture failed.");
2434
2413
  }
2435
2414
  const pxScale = shot.width / located.screen.width;
2436
2415
  const r = located.rect;
@@ -2446,7 +2425,7 @@ function registerScreenshotTools(server, sync) {
2446
2425
  }
2447
2426
  );
2448
2427
  if (!crop.ok || !crop.buffer) {
2449
- return fail11(crop.error ?? "Crop failed.");
2428
+ return fail3(crop.error ?? "Crop failed.");
2450
2429
  }
2451
2430
  const label = located.componentName || "component";
2452
2431
  const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
@@ -2459,7 +2438,7 @@ function registerScreenshotTools(server, sync) {
2459
2438
  ]
2460
2439
  };
2461
2440
  } catch (e) {
2462
- return fail11(errMsg11(e));
2441
+ return fail3(errMsg3(e));
2463
2442
  }
2464
2443
  }
2465
2444
  );
@@ -2467,14 +2446,11 @@ function registerScreenshotTools(server, sync) {
2467
2446
 
2468
2447
  // src/server.ts
2469
2448
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2470
- import { z as z24 } from "zod";
2449
+ import { z as z26 } from "zod";
2471
2450
 
2472
2451
  // src/tools/packs/network.ts
2473
- import { z as z12 } from "zod";
2452
+ import { z as z13 } from "zod";
2474
2453
  var NETWORK_TOOL_ID = "network";
2475
- var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2476
- var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2477
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2478
2454
  var masterNote = (state) => {
2479
2455
  if (state?.enabled !== false) return "";
2480
2456
  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";
@@ -2600,12 +2576,12 @@ function registerNetworkTools(server, sync) {
2600
2576
  {
2601
2577
  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).",
2602
2578
  inputSchema: {
2603
- limit: z12.number().optional().describe("Most-recent N requests. Default 25."),
2604
- pattern: z12.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
2605
- status: z12.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
2606
- flagged: z12.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."),
2607
- includeBodies: z12.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
2608
- deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2579
+ limit: z13.number().optional().describe("Most-recent N requests. Default 25."),
2580
+ pattern: z13.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
2581
+ status: z13.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
2582
+ flagged: z13.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."),
2583
+ includeBodies: z13.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
2584
+ deviceId: deviceIdInput
2609
2585
  }
2610
2586
  },
2611
2587
  async (args) => {
@@ -2629,7 +2605,7 @@ function registerNetworkTools(server, sync) {
2629
2605
  );
2630
2606
  const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
2631
2607
  if (rows2.length === 0) {
2632
- return text10(
2608
+ return text(
2633
2609
  `No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
2634
2610
 
2635
2611
  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.`
@@ -2646,7 +2622,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
2646
2622
  if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
2647
2623
  return out;
2648
2624
  }).join("\n");
2649
- return text10(
2625
+ return text(
2650
2626
  `**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
2651
2627
 
2652
2628
  ${body2}
@@ -2657,7 +2633,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
2657
2633
  const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
2658
2634
  if (rows.length === 0) {
2659
2635
  const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
2660
- return text10(`No requests on ${target}. ${why}`);
2636
+ return text(`No requests on ${target}. ${why}`);
2661
2637
  }
2662
2638
  const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
2663
2639
  const body = rows.map((e, i) => {
@@ -2667,7 +2643,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
2667
2643
  return out;
2668
2644
  }).join("\n");
2669
2645
  const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
2670
- return text10(
2646
+ return text(
2671
2647
  `**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
2672
2648
 
2673
2649
  ${body}
@@ -2675,7 +2651,7 @@ ${body}
2675
2651
  _\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)}`
2676
2652
  );
2677
2653
  } catch (e) {
2678
- return fail12(errMsg12(e));
2654
+ return fail2(errMsg2(e));
2679
2655
  }
2680
2656
  }
2681
2657
  );
@@ -2684,10 +2660,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2684
2660
  {
2685
2661
  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.",
2686
2662
  inputSchema: {
2687
- action: z12.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)."),
2688
- id: z12.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
2689
- key: z12.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
2690
- deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2663
+ action: z13.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)."),
2664
+ id: z13.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
2665
+ key: z13.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
2666
+ deviceId: deviceIdInput
2691
2667
  }
2692
2668
  },
2693
2669
  async (args) => {
@@ -2695,10 +2671,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2695
2671
  const target = sync.requireProDevice(args.deviceId);
2696
2672
  const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
2697
2673
  if (needsId && !args.id) {
2698
- return fail12(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
2674
+ return fail2(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
2699
2675
  }
2700
2676
  if (args.action === "remove" && !args.key) {
2701
- return fail12('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
2677
+ return fail2('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
2702
2678
  }
2703
2679
  switch (args.action) {
2704
2680
  case "pin":
@@ -2709,10 +2685,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2709
2685
  { id: args.id, pinned: args.action === "pin" },
2710
2686
  target
2711
2687
  );
2712
- if (res === null) return fail12(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
2688
+ 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.`);
2713
2689
  const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
2714
- if (capped) return fail12(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
2715
- return text10(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
2690
+ if (capped) return fail2(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
2691
+ return text(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
2716
2692
  }
2717
2693
  case "save":
2718
2694
  case "unsave": {
@@ -2722,26 +2698,26 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2722
2698
  { id: args.id, saved: args.action === "save" },
2723
2699
  target
2724
2700
  );
2725
- if (res === null) return fail12(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
2701
+ 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.`);
2726
2702
  const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
2727
- if (capped) return fail12(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
2728
- return text10(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
2703
+ if (capped) return fail2(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
2704
+ return text(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
2729
2705
  }
2730
2706
  case "remove":
2731
2707
  await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
2732
- return text10(`Removed kept record \`${args.key}\` on ${target}.`);
2708
+ return text(`Removed kept record \`${args.key}\` on ${target}.`);
2733
2709
  case "clearPinned":
2734
2710
  await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
2735
- return text10(`Unpinned all requests on ${target} (saved ones kept).`);
2711
+ return text(`Unpinned all requests on ${target} (saved ones kept).`);
2736
2712
  case "clearSaved":
2737
2713
  await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
2738
- return text10(`Cleared saved requests on ${target} (pinned ones kept).`);
2714
+ return text(`Cleared saved requests on ${target} (pinned ones kept).`);
2739
2715
  case "clearRequests":
2740
2716
  await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
2741
- return text10(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
2717
+ return text(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
2742
2718
  }
2743
2719
  } catch (e) {
2744
- return fail12(errMsg12(e));
2720
+ return fail2(errMsg2(e));
2745
2721
  }
2746
2722
  }
2747
2723
  );
@@ -2750,48 +2726,48 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2750
2726
  {
2751
2727
  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.",
2752
2728
  inputSchema: {
2753
- action: z12.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
2729
+ action: z13.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
2754
2730
  "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."
2755
2731
  ),
2756
- urlPattern: z12.string().optional().describe(
2732
+ urlPattern: z13.string().optional().describe(
2757
2733
  "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."
2758
2734
  ),
2759
- fromRequestId: z12.string().optional().describe(
2735
+ fromRequestId: z13.string().optional().describe(
2760
2736
  "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."
2761
2737
  ),
2762
- methods: z12.array(z12.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
2763
- kind: z12.enum(["respond", "fail", "delay"]).optional().describe(
2738
+ methods: z13.array(z13.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
2739
+ kind: z13.enum(["respond", "fail", "delay"]).optional().describe(
2764
2740
  "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."
2765
2741
  ),
2766
- status: z12.number().optional().describe(
2742
+ status: z13.number().optional().describe(
2767
2743
  "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."
2768
2744
  ),
2769
- body: z12.union([z12.string(), z12.record(z12.unknown()), z12.array(z12.unknown())]).optional().describe(
2745
+ body: z13.union([z13.string(), z13.record(z13.unknown()), z13.array(z13.unknown())]).optional().describe(
2770
2746
  "For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
2771
2747
  ),
2772
- statusText: z12.string().optional().describe(
2748
+ statusText: z13.string().optional().describe(
2773
2749
  "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."
2774
2750
  ),
2775
- headers: z12.record(z12.string()).optional().describe("For kind 'respond': response headers to send."),
2776
- failKind: z12.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
2777
- delayMs: z12.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
2778
- times: z12.number().optional().describe(
2751
+ headers: z13.record(z13.string()).optional().describe("For kind 'respond': response headers to send."),
2752
+ failKind: z13.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
2753
+ delayMs: z13.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
2754
+ times: z13.number().optional().describe(
2779
2755
  "Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
2780
2756
  ),
2781
- alternate: z12.boolean().optional().describe(
2757
+ alternate: z13.boolean().optional().describe(
2782
2758
  "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."
2783
2759
  ),
2784
- name: z12.string().optional().describe("Label shown in the developer's UI."),
2785
- enabled: z12.boolean().optional().describe(
2760
+ name: z13.string().optional().describe("Label shown in the developer's UI."),
2761
+ enabled: z13.boolean().optional().describe(
2786
2762
  "For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
2787
2763
  ),
2788
- id: z12.string().optional().describe(
2764
+ id: z13.string().optional().describe(
2789
2765
  "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."
2790
2766
  ),
2791
- url: z12.string().optional().describe(
2767
+ url: z13.string().optional().describe(
2792
2768
  "For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
2793
2769
  ),
2794
- deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2770
+ deviceId: deviceIdInput
2795
2771
  }
2796
2772
  },
2797
2773
  async (args) => {
@@ -2800,7 +2776,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2800
2776
  switch (args.action) {
2801
2777
  case "add": {
2802
2778
  if (!args.urlPattern && !args.fromRequestId) {
2803
- return fail12(
2779
+ return fail2(
2804
2780
  "`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*'."
2805
2781
  );
2806
2782
  }
@@ -2832,13 +2808,13 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2832
2808
  );
2833
2809
  const shaped = res;
2834
2810
  if (!shaped?.ok) {
2835
- return fail12(shaped?.error ?? `Could not add the rule on ${target}.`);
2811
+ return fail2(shaped?.error ?? `Could not add the rule on ${target}.`);
2836
2812
  }
2837
2813
  const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
2838
2814
  const armed = shaped.rule?.enabled === false ? `
2839
2815
 
2840
2816
  It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
2841
- return text10(
2817
+ return text(
2842
2818
  `Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
2843
2819
 
2844
2820
  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.`
@@ -2846,14 +2822,14 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2846
2822
  }
2847
2823
  case "remove": {
2848
2824
  if (!args.id) {
2849
- return fail12('`remove` needs an `id`. List them with network_override({action:"list"}).');
2825
+ return fail2('`remove` needs an `id`. List them with network_override({action:"list"}).');
2850
2826
  }
2851
2827
  await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
2852
- return text10(`Removed override \`${args.id}\` on ${target}.`);
2828
+ return text(`Removed override \`${args.id}\` on ${target}.`);
2853
2829
  }
2854
2830
  case "clear":
2855
2831
  await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
2856
- return text10(`Removed all override rules on ${target}. Traffic is real again.`);
2832
+ return text(`Removed all override rules on ${target}. Traffic is real again.`);
2857
2833
  case "enable":
2858
2834
  case "disable": {
2859
2835
  const enabled = args.action === "enable";
@@ -2864,12 +2840,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2864
2840
  { id: args.id, enabled },
2865
2841
  target
2866
2842
  );
2867
- return text10(
2843
+ return text(
2868
2844
  `Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
2869
2845
  );
2870
2846
  }
2871
2847
  await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
2872
- return text10(
2848
+ return text(
2873
2849
  enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
2874
2850
  );
2875
2851
  }
@@ -2884,11 +2860,11 @@ Trigger the request now (navigate / tap_element), then check it with get_network
2884
2860
  if (args.id) {
2885
2861
  const rule = rules.find((r) => r.id === args.id);
2886
2862
  if (!rule) {
2887
- return fail12(
2863
+ return fail2(
2888
2864
  `No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
2889
2865
  );
2890
2866
  }
2891
- return text10(
2867
+ return text(
2892
2868
  `**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
2893
2869
 
2894
2870
  \`\`\`json
@@ -2897,7 +2873,7 @@ ${JSON.stringify(rule, null, 2)}
2897
2873
  );
2898
2874
  }
2899
2875
  if (rules.length === 0) {
2900
- return text10(
2876
+ return text(
2901
2877
  `No override rules on ${target}.
2902
2878
 
2903
2879
  Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
@@ -2925,7 +2901,7 @@ Add one with network_override({action:"add", urlPattern:"*", status:500}) to for
2925
2901
  return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
2926
2902
  \u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
2927
2903
  });
2928
- return text10(
2904
+ return text(
2929
2905
  `**Override rules on ${target}**${masterNote(res)}
2930
2906
 
2931
2907
  ${rows.join("\n")}
@@ -2941,11 +2917,11 @@ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).
2941
2917
  target
2942
2918
  );
2943
2919
  if (!res) {
2944
- return fail12(
2920
+ return fail2(
2945
2921
  `${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
2946
2922
  );
2947
2923
  }
2948
- return text10(
2924
+ return text(
2949
2925
  `**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
2950
2926
 
2951
2927
  \`\`\`json
@@ -2957,20 +2933,245 @@ A rule that never fires is usually one of: the master switch is off, the interce
2957
2933
  }
2958
2934
  }
2959
2935
  } catch (e) {
2960
- return fail12(errMsg12(e));
2936
+ return fail2(errMsg2(e));
2937
+ }
2938
+ }
2939
+ );
2940
+ }
2941
+
2942
+ // src/tools/packs/sentry.ts
2943
+ import { z as z14 } from "zod";
2944
+ var SENTRY_TOOL_ID = "sentry";
2945
+ var text2 = (s) => ({ content: [{ type: "text", text: s }] });
2946
+ var fail4 = (message) => ({
2947
+ isError: true,
2948
+ content: [{ type: "text", text: message }]
2949
+ });
2950
+ var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
2951
+ function hhmmss2(ts) {
2952
+ const d = new Date(ts);
2953
+ const p = (n) => String(n).padStart(2, "0");
2954
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
2955
+ }
2956
+ function bytes(n) {
2957
+ if (n < 1024) return `${n}B`;
2958
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
2959
+ return `${(n / (1024 * 1024)).toFixed(2)}MB`;
2960
+ }
2961
+ function units(n, format) {
2962
+ if (format === "bytes") return bytes(n);
2963
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2964
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
2965
+ return String(n);
2966
+ }
2967
+ var FREE_QUOTA = {
2968
+ errors: 5e3,
2969
+ spans: 5e6,
2970
+ replays: 50,
2971
+ logBytes: 5 * 1024 ** 3,
2972
+ attachmentBytes: 1 * 1024 ** 3
2973
+ };
2974
+ var CATEGORY_FORMAT = {
2975
+ errors: "count",
2976
+ spans: "count",
2977
+ replays: "count",
2978
+ logBytes: "bytes",
2979
+ attachmentBytes: "bytes"
2980
+ };
2981
+ function statusNote(status) {
2982
+ switch (status) {
2983
+ case "attached":
2984
+ return "";
2985
+ case "sdk-not-found":
2986
+ 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.";
2987
+ case "no-client":
2988
+ return "\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";
2989
+ default:
2990
+ return "\n\n\u23F3 Still looking for the app's Sentry client.";
2991
+ }
2992
+ }
2993
+ function registerSentryTools(server, sync) {
2994
+ server.registerTool(
2995
+ "get_sentry_envelopes",
2996
+ {
2997
+ 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).",
2998
+ inputSchema: {
2999
+ type: z14.string().optional().describe(
3000
+ "Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
3001
+ ),
3002
+ limit: z14.number().optional().describe("Most-recent N envelopes. Default 25."),
3003
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3004
+ }
3005
+ },
3006
+ async (args) => {
3007
+ try {
3008
+ const target = sync.requireProDevice(args.deviceId);
3009
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
3010
+ const staleHeader2 = stale ? `${stale}
3011
+
3012
+ ` : "";
3013
+ const p = payload ?? {};
3014
+ const all = Array.isArray(p.envelopes) ? p.envelopes : [];
3015
+ const filtered = args.type ? all.filter((e) => e.items.some((i) => i.type === args.type)) : all;
3016
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
3017
+ const shown = filtered.slice(-limit);
3018
+ if (shown.length === 0) {
3019
+ return text2(
3020
+ `${staleHeader2}No Sentry envelopes${args.type ? ` containing a '${args.type}' item` : ""} (${all.length} captured this session).${statusNote(p.status)}`
3021
+ );
3022
+ }
3023
+ const lines = shown.map((e) => {
3024
+ const types = e.items.map((i) => i.type).join("+");
3025
+ const spans = e.items.reduce((sum, i) => sum + (i.spanCount ?? 0), 0);
3026
+ const spanNote = spans > 0 ? ` \xB7 ${spans} span${spans === 1 ? "" : "s"}` : "";
3027
+ const head = `${e.id}. ${hhmmss2(e.timestamp)} [${types}] ${bytes(e.totalBytes)}${spanNote}`;
3028
+ return `${head}
3029
+ \u2514\u2500 ${e.items[0]?.summary ?? "envelope"}`;
3030
+ });
3031
+ const header = `**Sentry \u2014 ${all.length} envelope${all.length === 1 ? "" : "s"} sent (showing ${shown.length}${args.type ? `, type '${args.type}'` : ""}):**`;
3032
+ return text2(
3033
+ `${staleHeader2}${header}${statusNote(p.status)}
3034
+
3035
+ ${lines.join("\n")}
3036
+
3037
+ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`
3038
+ );
3039
+ } catch (e) {
3040
+ return fail4(errMsg4(e));
3041
+ }
3042
+ }
3043
+ );
3044
+ server.registerTool(
3045
+ "get_sentry_cost",
3046
+ {
3047
+ 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.",
3048
+ inputSchema: {
3049
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3050
+ }
3051
+ },
3052
+ async (args) => {
3053
+ try {
3054
+ const target = sync.requireProDevice(args.deviceId);
3055
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
3056
+ const staleHeader2 = stale ? `${stale}
3057
+
3058
+ ` : "";
3059
+ const p = payload ?? {};
3060
+ const usage = p.usage;
3061
+ if (!usage?.session) {
3062
+ return text2(
3063
+ `${staleHeader2}No usage recorded yet \u2014 nothing has been sent to Sentry this session.${statusNote(p.status)}`
3064
+ );
3065
+ }
3066
+ const session = usage.session;
3067
+ const projected = usage.projectedMonthly;
3068
+ const keys = Object.keys(session);
3069
+ const rows = keys.map((key) => {
3070
+ const format = CATEGORY_FORMAT[key];
3071
+ const now = units(session[key], format);
3072
+ if (!projected) return `- ${key}: ${now} this session`;
3073
+ const month = projected[key];
3074
+ const quota = FREE_QUOTA[key];
3075
+ const pct = quota > 0 ? Math.round(month / quota * 100) : null;
3076
+ const pctNote = pct === null ? " (not on the free plan)" : ` \u2014 ${pct}% of the free plan's ${units(quota, format)}`;
3077
+ return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
3078
+ });
3079
+ const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
3080
+ const contributors = usage.topContributors?.length ? `
3081
+
3082
+ **What produced it:**
3083
+ ${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
3084
+ 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._";
3085
+ return text2(
3086
+ `${staleHeader2}**Sentry cost \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
3087
+
3088
+ ${rows.join("\n")}${contributors}${caveat}`
3089
+ );
3090
+ } catch (e) {
3091
+ return fail4(errMsg4(e));
3092
+ }
3093
+ }
3094
+ );
3095
+ server.registerTool(
3096
+ "get_sentry_drops",
3097
+ {
3098
+ 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.",
3099
+ inputSchema: {
3100
+ limit: z14.number().optional().describe("Most-recent N drop records. Default 30."),
3101
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3102
+ }
3103
+ },
3104
+ async (args) => {
3105
+ try {
3106
+ const target = sync.requireProDevice(args.deviceId);
3107
+ const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
3108
+ const staleHeader2 = stale ? `${stale}
3109
+
3110
+ ` : "";
3111
+ const p = payload ?? {};
3112
+ const summary = p.drops?.summary ?? [];
3113
+ const recent = p.drops?.recent ?? [];
3114
+ if (summary.length === 0) {
3115
+ return text2(
3116
+ `${staleHeader2}Nothing dropped this session \u2014 every captured event reached the transport.${statusNote(p.status)}
3117
+
3118
+ _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._`
3119
+ );
3120
+ }
3121
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
3122
+ const shown = recent.slice(-limit);
3123
+ const summaryLines = summary.map(
3124
+ (s) => `- ${s.reason}: ${s.units} unit${s.units === 1 ? "" : "s"} across ${s.records} record${s.records === 1 ? "" : "s"}`
3125
+ );
3126
+ const rows = shown.map(
3127
+ (d) => `${hhmmss2(d.timestamp)} [${d.reason}] ${d.category}${d.quantity > 1 ? ` \xD7${d.quantity}` : ""} \u2014 ${d.detail} (${d.source})`
3128
+ );
3129
+ return text2(
3130
+ `${staleHeader2}**Sentry drops \u2014 by reason:**
3131
+
3132
+ ${summaryLines.join("\n")}
3133
+
3134
+ **Recent (${shown.length}):**
3135
+
3136
+ \`\`\`
3137
+ ${rows.join("\n")}
3138
+ \`\`\`
3139
+
3140
+ _"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._`
3141
+ );
3142
+ } catch (e) {
3143
+ return fail4(errMsg4(e));
3144
+ }
3145
+ }
3146
+ );
3147
+ server.registerTool(
3148
+ "sentry_action",
3149
+ {
3150
+ 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.",
3151
+ inputSchema: {
3152
+ action: z14.enum(["clear"]).describe("The action to perform."),
3153
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3154
+ }
3155
+ },
3156
+ async (args) => {
3157
+ try {
3158
+ const target = sync.requireProDevice(args.deviceId);
3159
+ await sync.callAction(SENTRY_TOOL_ID, "clearEnvelopes", {}, target);
3160
+ return text2(
3161
+ "Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost."
3162
+ );
3163
+ } catch (e) {
3164
+ return fail4(errMsg4(e));
2961
3165
  }
2962
3166
  }
2963
3167
  );
2964
3168
  }
2965
3169
 
2966
3170
  // src/tools/packs/timeMachine.ts
2967
- import { z as z13 } from "zod";
3171
+ import { z as z15 } from "zod";
2968
3172
  var TIME_MACHINE_TOOL_ID = "time-machine";
2969
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2970
- var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2971
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2972
- function kb(bytes) {
2973
- return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
3173
+ function kb(bytes2) {
3174
+ return bytes2 < 1024 ? `${bytes2} B` : `${(bytes2 / 1024).toFixed(1)} KB`;
2974
3175
  }
2975
3176
  function formatOutcome(outcome) {
2976
3177
  const lines = Object.entries(outcome.results).map(([sid, r2]) => {
@@ -2995,7 +3196,7 @@ function registerTimeMachineTools(server, sync) {
2995
3196
  {
2996
3197
  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.",
2997
3198
  inputSchema: {
2998
- deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
3199
+ deviceId: deviceIdInput
2999
3200
  }
3000
3201
  },
3001
3202
  async (args) => {
@@ -3028,9 +3229,9 @@ ${providerLines.join("\n") || "_no providers registered_"}`
3028
3229
  ];
3029
3230
  if (p.lastOutcome) parts.push(`**Last restore:**
3030
3231
  ${formatOutcome(p.lastOutcome)}`);
3031
- return text11(parts.join("\n\n"));
3232
+ return text(parts.join("\n\n"));
3032
3233
  } catch (e) {
3033
- return fail13(errMsg13(e));
3234
+ return fail2(errMsg2(e));
3034
3235
  }
3035
3236
  }
3036
3237
  );
@@ -3039,14 +3240,14 @@ ${formatOutcome(p.lastOutcome)}`);
3039
3240
  {
3040
3241
  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.",
3041
3242
  inputSchema: {
3042
- action: z13.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
3043
- id: z13.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
3044
- name: z13.string().optional().describe("Snapshot name (for capture and rename)."),
3045
- mode: z13.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
3046
- excludeKeys: z13.array(z13.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").'),
3047
- restoreRoute: z13.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."),
3048
- sourceIds: z13.array(z13.string()).optional().describe("Capture/restore only these source ids (default all)."),
3049
- deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
3243
+ action: z15.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
3244
+ id: z15.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
3245
+ name: z15.string().optional().describe("Snapshot name (for capture and rename)."),
3246
+ mode: z15.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
3247
+ excludeKeys: z15.array(z15.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").'),
3248
+ restoreRoute: z15.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."),
3249
+ sourceIds: z15.array(z15.string()).optional().describe("Capture/restore only these source ids (default all)."),
3250
+ deviceId: deviceIdInput
3050
3251
  }
3051
3252
  },
3052
3253
  async (args) => {
@@ -3059,14 +3260,14 @@ ${formatOutcome(p.lastOutcome)}`);
3059
3260
  const s = result.snapshot;
3060
3261
  const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
3061
3262
  \u26A0 ${sid}: ${w}`)).join("");
3062
- return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
3263
+ return text(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
3063
3264
  }
3064
3265
  case "capture_baseline": {
3065
3266
  const result = await call("captureBaseline", {});
3066
- return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
3267
+ return text(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
3067
3268
  }
3068
3269
  case "preview": {
3069
- if (!args.id) return fail13("preview requires `id` (from get_time_machine).");
3270
+ if (!args.id) return fail2("preview requires `id` (from get_time_machine).");
3070
3271
  const result = await call("preview", { id: args.id });
3071
3272
  const lines = [];
3072
3273
  let totalApplies = 0;
@@ -3085,46 +3286,46 @@ ${formatOutcome(p.lastOutcome)}`);
3085
3286
  for (const note of src.notes) lines.push(` \xB7 ${note}`);
3086
3287
  }
3087
3288
  const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
3088
- return text11(`${head}
3289
+ return text(`${head}
3089
3290
  ${lines.join("\n")}`);
3090
3291
  }
3091
3292
  case "restore": {
3092
- if (!args.id) return fail13("restore requires `id` (from get_time_machine).");
3293
+ if (!args.id) return fail2("restore requires `id` (from get_time_machine).");
3093
3294
  const outcome = await call("restore", {
3094
3295
  id: args.id,
3095
3296
  mode: args.mode ?? "live",
3096
3297
  excludeKeys: args.excludeKeys,
3097
3298
  restoreRoute: args.restoreRoute
3098
3299
  });
3099
- return text11(formatOutcome(outcome));
3300
+ return text(formatOutcome(outcome));
3100
3301
  }
3101
3302
  case "delete": {
3102
- if (!args.id) return fail13("delete requires `id`.");
3303
+ if (!args.id) return fail2("delete requires `id`.");
3103
3304
  await call("delete", { id: args.id });
3104
- return text11(`Deleted snapshot \`${args.id}\`.`);
3305
+ return text(`Deleted snapshot \`${args.id}\`.`);
3105
3306
  }
3106
3307
  case "rename": {
3107
- if (!args.id || !args.name) return fail13("rename requires `id` and `name`.");
3308
+ if (!args.id || !args.name) return fail2("rename requires `id` and `name`.");
3108
3309
  await call("rename", { id: args.id, name: args.name });
3109
- return text11(`Renamed \`${args.id}\` to "${args.name}".`);
3310
+ return text(`Renamed \`${args.id}\` to "${args.name}".`);
3110
3311
  }
3111
3312
  }
3112
3313
  } catch (e) {
3113
- return fail13(errMsg13(e));
3314
+ return fail2(errMsg2(e));
3114
3315
  }
3115
3316
  }
3116
3317
  );
3117
3318
  }
3118
3319
 
3119
3320
  // src/tools/packs/scenarios.ts
3120
- import { z as z14 } from "zod";
3321
+ import { z as z16 } from "zod";
3121
3322
  var SCENARIOS_TOOL_ID = "scenarios";
3122
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
3123
- var fail14 = (message) => ({
3323
+ var text3 = (s) => ({ content: [{ type: "text", text: s }] });
3324
+ var fail5 = (message) => ({
3124
3325
  isError: true,
3125
3326
  content: [{ type: "text", text: message }]
3126
3327
  });
3127
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
3328
+ var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
3128
3329
  function varSpec(v) {
3129
3330
  const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
3130
3331
  const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
@@ -3186,7 +3387,7 @@ function registerScenariosTools(server, sync) {
3186
3387
  {
3187
3388
  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.",
3188
3389
  inputSchema: {
3189
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3390
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
3190
3391
  }
3191
3392
  },
3192
3393
  async (args) => {
@@ -3216,9 +3417,9 @@ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing r
3216
3417
  ${drafts.map(summarize).join("\n")}`
3217
3418
  );
3218
3419
  }
3219
- return text12(parts.join("\n\n"));
3420
+ return text3(parts.join("\n\n"));
3220
3421
  } catch (e) {
3221
- return fail14(errMsg14(e));
3422
+ return fail5(errMsg5(e));
3222
3423
  }
3223
3424
  }
3224
3425
  );
@@ -3227,66 +3428,66 @@ ${drafts.map(summarize).join("\n")}`
3227
3428
  {
3228
3429
  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.`,
3229
3430
  inputSchema: {
3230
- action: z14.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3231
- folder: z14.string().optional().describe(
3431
+ action: z16.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
3432
+ folder: z16.string().optional().describe(
3232
3433
  "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."
3233
3434
  ),
3234
- id: z14.string().optional().describe(
3435
+ id: z16.string().optional().describe(
3235
3436
  "Scenario id (required for preview/run/delete; from get_scenarios)."
3236
3437
  ),
3237
- params: z14.record(z14.union([z14.string(), z14.number(), z14.boolean()])).optional().describe(
3438
+ params: z16.record(z16.union([z16.string(), z16.number(), z16.boolean()])).optional().describe(
3238
3439
  "Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
3239
3440
  ),
3240
- scenario: z14.object({
3241
- id: z14.string().describe("Stable kebab-case id."),
3242
- name: z14.string().describe("Human name; may contain {{variable}} placeholders."),
3243
- description: z14.string().optional(),
3244
- expectedOutcome: z14.string().optional().describe(
3441
+ scenario: z16.object({
3442
+ id: z16.string().describe("Stable kebab-case id."),
3443
+ name: z16.string().describe("Human name; may contain {{variable}} placeholders."),
3444
+ description: z16.string().optional(),
3445
+ expectedOutcome: z16.string().optional().describe(
3245
3446
  "What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
3246
3447
  ),
3247
- author: z14.string().optional(),
3248
- authoredAgainst: z14.string().optional().describe("App version this was written against."),
3249
- folder: z14.string().optional().describe(
3448
+ author: z16.string().optional(),
3449
+ authoredAgainst: z16.string().optional().describe("App version this was written against."),
3450
+ folder: z16.string().optional().describe(
3250
3451
  'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
3251
3452
  ),
3252
- tags: z14.array(z14.string()).optional(),
3253
- vars: z14.array(
3254
- z14.object({
3255
- key: z14.string(),
3256
- label: z14.string().optional(),
3257
- type: z14.enum(["string", "number", "boolean", "enum"]),
3258
- options: z14.array(z14.union([z14.string(), z14.number(), z14.boolean()])).optional(),
3259
- default: z14.union([z14.string(), z14.number(), z14.boolean()]).optional()
3453
+ tags: z16.array(z16.string()).optional(),
3454
+ vars: z16.array(
3455
+ z16.object({
3456
+ key: z16.string(),
3457
+ label: z16.string().optional(),
3458
+ type: z16.enum(["string", "number", "boolean", "enum"]),
3459
+ options: z16.array(z16.union([z16.string(), z16.number(), z16.boolean()])).optional(),
3460
+ default: z16.union([z16.string(), z16.number(), z16.boolean()]).optional()
3260
3461
  })
3261
3462
  ).optional().describe(
3262
3463
  "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."
3263
3464
  ),
3264
- steps: z14.array(
3265
- z14.object({
3266
- id: z14.string().optional(),
3267
- tool: z14.string().describe(
3465
+ steps: z16.array(
3466
+ z16.object({
3467
+ id: z16.string().optional(),
3468
+ tool: z16.string().describe(
3268
3469
  "Tool id: network | storage | impersonate | route-events | query | time-machine"
3269
3470
  ),
3270
- action: z14.string().describe(
3471
+ action: z16.string().describe(
3271
3472
  "Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
3272
3473
  ),
3273
- params: z14.unknown().optional(),
3274
- label: z14.string().optional()
3474
+ params: z16.unknown().optional(),
3475
+ label: z16.string().optional()
3275
3476
  })
3276
3477
  ).describe("Steps, in order."),
3277
- undoSteps: z14.array(
3278
- z14.object({
3279
- id: z14.string().optional(),
3280
- tool: z14.string(),
3281
- action: z14.string(),
3282
- params: z14.unknown().optional(),
3283
- label: z14.string().optional()
3478
+ undoSteps: z16.array(
3479
+ z16.object({
3480
+ id: z16.string().optional(),
3481
+ tool: z16.string(),
3482
+ action: z16.string(),
3483
+ params: z16.unknown().optional(),
3484
+ label: z16.string().optional()
3284
3485
  })
3285
3486
  ).optional().describe(
3286
3487
  "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."
3287
3488
  )
3288
3489
  }).optional().describe("The scenario to save (action 'save')."),
3289
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
3490
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
3290
3491
  }
3291
3492
  },
3292
3493
  async (args) => {
@@ -3295,7 +3496,7 @@ ${drafts.map(summarize).join("\n")}`
3295
3496
  const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
3296
3497
  switch (args.action) {
3297
3498
  case "preview": {
3298
- if (!args.id) return fail14("preview requires `id` (from get_scenarios).");
3499
+ if (!args.id) return fail5("preview requires `id` (from get_scenarios).");
3299
3500
  const r = await call("preview", {
3300
3501
  id: args.id,
3301
3502
  params: args.params ?? {}
@@ -3318,16 +3519,16 @@ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
3318
3519
  r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
3319
3520
  ${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
3320
3521
  );
3321
- return text12(lines.join("\n\n"));
3522
+ return text3(lines.join("\n\n"));
3322
3523
  }
3323
3524
  case "run": {
3324
- if (!args.id) return fail14("run requires `id` (from get_scenarios).");
3525
+ if (!args.id) return fail5("run requires `id` (from get_scenarios).");
3325
3526
  const r = await call("run", {
3326
3527
  id: args.id,
3327
3528
  params: args.params ?? {}
3328
3529
  });
3329
3530
  if (r.preflightErrors?.length) {
3330
- return fail14(
3531
+ return fail5(
3331
3532
  `Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
3332
3533
  ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3333
3534
  );
@@ -3339,14 +3540,14 @@ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
3339
3540
  return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
3340
3541
  }).join("\n");
3341
3542
  if (!r.ok) {
3342
- return fail14(
3543
+ return fail5(
3343
3544
  `Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
3344
3545
  ${steps}
3345
3546
 
3346
3547
  The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
3347
3548
  );
3348
3549
  }
3349
- return text12(
3550
+ return text3(
3350
3551
  [
3351
3552
  `\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
3352
3553
  steps,
@@ -3370,49 +3571,49 @@ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
3370
3571
  const errs = rep.errors.length ? `
3371
3572
 
3372
3573
  Errors: ${rep.errors.join("; ")}` : "";
3373
- return text12(`**Deactivated.**
3574
+ return text3(`**Deactivated.**
3374
3575
  ${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
3375
3576
  }
3376
3577
  case "save": {
3377
- if (!args.scenario) return fail14("save requires `scenario`.");
3578
+ if (!args.scenario) return fail5("save requires `scenario`.");
3378
3579
  const r = await call("save", { scenario: args.scenario });
3379
3580
  if (!r.ok) {
3380
- return fail14(
3581
+ return fail5(
3381
3582
  `Save rejected:
3382
3583
  ${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
3383
3584
  );
3384
3585
  }
3385
- return text12(
3586
+ return text3(
3386
3587
  `Saved \`${r.id}\` **as a draft**.
3387
3588
 
3388
3589
  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.`
3389
3590
  );
3390
3591
  }
3391
3592
  case "setFolder": {
3392
- if (!args.id) return fail14("setFolder requires `id` (from get_scenarios).");
3593
+ if (!args.id) return fail5("setFolder requires `id` (from get_scenarios).");
3393
3594
  const r = await call("setFolder", {
3394
3595
  id: args.id,
3395
3596
  folder: args.folder
3396
3597
  });
3397
- return text12(
3598
+ return text3(
3398
3599
  r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
3399
3600
  );
3400
3601
  }
3401
3602
  case "delete": {
3402
- if (!args.id) return fail14("delete requires `id`.");
3603
+ if (!args.id) return fail5("delete requires `id`.");
3403
3604
  await call("delete", { id: args.id });
3404
- return text12(`Deleted \`${args.id}\` from the device library.`);
3605
+ return text3(`Deleted \`${args.id}\` from the device library.`);
3405
3606
  }
3406
3607
  }
3407
3608
  } catch (e) {
3408
- return fail14(errMsg14(e));
3609
+ return fail5(errMsg5(e));
3409
3610
  }
3410
3611
  }
3411
3612
  );
3412
3613
  }
3413
3614
 
3414
3615
  // src/tools/packs/camera.ts
3415
- import { z as z15 } from "zod";
3616
+ import { z as z17 } from "zod";
3416
3617
 
3417
3618
  // src/capture/camera.ts
3418
3619
  import { execFile as execFile2 } from "child_process";
@@ -3568,9 +3769,9 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
3568
3769
  }
3569
3770
 
3570
3771
  // src/tools/packs/camera.ts
3571
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
3572
- var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
3573
- var fail15 = (message) => ({
3772
+ var text4 = (s) => ({ content: [{ type: "text", text: s }] });
3773
+ var gate = (tool) => readEntitlement().isPro ? null : fail6(proRequired(tool));
3774
+ var fail6 = (message) => ({
3574
3775
  isError: true,
3575
3776
  content: [{ type: "text", text: message }]
3576
3777
  });
@@ -3580,11 +3781,11 @@ function registerCameraTools(server) {
3580
3781
  {
3581
3782
  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.",
3582
3783
  inputSchema: {
3583
- kind: z15.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
3584
- value: z15.string().optional().describe(
3784
+ kind: z17.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
3785
+ value: z17.string().optional().describe(
3585
3786
  "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`."
3586
3787
  ),
3587
- format: z15.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
3788
+ format: z17.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
3588
3789
  }
3589
3790
  },
3590
3791
  async (args) => {
@@ -3595,12 +3796,12 @@ function registerCameraTools(server) {
3595
3796
  if (args.format) cmd.push("--format", args.format);
3596
3797
  const result = await run(cmd);
3597
3798
  if (!result.ok) {
3598
- return fail15(
3799
+ return fail6(
3599
3800
  result.error ?? result.data?.error ?? "could not set the camera source"
3600
3801
  );
3601
3802
  }
3602
3803
  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;
3603
- return text13(
3804
+ return text4(
3604
3805
  `The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
3605
3806
 
3606
3807
  _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._`
@@ -3618,15 +3819,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3618
3819
  if (blocked) return blocked;
3619
3820
  const result = await run(["status"]);
3620
3821
  if (!result.ok || !result.data) {
3621
- return fail15(result.error ?? "could not read the simulator camera status");
3822
+ return fail6(result.error ?? "could not read the simulator camera status");
3622
3823
  }
3623
3824
  const { publishing, fps, codes } = result.data;
3624
3825
  if (!publishing) {
3625
- return text13(
3826
+ return text4(
3626
3827
  "The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
3627
3828
  );
3628
3829
  }
3629
- return text13(
3830
+ return text4(
3630
3831
  [
3631
3832
  `Publishing at ${fps} fps.`,
3632
3833
  codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
@@ -3641,8 +3842,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3641
3842
  {
3642
3843
  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.",
3643
3844
  inputSchema: {
3644
- bundleId: z15.string().describe("The app's bundle identifier."),
3645
- udid: z15.string().optional().describe("Simulator udid, if more than one is booted.")
3845
+ bundleId: z17.string().describe("The app's bundle identifier."),
3846
+ udid: z17.string().optional().describe("Simulator udid, if more than one is booted.")
3646
3847
  }
3647
3848
  },
3648
3849
  async (args) => {
@@ -3652,9 +3853,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3652
3853
  if (args.udid) cmd.push("--udid", args.udid);
3653
3854
  const result = await run(cmd);
3654
3855
  if (!result.ok) {
3655
- return fail15(result.error ?? result.data?.error ?? "could not launch the app");
3856
+ return fail6(result.error ?? result.data?.error ?? "could not launch the app");
3656
3857
  }
3657
- return text13(
3858
+ return text4(
3658
3859
  `Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
3659
3860
  );
3660
3861
  }
@@ -3664,7 +3865,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3664
3865
  {
3665
3866
  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.",
3666
3867
  inputSchema: {
3667
- bundleId: z15.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
3868
+ bundleId: z17.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
3668
3869
  }
3669
3870
  },
3670
3871
  async (args) => {
@@ -3672,11 +3873,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3672
3873
  if (args.bundleId) cmd.push(args.bundleId);
3673
3874
  const result = await run(cmd);
3674
3875
  if (!result.data?.checks) {
3675
- return fail15(result.error ?? "could not run the simulator camera diagnosis");
3876
+ return fail6(result.error ?? "could not run the simulator camera diagnosis");
3676
3877
  }
3677
3878
  const ent = readEntitlement();
3678
3879
  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._";
3679
- return text13(describeDiagnosis(result.data.checks) + tierNote);
3880
+ return text4(describeDiagnosis(result.data.checks) + tierNote);
3680
3881
  }
3681
3882
  );
3682
3883
  server.registerTool(
@@ -3690,9 +3891,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3690
3891
  if (blocked) return blocked;
3691
3892
  const result = await run(["stop"]);
3692
3893
  if (!result.ok) {
3693
- return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
3894
+ return fail6(result.error ?? result.data?.error ?? "could not stop the camera");
3694
3895
  }
3695
- return text13("The simulator camera is off.");
3896
+ return text4("The simulator camera is off.");
3696
3897
  }
3697
3898
  );
3698
3899
  server.registerTool(
@@ -3700,7 +3901,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3700
3901
  {
3701
3902
  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.",
3702
3903
  inputSchema: {
3703
- udid: z15.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
3904
+ udid: z17.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
3704
3905
  }
3705
3906
  },
3706
3907
  async (args) => {
@@ -3708,7 +3909,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3708
3909
  if (blocked) return blocked;
3709
3910
  const devicesResult = await run(["devices"]);
3710
3911
  if (!devicesResult.ok || !devicesResult.data) {
3711
- return fail15(devicesResult.error ?? "could not list booted simulators");
3912
+ return fail6(devicesResult.error ?? "could not list booted simulators");
3712
3913
  }
3713
3914
  const devices = devicesResult.data.devices ?? [];
3714
3915
  let apps = null;
@@ -3719,7 +3920,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3719
3920
  ]);
3720
3921
  apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
3721
3922
  }
3722
- return text13(describeDevices(devices, apps));
3923
+ return text4(describeDevices(devices, apps));
3723
3924
  }
3724
3925
  );
3725
3926
  server.registerTool(
@@ -3736,11 +3937,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3736
3937
  run(["windows"])
3737
3938
  ]);
3738
3939
  if (!cams.ok && !wins.ok) {
3739
- return fail15(
3940
+ return fail6(
3740
3941
  cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
3741
3942
  );
3742
3943
  }
3743
- return text13(
3944
+ return text4(
3744
3945
  describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
3745
3946
  );
3746
3947
  }
@@ -3750,8 +3951,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3750
3951
  {
3751
3952
  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.",
3752
3953
  inputSchema: {
3753
- action: z15.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
3754
- udid: z15.string().optional().describe("Which simulator. Only needed when several are booted.")
3954
+ action: z17.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
3955
+ udid: z17.string().optional().describe("Which simulator. Only needed when several are booted.")
3755
3956
  }
3756
3957
  },
3757
3958
  async (args) => {
@@ -3759,36 +3960,36 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
3759
3960
  if (blocked) return blocked;
3760
3961
  const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
3761
3962
  if (!result.ok) {
3762
- return fail15(
3963
+ return fail6(
3763
3964
  result.error ?? result.data?.error ?? "could not change zero-setup mode"
3764
3965
  );
3765
3966
  }
3766
3967
  const d = result.data ?? {};
3767
3968
  if (args.action === "status") {
3768
- if (!d.enabled) return text13("Zero-setup mode is **off**.");
3769
- return text13(
3969
+ if (!d.enabled) return text4("Zero-setup mode is **off**.");
3970
+ return text4(
3770
3971
  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."
3771
3972
  );
3772
3973
  }
3773
3974
  if (args.action === "on") {
3774
- return text13(
3975
+ return text4(
3775
3976
  "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._"
3776
3977
  );
3777
3978
  }
3778
- return text13("Zero-setup mode is **off**.");
3979
+ return text4("Zero-setup mode is **off**.");
3779
3980
  }
3780
3981
  );
3781
3982
  }
3782
3983
 
3783
3984
  // src/tools/packs/interact.ts
3784
- import { z as z16 } from "zod";
3985
+ import { z as z18 } from "zod";
3785
3986
  var TOOL_ID2 = "highlight-updates";
3786
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3787
- var fail16 = (message) => ({
3987
+ var text5 = (s) => ({ content: [{ type: "text", text: s }] });
3988
+ var fail7 = (message) => ({
3788
3989
  isError: true,
3789
3990
  content: [{ type: "text", text: message }]
3790
3991
  });
3791
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3992
+ var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
3792
3993
  function formatElement(e) {
3793
3994
  let line = e.interactive ? "\u2022 " : " ";
3794
3995
  line += e.name;
@@ -3811,8 +4012,8 @@ function registerInteractTools(server, sync) {
3811
4012
  {
3812
4013
  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.",
3813
4014
  inputSchema: {
3814
- interactiveOnly: z16.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
3815
- deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
4015
+ interactiveOnly: z18.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
4016
+ deviceId: deviceIdInput
3816
4017
  }
3817
4018
  },
3818
4019
  async (args) => {
@@ -3822,16 +4023,16 @@ function registerInteractTools(server, sync) {
3822
4023
  const all = res?.elements ?? [];
3823
4024
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
3824
4025
  if (elements.length === 0) {
3825
- return text14(
4026
+ return text5(
3826
4027
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
3827
4028
  );
3828
4029
  }
3829
4030
  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 }).`;
3830
- return text14(`${header}
4031
+ return text5(`${header}
3831
4032
 
3832
4033
  ${elements.map(formatElement).join("\n")}`);
3833
4034
  } catch (e) {
3834
- return fail16(errMsg15(e));
4035
+ return fail7(errMsg6(e));
3835
4036
  }
3836
4037
  }
3837
4038
  );
@@ -3840,20 +4041,20 @@ ${elements.map(formatElement).join("\n")}`);
3840
4041
  {
3841
4042
  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.",
3842
4043
  inputSchema: {
3843
- nativeTag: z16.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
3844
- testID: z16.string().optional().describe("Exact testID of the element."),
3845
- query: z16.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
3846
- value: z16.union([z16.number(), z16.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
3847
- text: z16.string().optional().describe("For a text input: the string to set via onChangeText."),
3848
- longPress: z16.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
3849
- scrollIntoView: z16.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
3850
- deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
4044
+ nativeTag: z18.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
4045
+ testID: z18.string().optional().describe("Exact testID of the element."),
4046
+ query: z18.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
4047
+ value: z18.union([z18.number(), z18.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
4048
+ text: z18.string().optional().describe("For a text input: the string to set via onChangeText."),
4049
+ longPress: z18.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
4050
+ scrollIntoView: z18.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
4051
+ deviceId: deviceIdInput
3851
4052
  }
3852
4053
  },
3853
4054
  async (args) => {
3854
4055
  try {
3855
4056
  if (args.nativeTag == null && !args.testID && !args.query) {
3856
- return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
4057
+ return fail7("Provide one of: nativeTag, testID, or query (from describe_screen).");
3857
4058
  }
3858
4059
  const target = sync.requireProDevice(args.deviceId);
3859
4060
  const res = await sync.callAction(
@@ -3878,26 +4079,23 @@ ${elements.map(formatElement).join("\n")}`);
3878
4079
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
3879
4080
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
3880
4081
  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}`;
3881
- return text14(`${verb}${scrolledNote}.${flowTip}`);
4082
+ return text5(`${verb}${scrolledNote}.${flowTip}`);
3882
4083
  }
3883
4084
  let msg = res?.reason ?? "Tap failed.";
3884
4085
  if (res?.candidates?.length) {
3885
4086
  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");
3886
4087
  }
3887
- return fail16(msg);
4088
+ return fail7(msg);
3888
4089
  } catch (e) {
3889
- return fail16(errMsg15(e));
4090
+ return fail7(errMsg6(e));
3890
4091
  }
3891
4092
  }
3892
4093
  );
3893
4094
  }
3894
4095
 
3895
4096
  // src/tools/packs/jsThread.ts
3896
- import { z as z17 } from "zod";
4097
+ import { z as z19 } from "zod";
3897
4098
  var JS_TOP_TOOL_ID = "js-top";
3898
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3899
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3900
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3901
4099
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
3902
4100
  function sparkline(values) {
3903
4101
  const max = Math.max(1, ...values);
@@ -3968,9 +4166,9 @@ function registerJsThreadTools(server, sync) {
3968
4166
  {
3969
4167
  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.',
3970
4168
  inputSchema: {
3971
- deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
3972
- durationMs: z17.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
3973
- clearFirst: z17.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
4169
+ deviceId: deviceIdInput,
4170
+ durationMs: z19.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
4171
+ clearFirst: z19.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
3974
4172
  }
3975
4173
  },
3976
4174
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -3983,7 +4181,7 @@ function registerJsThreadTools(server, sync) {
3983
4181
  { durationMs: duration, clearFirst: clearFirst === true },
3984
4182
  target
3985
4183
  );
3986
- if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
4184
+ if (!snap) return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
3987
4185
  const md = formatTopMarkdown(snap, duration);
3988
4186
  const slim = {
3989
4187
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -4002,13 +4200,13 @@ function registerJsThreadTools(server, sync) {
4002
4200
  worstFreezeMs: r.worstFreezeMs
4003
4201
  }))
4004
4202
  };
4005
- return text15(`${md}
4203
+ return text(`${md}
4006
4204
 
4007
4205
  \`\`\`json
4008
4206
  ${JSON.stringify(slim)}
4009
4207
  \`\`\``);
4010
4208
  } catch (e) {
4011
- return fail17(errMsg16(e));
4209
+ return fail2(errMsg2(e));
4012
4210
  }
4013
4211
  }
4014
4212
  );
@@ -4017,8 +4215,8 @@ ${JSON.stringify(slim)}
4017
4215
  {
4018
4216
  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).',
4019
4217
  inputSchema: {
4020
- deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
4021
- key: z17.string().describe("Origin key from get_js_thread_top's JSON rows.")
4218
+ deviceId: deviceIdInput,
4219
+ key: z19.string().describe("Origin key from get_js_thread_top's JSON rows.")
4022
4220
  }
4023
4221
  },
4024
4222
  async ({ deviceId, key }) => {
@@ -4031,18 +4229,18 @@ ${JSON.stringify(slim)}
4031
4229
  target
4032
4230
  );
4033
4231
  if (!detail) {
4034
- return fail17(
4232
+ return fail2(
4035
4233
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
4036
4234
  );
4037
4235
  }
4038
4236
  const md = formatDetailMarkdown(detail);
4039
- return text15(`${md}
4237
+ return text(`${md}
4040
4238
 
4041
4239
  \`\`\`json
4042
4240
  ${JSON.stringify(detail)}
4043
4241
  \`\`\``);
4044
4242
  } catch (e) {
4045
- return fail17(errMsg16(e));
4243
+ return fail2(errMsg2(e));
4046
4244
  }
4047
4245
  }
4048
4246
  );
@@ -4051,27 +4249,24 @@ ${JSON.stringify(detail)}
4051
4249
  {
4052
4250
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
4053
4251
  inputSchema: {
4054
- deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected).")
4252
+ deviceId: deviceIdInput
4055
4253
  }
4056
4254
  },
4057
4255
  async ({ deviceId }) => {
4058
4256
  try {
4059
4257
  const target = sync.requireProDevice(deviceId);
4060
4258
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
4061
- return text15("Cleared js-top stats.");
4259
+ return text("Cleared js-top stats.");
4062
4260
  } catch (e) {
4063
- return fail17(errMsg16(e));
4261
+ return fail2(errMsg2(e));
4064
4262
  }
4065
4263
  }
4066
4264
  );
4067
4265
  }
4068
4266
 
4069
4267
  // src/tools/packs/images.ts
4070
- import { z as z18 } from "zod";
4268
+ import { z as z20 } from "zod";
4071
4269
  var IMAGES_TOOL_ID = "images";
4072
- var text16 = (s) => ({ content: [{ type: "text", text: s }] });
4073
- var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4074
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
4075
4270
  function uriTail(uri) {
4076
4271
  if (!uri) return "(no source)";
4077
4272
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -4091,8 +4286,8 @@ function cacheLabel(r) {
4091
4286
  if (r.progressSeen) return "network";
4092
4287
  return "?";
4093
4288
  }
4094
- function mb(bytes) {
4095
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
4289
+ function mb(bytes2) {
4290
+ return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`;
4096
4291
  }
4097
4292
  function formatListMarkdown(list, limit) {
4098
4293
  const lines = [];
@@ -4159,9 +4354,9 @@ function registerImagesTools(server, sync) {
4159
4354
  {
4160
4355
  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.",
4161
4356
  inputSchema: {
4162
- deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
4163
- limit: z18.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
4164
- status: z18.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
4357
+ deviceId: deviceIdInput,
4358
+ limit: z20.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
4359
+ status: z20.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
4165
4360
  }
4166
4361
  },
4167
4362
  async ({ deviceId, limit, status }) => {
@@ -4174,7 +4369,7 @@ function registerImagesTools(server, sync) {
4174
4369
  { limit: capped, status },
4175
4370
  target
4176
4371
  );
4177
- if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
4372
+ if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
4178
4373
  const md = formatListMarkdown(list, capped);
4179
4374
  const slim = {
4180
4375
  stats: list.stats,
@@ -4201,13 +4396,13 @@ function registerImagesTools(server, sync) {
4201
4396
  layoutShifts: r.layoutShifts
4202
4397
  }))
4203
4398
  };
4204
- return text16(`${md}
4399
+ return text(`${md}
4205
4400
 
4206
4401
  \`\`\`json
4207
4402
  ${JSON.stringify(slim)}
4208
4403
  \`\`\``);
4209
4404
  } catch (e) {
4210
- return fail18(errMsg17(e));
4405
+ return fail2(errMsg2(e));
4211
4406
  }
4212
4407
  }
4213
4408
  );
@@ -4216,9 +4411,9 @@ ${JSON.stringify(slim)}
4216
4411
  {
4217
4412
  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.",
4218
4413
  inputSchema: {
4219
- deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
4220
- id: z18.number().describe("Record id from get_images."),
4221
- action: z18.enum([
4414
+ deviceId: deviceIdInput,
4415
+ id: z20.number().describe("Record id from get_images."),
4416
+ action: z20.enum([
4222
4417
  "hard_reload",
4223
4418
  "retry",
4224
4419
  "evict_disk",
@@ -4230,7 +4425,7 @@ ${JSON.stringify(slim)}
4230
4425
  "swap_url",
4231
4426
  "restore"
4232
4427
  ]).describe("What to do."),
4233
- url: z18.string().optional().describe("Replacement URL (swap_url only).")
4428
+ url: z20.string().optional().describe("Replacement URL (swap_url only).")
4234
4429
  }
4235
4430
  },
4236
4431
  async ({ deviceId, id, action, url }) => {
@@ -4238,8 +4433,8 @@ ${JSON.stringify(slim)}
4238
4433
  const target = sync.requireProDevice(deviceId);
4239
4434
  if (action === "prove_savings") {
4240
4435
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
4241
- if (!savings) return fail18("Device returned nothing for proveSavings.");
4242
- if (!savings.ok) return fail18(savings.message);
4436
+ if (!savings) return fail2("Device returned nothing for proveSavings.");
4437
+ if (!savings.ok) return fail2(savings.message);
4243
4438
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
4244
4439
  if (savings.originalBytes != null)
4245
4440
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -4251,11 +4446,11 @@ ${JSON.stringify(slim)}
4251
4446
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
4252
4447
  if (savings.optimizedUri)
4253
4448
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
4254
- return text16(parts.join("\n"));
4449
+ return text(parts.join("\n"));
4255
4450
  }
4256
4451
  const entry = ACTION_MAP[action];
4257
- if (!entry) return fail18(`Unknown action: ${action}`);
4258
- if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
4452
+ if (!entry) return fail2(`Unknown action: ${action}`);
4453
+ if (action === "swap_url" && !url) return fail2("swap_url requires the `url` param.");
4259
4454
  const result = await sync.callAction(
4260
4455
  IMAGES_TOOL_ID,
4261
4456
  entry.action,
@@ -4275,9 +4470,9 @@ ${JSON.stringify(slim)}
4275
4470
  );
4276
4471
  if (detail.error) lines.push(`Error: ${detail.error}`);
4277
4472
  }
4278
- return text16(lines.join("\n"));
4473
+ return text(lines.join("\n"));
4279
4474
  } catch (e) {
4280
- return fail18(errMsg17(e));
4475
+ return fail2(errMsg2(e));
4281
4476
  }
4282
4477
  }
4283
4478
  );
@@ -4286,12 +4481,12 @@ ${JSON.stringify(slim)}
4286
4481
  {
4287
4482
  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.",
4288
4483
  inputSchema: {
4289
- deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
4290
- network: z18.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
4291
- blankImages: z18.boolean().optional().describe("Render all images without a source."),
4292
- massAction: z18.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."),
4293
- clearRegistry: z18.boolean().optional().describe("Clear the captured image log."),
4294
- clearExpoCaches: z18.boolean().optional().describe("Clear expo-image memory + disk caches.")
4484
+ deviceId: deviceIdInput,
4485
+ network: z20.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
4486
+ blankImages: z20.boolean().optional().describe("Render all images without a source."),
4487
+ massAction: z20.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."),
4488
+ clearRegistry: z20.boolean().optional().describe("Clear the captured image log."),
4489
+ clearExpoCaches: z20.boolean().optional().describe("Clear expo-image memory + disk caches.")
4295
4490
  }
4296
4491
  },
4297
4492
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -4318,17 +4513,17 @@ ${JSON.stringify(slim)}
4318
4513
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
4319
4514
  done.push(r?.message ?? "expo caches cleared");
4320
4515
  }
4321
- if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4322
- return text16(`Applied: ${done.join(" \xB7 ")}`);
4516
+ if (done.length === 0) return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
4517
+ return text(`Applied: ${done.join(" \xB7 ")}`);
4323
4518
  } catch (e) {
4324
- return fail18(errMsg17(e));
4519
+ return fail2(errMsg2(e));
4325
4520
  }
4326
4521
  }
4327
4522
  );
4328
4523
  }
4329
4524
 
4330
4525
  // src/tools/packs/assets.ts
4331
- import { z as z19 } from "zod";
4526
+ import { z as z21 } from "zod";
4332
4527
 
4333
4528
  // src/tools/packs/assetsGraphNote.ts
4334
4529
  function graphUnavailableNote(ss) {
@@ -4343,14 +4538,11 @@ function graphUnavailableNote(ss) {
4343
4538
 
4344
4539
  // src/tools/packs/assets.ts
4345
4540
  var ASSETS_TOOL_ID = "assets";
4346
- var text17 = (s) => ({ content: [{ type: "text", text: s }] });
4347
- var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4348
- var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
4349
- function kb2(bytes) {
4350
- if (bytes == null || bytes <= 0) return "\u2013";
4351
- if (bytes < 1024) return `${bytes}B`;
4352
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
4353
- return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
4541
+ function kb2(bytes2) {
4542
+ if (bytes2 == null || bytes2 <= 0) return "\u2013";
4543
+ if (bytes2 < 1024) return `${bytes2}B`;
4544
+ if (bytes2 < 1024 * 1024) return `${(bytes2 / 1024).toFixed(1)}KB`;
4545
+ return `${(bytes2 / (1024 * 1024)).toFixed(2)}MB`;
4354
4546
  }
4355
4547
  function sizeLabel(r) {
4356
4548
  if (r.sizeBytes == null) return "\u2013";
@@ -4433,10 +4625,10 @@ function registerAssetsTools(server, sync) {
4433
4625
  {
4434
4626
  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.",
4435
4627
  inputSchema: {
4436
- deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
4437
- limit: z19.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4438
- kind: z19.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4439
- unusedOnly: z19.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4628
+ deviceId: deviceIdInput,
4629
+ limit: z21.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
4630
+ kind: z21.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
4631
+ unusedOnly: z21.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
4440
4632
  }
4441
4633
  },
4442
4634
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -4449,7 +4641,7 @@ function registerAssetsTools(server, sync) {
4449
4641
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
4450
4642
  target
4451
4643
  );
4452
- if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4644
+ if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
4453
4645
  if (unusedOnly) {
4454
4646
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
4455
4647
  }
@@ -4473,13 +4665,13 @@ function registerAssetsTools(server, sync) {
4473
4665
  sizeSource: r.sizeSource
4474
4666
  }))
4475
4667
  };
4476
- return text17(`${md}
4668
+ return text(`${md}
4477
4669
 
4478
4670
  \`\`\`json
4479
4671
  ${JSON.stringify(slim)}
4480
4672
  \`\`\``);
4481
4673
  } catch (e) {
4482
- return fail19(errMsg18(e));
4674
+ return fail2(errMsg2(e));
4483
4675
  }
4484
4676
  }
4485
4677
  );
@@ -4488,9 +4680,9 @@ ${JSON.stringify(slim)}
4488
4680
  {
4489
4681
  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).",
4490
4682
  inputSchema: {
4491
- deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
4492
- action: z19.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4493
- id: z19.number().optional().describe("Asset id from get_assets (detail only).")
4683
+ deviceId: deviceIdInput,
4684
+ action: z21.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
4685
+ id: z21.number().optional().describe("Asset id from get_assets (detail only).")
4494
4686
  }
4495
4687
  },
4496
4688
  async ({ deviceId, action, id }) => {
@@ -4500,19 +4692,19 @@ ${JSON.stringify(slim)}
4500
4692
  case "rescan": {
4501
4693
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
4502
4694
  const ss = result?.scanStatus;
4503
- if (!ss) return fail19("Device returned nothing for rescan.");
4504
- return text17(
4695
+ if (!ss) return fail2("Device returned nothing for rescan.");
4696
+ return text(
4505
4697
  `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" : "")
4506
4698
  );
4507
4699
  }
4508
4700
  case "measure_sizes": {
4509
4701
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
4510
- return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4702
+ return text(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
4511
4703
  }
4512
4704
  case "detail": {
4513
- if (id == null) return fail19("detail requires the `id` param (from get_assets).");
4705
+ if (id == null) return fail2("detail requires the `id` param (from get_assets).");
4514
4706
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
4515
- if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4707
+ if (!detail) return fail2(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
4516
4708
  const lines = [
4517
4709
  `## ${detail.name}.${detail.type} (#${detail.id})`,
4518
4710
  "",
@@ -4520,8 +4712,8 @@ ${JSON.stringify(slim)}
4520
4712
  ];
4521
4713
  if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
4522
4714
  if (detail.sizesByScale) {
4523
- for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
4524
- lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
4715
+ for (const [scale, bytes2] of Object.entries(detail.sizesByScale)) {
4716
+ lines.push(`- Size @${scale}x: ${kb2(bytes2)}`);
4525
4717
  }
4526
4718
  }
4527
4719
  if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
@@ -4530,32 +4722,32 @@ ${JSON.stringify(slim)}
4530
4722
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
4531
4723
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
4532
4724
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
4533
- return text17(lines.join("\n"));
4725
+ return text(lines.join("\n"));
4534
4726
  }
4535
4727
  case "save_baseline": {
4536
4728
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
4537
- return text17(
4729
+ return text(
4538
4730
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
4539
4731
  );
4540
4732
  }
4541
4733
  case "clear_baseline": {
4542
4734
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
4543
- return text17("Baseline cleared.");
4735
+ return text("Baseline cleared.");
4544
4736
  }
4545
4737
  case "clear_records": {
4546
4738
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
4547
- return text17(result?.message ?? "Inventory cleared.");
4739
+ return text(result?.message ?? "Inventory cleared.");
4548
4740
  }
4549
4741
  }
4550
4742
  } catch (e) {
4551
- return fail19(errMsg18(e));
4743
+ return fail2(errMsg2(e));
4552
4744
  }
4553
4745
  }
4554
4746
  );
4555
4747
  }
4556
4748
 
4557
4749
  // src/tools/packs/reload.ts
4558
- import { z as z20 } from "zod";
4750
+ import { z as z22 } from "zod";
4559
4751
 
4560
4752
  // src/capture/devServerReload.ts
4561
4753
  import { WebSocket } from "ws";
@@ -4618,9 +4810,6 @@ var DEFAULT_WAIT_MS = 3e4;
4618
4810
  var TEARDOWN_GRACE_MS = 1200;
4619
4811
  var PROBE_TIMEOUT_MS = 1500;
4620
4812
  var PROBE_INTERVAL_MS = 400;
4621
- var text18 = (s) => ({ content: [{ type: "text", text: s }] });
4622
- var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
4623
- var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
4624
4813
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
4625
4814
  async function ping(sync, deviceId, timeoutMs) {
4626
4815
  try {
@@ -4659,12 +4848,12 @@ function registerReloadTools(server, sync) {
4659
4848
  {
4660
4849
  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.",
4661
4850
  inputSchema: {
4662
- strategy: z20.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4851
+ strategy: z22.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
4663
4852
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
4664
4853
  ),
4665
- wait: z20.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4666
- timeoutMs: z20.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4667
- deviceId: z20.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4854
+ wait: z22.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
4855
+ timeoutMs: z22.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
4856
+ deviceId: z22.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
4668
4857
  }
4669
4858
  },
4670
4859
  async (args) => {
@@ -4706,39 +4895,39 @@ function registerReloadTools(server, sync) {
4706
4895
  }
4707
4896
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
4708
4897
  if (args.wait === false) {
4709
- return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4898
+ return text(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
4710
4899
  }
4711
4900
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
4712
4901
  if (elapsed == null) {
4713
- return fail20(
4902
+ return fail2(
4714
4903
  `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.`
4715
4904
  );
4716
4905
  }
4717
- return text18(
4906
+ return text(
4718
4907
  `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.`
4719
4908
  );
4720
4909
  } catch (e) {
4721
- return fail20(errMsg19(e));
4910
+ return fail2(errMsg2(e));
4722
4911
  }
4723
4912
  }
4724
4913
  );
4725
4914
  }
4726
4915
 
4727
4916
  // src/tools/packs/triage.ts
4728
- import { z as z21 } from "zod";
4729
- var text19 = (s) => ({ content: [{ type: "text", text: s }] });
4730
- var fail21 = (message) => ({
4917
+ import { z as z23 } from "zod";
4918
+ var text6 = (s) => ({ content: [{ type: "text", text: s }] });
4919
+ var fail8 = (message) => ({
4731
4920
  isError: true,
4732
4921
  content: [{ type: "text", text: message }]
4733
4922
  });
4734
- var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
4923
+ var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
4735
4924
  var PROBE_TIMEOUT_MS2 = 5e3;
4736
4925
  var MAX_STORAGE_KEYS = 20;
4737
4926
  var MAX_STORAGE_VALUE = 400;
4738
4927
  var STORAGE_VALUE_BUDGET = 1600;
4739
4928
  var MAX_LINE = 160;
4740
4929
  var cap = (s, n = MAX_LINE) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
4741
- function hhmmss2(ts) {
4930
+ function hhmmss3(ts) {
4742
4931
  if (!ts) return "--:--:--";
4743
4932
  const d = new Date(ts);
4744
4933
  const p = (n) => String(n).padStart(2, "0");
@@ -4768,7 +4957,7 @@ async function section(title, run2) {
4768
4957
  return body ? `${title}
4769
4958
  ${body}` : null;
4770
4959
  } catch (e) {
4771
- const msg = errMsg20(e);
4960
+ const msg = errMsg7(e);
4772
4961
  if (/not responding|timed out|got no answer/i.test(msg)) {
4773
4962
  return `${title}
4774
4963
  (no answer \u2014 see the note above)`;
@@ -4783,7 +4972,7 @@ function registerTriageTools(server, sync) {
4783
4972
  {
4784
4973
  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.",
4785
4974
  inputSchema: {
4786
- deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
4975
+ deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
4787
4976
  }
4788
4977
  },
4789
4978
  async (args) => {
@@ -4807,7 +4996,7 @@ function registerTriageTools(server, sync) {
4807
4996
  return `(none \u2014 ${entries.length} entries captured)`;
4808
4997
  }
4809
4998
  return shown.map(
4810
- (e) => `${hhmmss2(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
4999
+ (e) => `${hhmmss3(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
4811
5000
  (e.message ?? "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).join(" \u23CE "),
4812
5001
  220
4813
5002
  )}`
@@ -4890,7 +5079,7 @@ _Values: get_storage includeValues:true_`;
4890
5079
 
4891
5080
  \u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
4892
5081
  const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
4893
- return text19(
5082
+ return text6(
4894
5083
  `${head}${banner}
4895
5084
 
4896
5085
  ${body}
@@ -4898,22 +5087,22 @@ ${body}
4898
5087
  _Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
4899
5088
  );
4900
5089
  } catch (e) {
4901
- return fail21(errMsg20(e));
5090
+ return fail8(errMsg7(e));
4902
5091
  }
4903
5092
  }
4904
5093
  );
4905
5094
  }
4906
5095
 
4907
5096
  // src/tools/packs/flow.ts
4908
- import { z as z22 } from "zod";
5097
+ import { z as z24 } from "zod";
4909
5098
  var UI_TOOL_ID = "highlight-updates";
4910
5099
  var APP_TOOL_ID2 = "app";
4911
- var text20 = (s) => ({ content: [{ type: "text", text: s }] });
4912
- var fail22 = (message) => ({
5100
+ var text7 = (s) => ({ content: [{ type: "text", text: s }] });
5101
+ var fail9 = (message) => ({
4913
5102
  isError: true,
4914
5103
  content: [{ type: "text", text: message }]
4915
5104
  });
4916
- var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
5105
+ var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
4917
5106
  var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
4918
5107
  var POLL_MS = 250;
4919
5108
  var DEFAULT_WAIT_MS2 = 5e3;
@@ -4930,19 +5119,19 @@ function formatElement2(e) {
4930
5119
  const display = e.text ?? e.label;
4931
5120
  return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
4932
5121
  }
4933
- var stepSchema = z22.object({
4934
- tap: z22.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
4935
- longPress: z22.boolean().optional().describe("Make `tap` a long-press instead."),
4936
- type: z22.string().optional().describe("Text to type (needs `into`)."),
4937
- into: z22.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
4938
- set: z22.union([z22.number(), z22.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
4939
- reload: z22.boolean().optional().describe("Reload the JS bundle before this step's action."),
4940
- waitMs: z22.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
4941
- waitFor: z22.string().optional().describe("Wait until an element matching this appears (default 5s)."),
4942
- waitGone: z22.string().optional().describe("Wait until no element matches this (default 5s)."),
4943
- expect: z22.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
4944
- expectGone: z22.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
4945
- timeoutMs: z22.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
5122
+ var stepSchema = z24.object({
5123
+ tap: z24.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5124
+ longPress: z24.boolean().optional().describe("Make `tap` a long-press instead."),
5125
+ type: z24.string().optional().describe("Text to type (needs `into`)."),
5126
+ into: z24.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5127
+ set: z24.union([z24.number(), z24.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
5128
+ reload: z24.boolean().optional().describe("Reload the JS bundle before this step's action."),
5129
+ waitMs: z24.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
5130
+ waitFor: z24.string().optional().describe("Wait until an element matching this appears (default 5s)."),
5131
+ waitGone: z24.string().optional().describe("Wait until no element matches this (default 5s)."),
5132
+ expect: z24.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
5133
+ expectGone: z24.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
5134
+ timeoutMs: z24.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
4946
5135
  }).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
4947
5136
  function registerFlowTools(server, sync) {
4948
5137
  server.registerTool(
@@ -4950,10 +5139,10 @@ function registerFlowTools(server, sync) {
4950
5139
  {
4951
5140
  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.",
4952
5141
  inputSchema: {
4953
- steps: z22.array(stepSchema).min(1).describe("Steps to run, in order."),
4954
- include: z22.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."),
4955
- continueOnFailure: z22.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
4956
- deviceId: z22.string().optional().describe("Target device id (auto-selected if one is connected).")
5142
+ steps: z24.array(stepSchema).min(1).describe("Steps to run, in order."),
5143
+ include: z24.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."),
5144
+ continueOnFailure: z24.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
5145
+ deviceId: deviceIdInput
4957
5146
  }
4958
5147
  },
4959
5148
  async (args) => {
@@ -5125,7 +5314,7 @@ function registerFlowTools(server, sync) {
5125
5314
  else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
5126
5315
  }
5127
5316
  } catch (e) {
5128
- stepErrors.push(errMsg21(e));
5317
+ stepErrors.push(errMsg8(e));
5129
5318
  }
5130
5319
  if (stepErrors.length) {
5131
5320
  lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
@@ -5148,7 +5337,7 @@ function registerFlowTools(server, sync) {
5148
5337
  ${shown.map(formatElement2).join("\n")}`
5149
5338
  );
5150
5339
  } catch (e) {
5151
- parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
5340
+ parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(e))}`);
5152
5341
  }
5153
5342
  }
5154
5343
  if (include === "all") {
@@ -5177,9 +5366,9 @@ ${shown.map(formatElement2).join("\n")}`
5177
5366
  }
5178
5367
  }
5179
5368
  }
5180
- return text20(parts.join("\n\n"));
5369
+ return text7(parts.join("\n\n"));
5181
5370
  } catch (e) {
5182
- return fail22(errMsg21(e));
5371
+ return fail9(errMsg8(e));
5183
5372
  }
5184
5373
  }
5185
5374
  );
@@ -5200,25 +5389,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
5200
5389
  var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
5201
5390
 
5202
5391
  // src/tools/packs/renders.ts
5203
- import { z as z23 } from "zod";
5392
+ import { z as z25 } from "zod";
5204
5393
  var UI_TOOL_ID2 = "highlight-updates";
5205
- var text21 = (s) => ({ content: [{ type: "text", text: s }] });
5206
- var fail23 = (message) => ({
5394
+ var text8 = (s) => ({ content: [{ type: "text", text: s }] });
5395
+ var fail10 = (message) => ({
5207
5396
  isError: true,
5208
5397
  content: [{ type: "text", text: message }]
5209
5398
  });
5210
- var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
5399
+ var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
5211
5400
  var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
5212
5401
  var DEFAULT_IDLE_MS = 3e3;
5213
5402
  var SETTLE_MS = 400;
5214
5403
  var DEFAULT_TOP = 12;
5215
5404
  var lastCapture = /* @__PURE__ */ new Map();
5216
- var stepSchema2 = z23.object({
5217
- tap: z23.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5218
- type: z23.string().optional().describe("Text to type (needs `into`)."),
5219
- into: z23.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5220
- waitMs: z23.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
5221
- repeat: z23.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
5405
+ var stepSchema2 = z25.object({
5406
+ tap: z25.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
5407
+ type: z25.string().optional().describe("Text to type (needs `into`)."),
5408
+ into: z25.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
5409
+ waitMs: z25.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
5410
+ repeat: z25.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
5222
5411
  }).describe("One interaction to measure. Keep it to the thing you're asking about.");
5223
5412
  function selector2(needle) {
5224
5413
  return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
@@ -5311,14 +5500,14 @@ function registerRenderTools(server, sync) {
5311
5500
  {
5312
5501
  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.",
5313
5502
  inputSchema: {
5314
- steps: z23.array(stepSchema2).max(20).optional().describe(
5503
+ steps: z25.array(stepSchema2).max(20).optional().describe(
5315
5504
  "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."
5316
5505
  ),
5317
- idleMs: z23.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5318
- label: z23.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5319
- compareToPrevious: z23.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5320
- top: z23.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5321
- deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
5506
+ idleMs: z25.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
5507
+ label: z25.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
5508
+ compareToPrevious: z25.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
5509
+ top: z25.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
5510
+ deviceId: deviceIdInput
5322
5511
  }
5323
5512
  },
5324
5513
  async (args) => {
@@ -5327,18 +5516,18 @@ function registerRenderTools(server, sync) {
5327
5516
  const device = sync.connectedDevices().find((d) => d.deviceId === target);
5328
5517
  const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
5329
5518
  if (!uiTool) {
5330
- return fail23(
5519
+ return fail10(
5331
5520
  "This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
5332
5521
  );
5333
5522
  }
5334
5523
  if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
5335
- return fail23(
5524
+ return fail10(
5336
5525
  "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."
5337
5526
  );
5338
5527
  }
5339
5528
  const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
5340
5529
  if (began && began.ok === false) {
5341
- return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5530
+ return fail10(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
5342
5531
  }
5343
5532
  await delay5(SETTLE_MS);
5344
5533
  const startedAt = Date.now();
@@ -5383,7 +5572,7 @@ function registerRenderTools(server, sync) {
5383
5572
  const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
5384
5573
  const summary = ended?.summary;
5385
5574
  if (!summary) {
5386
- return fail23(
5575
+ return fail10(
5387
5576
  "The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
5388
5577
  );
5389
5578
  }
@@ -5408,9 +5597,9 @@ function registerRenderTools(server, sync) {
5408
5597
  } else if (!args.compareToPrevious) {
5409
5598
  parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
5410
5599
  }
5411
- return text21(parts.join("\n"));
5600
+ return text8(parts.join("\n"));
5412
5601
  } catch (e) {
5413
- return fail23(errMsg22(e));
5602
+ return fail10(errMsg9(e));
5414
5603
  }
5415
5604
  }
5416
5605
  );
@@ -5694,6 +5883,7 @@ function createMcpServer(sync) {
5694
5883
  registerStorageTools(server, sync);
5695
5884
  registerRoutesTools(server, sync);
5696
5885
  registerConsoleTools(server, sync);
5886
+ registerSentryTools(server, sync);
5697
5887
  registerTimeMachineTools(server, sync);
5698
5888
  registerScenariosTools(server, sync);
5699
5889
  registerScreenshotTools(server, sync);
@@ -5706,7 +5896,7 @@ function createMcpServer(sync) {
5706
5896
  return server;
5707
5897
  }
5708
5898
  var deviceIdField = {
5709
- deviceId: z24.string().optional().describe(
5899
+ deviceId: z26.string().optional().describe(
5710
5900
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
5711
5901
  )
5712
5902
  };