@buoy-gg/mcp 7.0.4 → 7.0.6

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.
@@ -296,7 +296,7 @@ async function waitForBroker(url, attempts = 20) {
296
296
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
297
297
 
298
298
  // src/version.ts
299
- var VERSION = "7.0.4" ? "7.0.4" : "0.0.0-dev";
299
+ var VERSION = "7.0.6" ? "7.0.6" : "0.0.0-dev";
300
300
  var PACKAGE_NAME = "@buoy-gg/mcp";
301
301
 
302
302
  // src/update/checkForUpdate.ts
@@ -1503,7 +1503,18 @@ var REDUX_TOOL_ID = "redux";
1503
1503
  var text6 = (s) => ({ content: [{ type: "text", text: s }] });
1504
1504
  var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1505
1505
  var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
1506
- var NO_STORE = "No instrumented Redux store found. Open the app with Buoy redux devtools active (auto-instrumentation), then retry.";
1506
+ function noStoreMessage(reason) {
1507
+ switch (reason) {
1508
+ case "no-react-redux":
1509
+ return "react-redux is not installed in the app \u2014 the Redux tool requires it (npm install react-redux).";
1510
+ case "no-provider":
1511
+ return "No Redux Provider above <FloatingDevTools />. Move <FloatingDevTools /> inside your <Provider store={store}> and retry.";
1512
+ case "not-instrumented":
1513
+ default:
1514
+ return "Redux store not instrumented yet. On current Buoy versions the store binds automatically at app startup \u2014 this usually means the app is running an older @buoy-gg/core (upgrade it), or the Redux tool hasn't been opened once (older versions only). Apps can also bind explicitly with registerReduxStore(store) from @buoy-gg/redux.";
1515
+ }
1516
+ }
1517
+ var TOP_LEVEL_ONLY_NOTE = "\n\n_Note: capture is top-level-only on this device (thunk-internal / RTK Query actions aren't recorded). Put `import '@buoy-gg/redux';` first in the app entry, or add `buoyReduxMiddleware`, for full capture._";
1507
1518
  function registerReduxTools(server, sync) {
1508
1519
  server.registerTool(
1509
1520
  "get_redux_state",
@@ -1528,19 +1539,22 @@ function registerReduxTools(server, sync) {
1528
1539
  "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)."
1529
1540
  );
1530
1541
  }
1531
- if (!res.available) return text6(NO_STORE);
1542
+ if (!res.available) return fail7(noStoreMessage(res.reason));
1532
1543
  const slices = res.slices.length ? res.slices.join(", ") : "(none)";
1544
+ const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
1533
1545
  if (args.includeValues) {
1534
- return text6(`**Redux store \u2014 slices: ${slices}**
1546
+ return text6(
1547
+ `**Redux store \u2014 slices: ${slices}**
1535
1548
 
1536
1549
  \`\`\`json
1537
1550
  ${JSON.stringify(res.state, null, 2)}
1538
- \`\`\``);
1551
+ \`\`\`${captureNote}`
1552
+ );
1539
1553
  }
1540
1554
  return text6(
1541
1555
  `**Redux store \u2014 slices: ${slices}**
1542
1556
 
1543
- _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._`
1557
+ _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
1544
1558
  );
1545
1559
  } catch (e) {
1546
1560
  return fail7(errMsg7(e));
@@ -1563,7 +1577,7 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
1563
1577
  const action = { type: args.type };
1564
1578
  if (args.payload !== void 0) action.payload = args.payload;
1565
1579
  const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
1566
- if (!res || !res.dispatched) return fail7(NO_STORE);
1580
+ if (!res || !res.dispatched) return fail7(noStoreMessage(res?.reason));
1567
1581
  return text6(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
1568
1582
  } catch (e) {
1569
1583
  return fail7(errMsg7(e));
@@ -2006,17 +2020,238 @@ function registerScreenshotTools(server, sync) {
2006
2020
 
2007
2021
  // src/server.ts
2008
2022
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2009
- import { z as z15 } from "zod";
2023
+ import { z as z17 } from "zod";
2010
2024
 
2011
- // src/tools/packs/interact.ts
2025
+ // src/tools/packs/network.ts
2012
2026
  import { z as z12 } from "zod";
2013
- var TOOL_ID2 = "highlight-updates";
2027
+ var NETWORK_TOOL_ID = "network";
2014
2028
  var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2015
- var fail12 = (message) => ({
2029
+ var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2030
+ var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2031
+ function parseSnapshot(payload) {
2032
+ if (Array.isArray(payload)) return { events: payload, saved: [] };
2033
+ if (payload && typeof payload === "object") {
2034
+ const shaped = payload;
2035
+ return {
2036
+ events: Array.isArray(shaped.events) ? shaped.events : [],
2037
+ saved: Array.isArray(shaped.saved) ? shaped.saved : []
2038
+ };
2039
+ }
2040
+ return { events: [], saved: [] };
2041
+ }
2042
+ function statusIcon(e) {
2043
+ if (e.error) return "\u274C";
2044
+ if (e.status === void 0) return "\u23F3";
2045
+ if (e.status >= 400) return "\u274C";
2046
+ if (e.status >= 300) return "\u21AA\uFE0F";
2047
+ return "\u2705";
2048
+ }
2049
+ function statusLabel(e) {
2050
+ if (e.error) return `ERR ${e.error}`;
2051
+ if (e.status === void 0) return "pending";
2052
+ return String(e.status);
2053
+ }
2054
+ function formatBytes(n) {
2055
+ if (!n) return "";
2056
+ if (n < 1024) return `${n}B`;
2057
+ if (n < 1024 * 1024) return `${Math.round(n / 1024)}KB`;
2058
+ return `${(n / (1024 * 1024)).toFixed(1)}MB`;
2059
+ }
2060
+ function matchesStatus(e, status) {
2061
+ if (!status || status === "all") return true;
2062
+ const failed = !!e.error || e.status !== void 0 && e.status >= 400;
2063
+ if (status === "errors") return failed;
2064
+ if (status === "pending") return e.status === void 0 && !e.error;
2065
+ if (status === "success") return !failed && e.status !== void 0;
2066
+ return true;
2067
+ }
2068
+ function truncate2(v, n = 600) {
2069
+ if (v === null || v === void 0) return "(none)";
2070
+ const s = typeof v === "string" ? v : JSON.stringify(v);
2071
+ return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
2072
+ }
2073
+ function formatRow(index, e, markers) {
2074
+ const where = e.path || e.url;
2075
+ const bits = [
2076
+ `${e.duration !== void 0 ? `${Math.round(e.duration)}ms` : "\u2014"}`,
2077
+ formatBytes(e.responseSize),
2078
+ e.host ?? "",
2079
+ e.requestClient ?? ""
2080
+ ].filter(Boolean);
2081
+ const op = e.operationName ? ` (${e.operationName})` : "";
2082
+ return `${index}. \`${e.id}\` ${statusIcon(e)} ${statusLabel(e)} ${e.method} ${where}${op}${markers ? ` ${markers}` : ""}
2083
+ \u2514\u2500 ${bits.join(" \xB7 ")}`;
2084
+ }
2085
+ function registerNetworkTools(server, sync) {
2086
+ server.registerTool(
2087
+ "get_network_requests",
2088
+ {
2089
+ description: "List captured HTTP requests WITH their request ids, plus which ones are pinned/saved. 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).",
2090
+ inputSchema: {
2091
+ limit: z12.number().optional().describe("Most-recent N requests. Default 25."),
2092
+ pattern: z12.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
2093
+ status: z12.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
2094
+ 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."),
2095
+ includeBodies: z12.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
2096
+ deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2097
+ }
2098
+ },
2099
+ async (args) => {
2100
+ try {
2101
+ const target = sync.requireProDevice(args.deviceId);
2102
+ const { payload } = await sync.getSnapshot(NETWORK_TOOL_ID, target);
2103
+ const { events, saved } = parseSnapshot(payload);
2104
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
2105
+ const pattern = args.pattern?.toLowerCase();
2106
+ const pinnedIds = /* @__PURE__ */ new Set();
2107
+ const savedIds = /* @__PURE__ */ new Set();
2108
+ for (const r of saved) {
2109
+ if (r.pinned) pinnedIds.add(r.event?.id);
2110
+ if (r.saved) savedIds.add(r.event?.id);
2111
+ if (r.pinned) pinnedIds.add(r.liveId);
2112
+ if (r.saved) savedIds.add(r.liveId);
2113
+ }
2114
+ if (args.flagged) {
2115
+ const wanted = saved.filter(
2116
+ (r) => args.flagged === "pinned" ? r.pinned : args.flagged === "saved" ? r.saved : r.pinned || r.saved
2117
+ );
2118
+ const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
2119
+ if (rows2.length === 0) {
2120
+ return text10(
2121
+ `No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
2122
+
2123
+ 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.`
2124
+ );
2125
+ }
2126
+ const body2 = rows2.map((r, i) => {
2127
+ const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
2128
+ const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
2129
+ const trunc = r.bodyTruncated ? " \xB7 body truncated" : "";
2130
+ let out = formatRow(i + 1, r.event, marks);
2131
+ out += `
2132
+ \u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
2133
+ if (args.includeBodies) {
2134
+ out += `
2135
+ \u2514\u2500 request: ${truncate2(r.event.requestData)}`;
2136
+ out += `
2137
+ \u2514\u2500 response: ${truncate2(r.event.responseData)}`;
2138
+ }
2139
+ return out;
2140
+ }).join("\n");
2141
+ return text10(
2142
+ `**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
2143
+
2144
+ ${body2}
2145
+
2146
+ _These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._`
2147
+ );
2148
+ }
2149
+ const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
2150
+ if (rows.length === 0) {
2151
+ const why = events.length === 0 ? "The network tool has captured nothing yet (is it enabled, and has the app made a request?)." : "No request matched the filter.";
2152
+ return text10(`No requests on ${target}. ${why}`);
2153
+ }
2154
+ const body = rows.map((e, i) => {
2155
+ const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
2156
+ let out = formatRow(i + 1, e, marks);
2157
+ if (args.includeBodies) {
2158
+ out += `
2159
+ \u2514\u2500 request: ${truncate2(e.requestData)}`;
2160
+ out += `
2161
+ \u2514\u2500 response: ${truncate2(e.responseData)}`;
2162
+ }
2163
+ return out;
2164
+ }).join("\n");
2165
+ const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
2166
+ return text10(
2167
+ `**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
2168
+
2169
+ ${body}
2170
+
2171
+ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
2172
+ );
2173
+ } catch (e) {
2174
+ return fail12(errMsg12(e));
2175
+ }
2176
+ }
2177
+ );
2178
+ server.registerTool(
2179
+ "network_action",
2180
+ {
2181
+ 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.",
2182
+ inputSchema: {
2183
+ 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)."),
2184
+ id: z12.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
2185
+ key: z12.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
2186
+ deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2187
+ }
2188
+ },
2189
+ async (args) => {
2190
+ try {
2191
+ const target = sync.requireProDevice(args.deviceId);
2192
+ const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
2193
+ if (needsId && !args.id) {
2194
+ return fail12(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
2195
+ }
2196
+ if (args.action === "remove" && !args.key) {
2197
+ return fail12('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
2198
+ }
2199
+ switch (args.action) {
2200
+ case "pin":
2201
+ case "unpin": {
2202
+ const res = await sync.callAction(
2203
+ NETWORK_TOOL_ID,
2204
+ "setPinned",
2205
+ { id: args.id, pinned: args.action === "pin" },
2206
+ target
2207
+ );
2208
+ 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.`);
2209
+ const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
2210
+ if (capped) return fail12(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
2211
+ return text10(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
2212
+ }
2213
+ case "save":
2214
+ case "unsave": {
2215
+ const res = await sync.callAction(
2216
+ NETWORK_TOOL_ID,
2217
+ "setSaved",
2218
+ { id: args.id, saved: args.action === "save" },
2219
+ target
2220
+ );
2221
+ 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.`);
2222
+ const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
2223
+ if (capped) return fail12(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
2224
+ return text10(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
2225
+ }
2226
+ case "remove":
2227
+ await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
2228
+ return text10(`Removed kept record \`${args.key}\` on ${target}.`);
2229
+ case "clearPinned":
2230
+ await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
2231
+ return text10(`Unpinned all requests on ${target} (saved ones kept).`);
2232
+ case "clearSaved":
2233
+ await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
2234
+ return text10(`Cleared saved requests on ${target} (pinned ones kept).`);
2235
+ case "clearRequests":
2236
+ await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
2237
+ return text10(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
2238
+ }
2239
+ } catch (e) {
2240
+ return fail12(errMsg12(e));
2241
+ }
2242
+ }
2243
+ );
2244
+ }
2245
+
2246
+ // src/tools/packs/interact.ts
2247
+ import { z as z13 } from "zod";
2248
+ var TOOL_ID2 = "highlight-updates";
2249
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2250
+ var fail13 = (message) => ({
2016
2251
  isError: true,
2017
2252
  content: [{ type: "text", text: message }]
2018
2253
  });
2019
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2254
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2020
2255
  function formatElement(e) {
2021
2256
  let line = e.interactive ? "\u2022 " : " ";
2022
2257
  line += e.name;
@@ -2024,6 +2259,7 @@ function formatElement(e) {
2024
2259
  if (display) line += ` "${display}"`;
2025
2260
  if (e.role) line += ` <${e.role}>`;
2026
2261
  if (e.control) line += ` {${e.control}${e.value !== void 0 ? `=${JSON.stringify(e.value)}` : ""}}`;
2262
+ if (e.longPressable) line += " {long-press}";
2027
2263
  if (e.testID) line += ` [testID=${e.testID}]`;
2028
2264
  if (e.tap) line += ` (tap: ${e.tap.x},${e.tap.y})`;
2029
2265
  if (e.nativeTag != null) line += ` #${e.nativeTag}`;
@@ -2035,8 +2271,8 @@ function registerInteractTools(server, sync) {
2035
2271
  {
2036
2272
  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. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
2037
2273
  inputSchema: {
2038
- interactiveOnly: z12.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2039
- deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2274
+ interactiveOnly: z13.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2275
+ deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
2040
2276
  }
2041
2277
  },
2042
2278
  async (args) => {
@@ -2046,37 +2282,38 @@ function registerInteractTools(server, sync) {
2046
2282
  const all = res?.elements ?? [];
2047
2283
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
2048
2284
  if (elements.length === 0) {
2049
- return text10(
2285
+ return text11(
2050
2286
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
2051
2287
  );
2052
2288
  }
2053
2289
  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 }).`;
2054
- return text10(`${header}
2290
+ return text11(`${header}
2055
2291
 
2056
2292
  ${elements.map(formatElement).join("\n")}`);
2057
2293
  } catch (e) {
2058
- return fail12(errMsg12(e));
2294
+ return fail13(errMsg13(e));
2059
2295
  }
2060
2296
  }
2061
2297
  );
2062
2298
  server.registerTool(
2063
2299
  "tap_element",
2064
2300
  {
2065
- 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), 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`. 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. Requires Buoy Pro.",
2301
+ 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. Requires Buoy Pro.",
2066
2302
  inputSchema: {
2067
- nativeTag: z12.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2068
- testID: z12.string().optional().describe("Exact testID of the element."),
2069
- query: z12.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2070
- value: z12.union([z12.number(), z12.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
2071
- text: z12.string().optional().describe("For a text input: the string to set via onChangeText."),
2072
- scrollIntoView: z12.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2073
- deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2303
+ nativeTag: z13.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2304
+ testID: z13.string().optional().describe("Exact testID of the element."),
2305
+ query: z13.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2306
+ value: z13.union([z13.number(), z13.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
2307
+ text: z13.string().optional().describe("For a text input: the string to set via onChangeText."),
2308
+ longPress: z13.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
2309
+ scrollIntoView: z13.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2310
+ deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
2074
2311
  }
2075
2312
  },
2076
2313
  async (args) => {
2077
2314
  try {
2078
2315
  if (args.nativeTag == null && !args.testID && !args.query) {
2079
- return fail12("Provide one of: nativeTag, testID, or query (from describe_screen).");
2316
+ return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
2080
2317
  }
2081
2318
  const target = sync.requireProDevice(args.deviceId);
2082
2319
  const res = await sync.callAction(
@@ -2088,6 +2325,7 @@ ${elements.map(formatElement).join("\n")}`);
2088
2325
  query: args.query,
2089
2326
  value: args.value,
2090
2327
  text: args.text,
2328
+ longPress: args.longPress,
2091
2329
  scrollIntoView: args.scrollIntoView
2092
2330
  },
2093
2331
  target
@@ -2097,27 +2335,27 @@ ${elements.map(formatElement).join("\n")}`);
2097
2335
  const label = m.text ?? m.label ?? m.name;
2098
2336
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
2099
2337
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
2100
- const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : `Tapped "${label}"${id}${m.via === "onLongPress" ? " (via onLongPress)" : ""}`;
2101
- return text10(`${verb}${scrolledNote}.`);
2338
+ 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}`;
2339
+ return text11(`${verb}${scrolledNote}.`);
2102
2340
  }
2103
2341
  let msg = res?.reason ?? "Tap failed.";
2104
2342
  if (res?.candidates?.length) {
2105
2343
  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");
2106
2344
  }
2107
- return fail12(msg);
2345
+ return fail13(msg);
2108
2346
  } catch (e) {
2109
- return fail12(errMsg12(e));
2347
+ return fail13(errMsg13(e));
2110
2348
  }
2111
2349
  }
2112
2350
  );
2113
2351
  }
2114
2352
 
2115
2353
  // src/tools/packs/jsThread.ts
2116
- import { z as z13 } from "zod";
2354
+ import { z as z14 } from "zod";
2117
2355
  var JS_TOP_TOOL_ID = "js-top";
2118
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2119
- var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2120
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2356
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2357
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2358
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2121
2359
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2122
2360
  function sparkline(values) {
2123
2361
  const max = Math.max(1, ...values);
@@ -2188,9 +2426,9 @@ function registerJsThreadTools(server, sync) {
2188
2426
  {
2189
2427
  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. Requires @buoy-gg/js-top on the device.",
2190
2428
  inputSchema: {
2191
- deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected)."),
2192
- durationMs: z13.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2193
- clearFirst: z13.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2429
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2430
+ durationMs: z14.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2431
+ clearFirst: z14.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2194
2432
  }
2195
2433
  },
2196
2434
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -2203,7 +2441,7 @@ function registerJsThreadTools(server, sync) {
2203
2441
  { durationMs: duration, clearFirst: clearFirst === true },
2204
2442
  target
2205
2443
  );
2206
- if (!snap) return fail13("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2444
+ if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2207
2445
  const md = formatTopMarkdown(snap, duration);
2208
2446
  const slim = {
2209
2447
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -2222,13 +2460,13 @@ function registerJsThreadTools(server, sync) {
2222
2460
  worstFreezeMs: r.worstFreezeMs
2223
2461
  }))
2224
2462
  };
2225
- return text11(`${md}
2463
+ return text12(`${md}
2226
2464
 
2227
2465
  \`\`\`json
2228
2466
  ${JSON.stringify(slim)}
2229
2467
  \`\`\``);
2230
2468
  } catch (e) {
2231
- return fail13(errMsg13(e));
2469
+ return fail14(errMsg14(e));
2232
2470
  }
2233
2471
  }
2234
2472
  );
@@ -2237,8 +2475,8 @@ ${JSON.stringify(slim)}
2237
2475
  {
2238
2476
  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).',
2239
2477
  inputSchema: {
2240
- deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected)."),
2241
- key: z13.string().describe("Origin key from get_js_thread_top's JSON rows.")
2478
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2479
+ key: z14.string().describe("Origin key from get_js_thread_top's JSON rows.")
2242
2480
  }
2243
2481
  },
2244
2482
  async ({ deviceId, key }) => {
@@ -2251,18 +2489,18 @@ ${JSON.stringify(slim)}
2251
2489
  target
2252
2490
  );
2253
2491
  if (!detail) {
2254
- return fail13(
2492
+ return fail14(
2255
2493
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2256
2494
  );
2257
2495
  }
2258
2496
  const md = formatDetailMarkdown(detail);
2259
- return text11(`${md}
2497
+ return text12(`${md}
2260
2498
 
2261
2499
  \`\`\`json
2262
2500
  ${JSON.stringify(detail)}
2263
2501
  \`\`\``);
2264
2502
  } catch (e) {
2265
- return fail13(errMsg13(e));
2503
+ return fail14(errMsg14(e));
2266
2504
  }
2267
2505
  }
2268
2506
  );
@@ -2271,27 +2509,27 @@ ${JSON.stringify(detail)}
2271
2509
  {
2272
2510
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2273
2511
  inputSchema: {
2274
- deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
2512
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
2275
2513
  }
2276
2514
  },
2277
2515
  async ({ deviceId }) => {
2278
2516
  try {
2279
2517
  const target = sync.requireProDevice(deviceId);
2280
2518
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2281
- return text11("Cleared js-top stats.");
2519
+ return text12("Cleared js-top stats.");
2282
2520
  } catch (e) {
2283
- return fail13(errMsg13(e));
2521
+ return fail14(errMsg14(e));
2284
2522
  }
2285
2523
  }
2286
2524
  );
2287
2525
  }
2288
2526
 
2289
2527
  // src/tools/packs/images.ts
2290
- import { z as z14 } from "zod";
2528
+ import { z as z15 } from "zod";
2291
2529
  var IMAGES_TOOL_ID = "images";
2292
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2293
- var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2294
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2530
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2531
+ var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2532
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
2295
2533
  function uriTail(uri) {
2296
2534
  if (!uri) return "(no source)";
2297
2535
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -2379,9 +2617,9 @@ function registerImagesTools(server, sync) {
2379
2617
  {
2380
2618
  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.",
2381
2619
  inputSchema: {
2382
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2383
- limit: z14.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2384
- status: z14.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2620
+ deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
2621
+ limit: z15.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2622
+ status: z15.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2385
2623
  }
2386
2624
  },
2387
2625
  async ({ deviceId, limit, status }) => {
@@ -2394,7 +2632,7 @@ function registerImagesTools(server, sync) {
2394
2632
  { limit: capped, status },
2395
2633
  target
2396
2634
  );
2397
- if (!list) return fail14("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2635
+ if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2398
2636
  const md = formatListMarkdown(list, capped);
2399
2637
  const slim = {
2400
2638
  stats: list.stats,
@@ -2419,13 +2657,13 @@ function registerImagesTools(server, sync) {
2419
2657
  loadCount: r.loadCount
2420
2658
  }))
2421
2659
  };
2422
- return text12(`${md}
2660
+ return text13(`${md}
2423
2661
 
2424
2662
  \`\`\`json
2425
2663
  ${JSON.stringify(slim)}
2426
2664
  \`\`\``);
2427
2665
  } catch (e) {
2428
- return fail14(errMsg14(e));
2666
+ return fail15(errMsg15(e));
2429
2667
  }
2430
2668
  }
2431
2669
  );
@@ -2434,9 +2672,9 @@ ${JSON.stringify(slim)}
2434
2672
  {
2435
2673
  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.",
2436
2674
  inputSchema: {
2437
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2438
- id: z14.number().describe("Record id from get_images."),
2439
- action: z14.enum([
2675
+ deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
2676
+ id: z15.number().describe("Record id from get_images."),
2677
+ action: z15.enum([
2440
2678
  "hard_reload",
2441
2679
  "retry",
2442
2680
  "evict_disk",
@@ -2448,7 +2686,7 @@ ${JSON.stringify(slim)}
2448
2686
  "swap_url",
2449
2687
  "restore"
2450
2688
  ]).describe("What to do."),
2451
- url: z14.string().optional().describe("Replacement URL (swap_url only).")
2689
+ url: z15.string().optional().describe("Replacement URL (swap_url only).")
2452
2690
  }
2453
2691
  },
2454
2692
  async ({ deviceId, id, action, url }) => {
@@ -2456,8 +2694,8 @@ ${JSON.stringify(slim)}
2456
2694
  const target = sync.requireProDevice(deviceId);
2457
2695
  if (action === "prove_savings") {
2458
2696
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2459
- if (!savings) return fail14("Device returned nothing for proveSavings.");
2460
- if (!savings.ok) return fail14(savings.message);
2697
+ if (!savings) return fail15("Device returned nothing for proveSavings.");
2698
+ if (!savings.ok) return fail15(savings.message);
2461
2699
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2462
2700
  if (savings.originalBytes != null)
2463
2701
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -2469,11 +2707,11 @@ ${JSON.stringify(slim)}
2469
2707
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
2470
2708
  if (savings.optimizedUri)
2471
2709
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
2472
- return text12(parts.join("\n"));
2710
+ return text13(parts.join("\n"));
2473
2711
  }
2474
2712
  const entry = ACTION_MAP[action];
2475
- if (!entry) return fail14(`Unknown action: ${action}`);
2476
- if (action === "swap_url" && !url) return fail14("swap_url requires the `url` param.");
2713
+ if (!entry) return fail15(`Unknown action: ${action}`);
2714
+ if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
2477
2715
  const result = await sync.callAction(
2478
2716
  IMAGES_TOOL_ID,
2479
2717
  entry.action,
@@ -2493,9 +2731,9 @@ ${JSON.stringify(slim)}
2493
2731
  );
2494
2732
  if (detail.error) lines.push(`Error: ${detail.error}`);
2495
2733
  }
2496
- return text12(lines.join("\n"));
2734
+ return text13(lines.join("\n"));
2497
2735
  } catch (e) {
2498
- return fail14(errMsg14(e));
2736
+ return fail15(errMsg15(e));
2499
2737
  }
2500
2738
  }
2501
2739
  );
@@ -2504,12 +2742,12 @@ ${JSON.stringify(slim)}
2504
2742
  {
2505
2743
  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.",
2506
2744
  inputSchema: {
2507
- deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2508
- network: z14.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2509
- blankImages: z14.boolean().optional().describe("Render all images without a source."),
2510
- massAction: z14.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."),
2511
- clearRegistry: z14.boolean().optional().describe("Clear the captured image log."),
2512
- clearExpoCaches: z14.boolean().optional().describe("Clear expo-image memory + disk caches.")
2745
+ deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
2746
+ network: z15.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2747
+ blankImages: z15.boolean().optional().describe("Render all images without a source."),
2748
+ massAction: z15.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."),
2749
+ clearRegistry: z15.boolean().optional().describe("Clear the captured image log."),
2750
+ clearExpoCaches: z15.boolean().optional().describe("Clear expo-image memory + disk caches.")
2513
2751
  }
2514
2752
  },
2515
2753
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -2536,10 +2774,94 @@ ${JSON.stringify(slim)}
2536
2774
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
2537
2775
  done.push(r?.message ?? "expo caches cleared");
2538
2776
  }
2539
- if (done.length === 0) return fail14("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2540
- return text12(`Applied: ${done.join(" \xB7 ")}`);
2777
+ if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2778
+ return text13(`Applied: ${done.join(" \xB7 ")}`);
2541
2779
  } catch (e) {
2542
- return fail14(errMsg14(e));
2780
+ return fail15(errMsg15(e));
2781
+ }
2782
+ }
2783
+ );
2784
+ }
2785
+
2786
+ // src/tools/packs/reload.ts
2787
+ import { z as z16 } from "zod";
2788
+ var APP_TOOL_ID = "app";
2789
+ var RELOAD_ACTION = "reloadApp";
2790
+ var DEFAULT_WAIT_MS = 3e4;
2791
+ var TEARDOWN_GRACE_MS = 1200;
2792
+ var PROBE_TIMEOUT_MS = 1500;
2793
+ var PROBE_INTERVAL_MS = 400;
2794
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
2795
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2796
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
2797
+ var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
2798
+ async function ping(sync, deviceId, timeoutMs) {
2799
+ try {
2800
+ const pong = await sync.callAction(APP_TOOL_ID, "ping", {}, deviceId, { timeoutMs });
2801
+ return typeof pong?.startedAt === "number" ? pong.startedAt : null;
2802
+ } catch {
2803
+ return null;
2804
+ }
2805
+ }
2806
+ function assertReloadSupported(sync, deviceId) {
2807
+ if (deviceId === ALL_DEVICES) return;
2808
+ const entry = sync.toolActions(APP_TOOL_ID).find((t) => t.deviceId === deviceId);
2809
+ if (!entry?.actions.includes(RELOAD_ACTION)) {
2810
+ throw new Error(
2811
+ `This app doesn't expose remote reload \u2014 its @buoy-gg/core is too old. Upgrade @buoy-gg/core, then reload the app once by hand.`
2812
+ );
2813
+ }
2814
+ }
2815
+ async function waitForAppBack(sync, deviceId, timeoutMs, previousRealm) {
2816
+ const waitStart = Date.now();
2817
+ await delay3(TEARDOWN_GRACE_MS);
2818
+ while (Date.now() - waitStart < timeoutMs) {
2819
+ const realm = await ping(sync, deviceId, PROBE_TIMEOUT_MS);
2820
+ if (realm !== null && realm !== previousRealm) return Date.now() - waitStart;
2821
+ await delay3(PROBE_INTERVAL_MS);
2822
+ }
2823
+ return null;
2824
+ }
2825
+ function registerReloadTools(server, sync) {
2826
+ server.registerTool(
2827
+ "reload_app",
2828
+ {
2829
+ 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, or to re-run app startup. 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. 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.",
2830
+ inputSchema: {
2831
+ strategy: z16.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
2832
+ "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
2833
+ ),
2834
+ wait: z16.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
2835
+ timeoutMs: z16.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
2836
+ deviceId: z16.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
2837
+ }
2838
+ },
2839
+ async (args) => {
2840
+ try {
2841
+ const target = sync.requireProDevice(args.deviceId);
2842
+ assertReloadSupported(sync, target);
2843
+ const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
2844
+ const ack = await sync.callAction(
2845
+ APP_TOOL_ID,
2846
+ RELOAD_ACTION,
2847
+ { strategy: args.strategy ?? "auto" },
2848
+ target
2849
+ );
2850
+ const via = ack?.strategy ? ` via ${ack.strategy}` : "";
2851
+ if (args.wait === false) {
2852
+ return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
2853
+ }
2854
+ const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
2855
+ if (elapsed == null) {
2856
+ return fail16(
2857
+ `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.`
2858
+ );
2859
+ }
2860
+ return text14(
2861
+ `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.`
2862
+ );
2863
+ } catch (e) {
2864
+ return fail16(errMsg16(e));
2543
2865
  }
2544
2866
  }
2545
2867
  );
@@ -2558,6 +2880,13 @@ filter (e.g. sources:["network"] to list HTTP requests). It's the universal, tok
2558
2880
  activity reader across ALL tools and is compact by default \u2014 reach for it before reading source
2559
2881
  code, and prefer it over \`get_snapshot\` for any "what happened / show me the requests" question.
2560
2882
 
2883
+ Working on ONE HTTP request rather than skimming activity? \`get_events\` deliberately emits no
2884
+ request ids, so use \`get_network_requests\` \u2014 same compact style, but it leads with the id every
2885
+ network action is keyed by, and marks which requests are pinned/saved. \`flagged:'any'\` lists the
2886
+ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-request cap and app
2887
+ restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
2888
+ pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
2889
+
2561
2890
  Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
2562
2891
  a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
2563
2892
  iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
@@ -2576,6 +2905,11 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
2576
2905
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
2577
2906
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
2578
2907
 
2908
+ Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
2909
+ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
2910
+ builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
2911
+ re-read anything you cached.
2912
+
2579
2913
  For everything else:
2580
2914
  - \`list_tool_actions\` to see what a tool can do,
2581
2915
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -2595,6 +2929,7 @@ function createMcpServer(sync) {
2595
2929
  registerGenericTools(server, sync);
2596
2930
  registerPerfMonitorTools(server, sync);
2597
2931
  registerEventsTools(server, sync);
2932
+ registerNetworkTools(server, sync);
2598
2933
  registerReactQueryTools(server, sync);
2599
2934
  registerZustandTools(server, sync);
2600
2935
  registerJotaiTools(server, sync);
@@ -2606,10 +2941,11 @@ function createMcpServer(sync) {
2606
2941
  registerInteractTools(server, sync);
2607
2942
  registerJsThreadTools(server, sync);
2608
2943
  registerImagesTools(server, sync);
2944
+ registerReloadTools(server, sync);
2609
2945
  return server;
2610
2946
  }
2611
2947
  var deviceIdField = {
2612
- deviceId: z15.string().optional().describe(
2948
+ deviceId: z17.string().optional().describe(
2613
2949
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
2614
2950
  )
2615
2951
  };
@@ -2620,7 +2956,7 @@ import fs3 from "fs";
2620
2956
  import os3 from "os";
2621
2957
  import path3 from "path";
2622
2958
  var PUBLIC_REGISTRY = "https://registry.npmjs.org";
2623
- var PROBE_TIMEOUT_MS = 3e3;
2959
+ var PROBE_TIMEOUT_MS2 = 3e3;
2624
2960
  function runInit(opts = {}) {
2625
2961
  const cwd = opts.cwd ?? process.cwd();
2626
2962
  const skillSrcDir = opts.skillSrcDir ?? path3.resolve(__dirname, "../skill");
@@ -2733,7 +3069,7 @@ function effectiveRegistry() {
2733
3069
  }
2734
3070
  function npmConfigGet(key) {
2735
3071
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2736
- const res = spawnSync(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
3072
+ const res = spawnSync(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 });
2737
3073
  if (res.status !== 0 || typeof res.stdout !== "string") return null;
2738
3074
  const val = res.stdout.trim();
2739
3075
  if (!val || val === "undefined" || val === "null") return null;
@@ -2742,8 +3078,8 @@ function npmConfigGet(key) {
2742
3078
  function hostReachable(registry) {
2743
3079
  const done = spawnSync(
2744
3080
  process.execPath,
2745
- ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS)],
2746
- { encoding: "utf8", timeout: PROBE_TIMEOUT_MS + 2e3 }
3081
+ ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS2)],
3082
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 + 2e3 }
2747
3083
  );
2748
3084
  return done.status === 0;
2749
3085
  }