@buoy-gg/mcp 7.0.3 → 7.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/{chunk-MGZN6MJE.mjs → chunk-KZFJFXI4.mjs} +514 -75
- package/dist/cli.js +648 -203
- package/dist/cli.mjs +8 -2
- package/dist/index.d.mts +45 -4
- package/dist/index.d.ts +45 -4
- package/dist/index.js +641 -202
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
299
|
+
var VERSION = "7.0.5" ? "7.0.5" : "0.0.0-dev";
|
|
300
300
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
301
301
|
|
|
302
302
|
// src/update/checkForUpdate.ts
|
|
@@ -2006,17 +2006,238 @@ function registerScreenshotTools(server, sync) {
|
|
|
2006
2006
|
|
|
2007
2007
|
// src/server.ts
|
|
2008
2008
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2009
|
-
import { z as
|
|
2009
|
+
import { z as z17 } from "zod";
|
|
2010
2010
|
|
|
2011
|
-
// src/tools/packs/
|
|
2011
|
+
// src/tools/packs/network.ts
|
|
2012
2012
|
import { z as z12 } from "zod";
|
|
2013
|
-
var
|
|
2013
|
+
var NETWORK_TOOL_ID = "network";
|
|
2014
2014
|
var text10 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2015
|
-
var fail12 = (message) => ({
|
|
2015
|
+
var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2016
|
+
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2017
|
+
function parseSnapshot(payload) {
|
|
2018
|
+
if (Array.isArray(payload)) return { events: payload, saved: [] };
|
|
2019
|
+
if (payload && typeof payload === "object") {
|
|
2020
|
+
const shaped = payload;
|
|
2021
|
+
return {
|
|
2022
|
+
events: Array.isArray(shaped.events) ? shaped.events : [],
|
|
2023
|
+
saved: Array.isArray(shaped.saved) ? shaped.saved : []
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
return { events: [], saved: [] };
|
|
2027
|
+
}
|
|
2028
|
+
function statusIcon(e) {
|
|
2029
|
+
if (e.error) return "\u274C";
|
|
2030
|
+
if (e.status === void 0) return "\u23F3";
|
|
2031
|
+
if (e.status >= 400) return "\u274C";
|
|
2032
|
+
if (e.status >= 300) return "\u21AA\uFE0F";
|
|
2033
|
+
return "\u2705";
|
|
2034
|
+
}
|
|
2035
|
+
function statusLabel(e) {
|
|
2036
|
+
if (e.error) return `ERR ${e.error}`;
|
|
2037
|
+
if (e.status === void 0) return "pending";
|
|
2038
|
+
return String(e.status);
|
|
2039
|
+
}
|
|
2040
|
+
function formatBytes(n) {
|
|
2041
|
+
if (!n) return "";
|
|
2042
|
+
if (n < 1024) return `${n}B`;
|
|
2043
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)}KB`;
|
|
2044
|
+
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
2045
|
+
}
|
|
2046
|
+
function matchesStatus(e, status) {
|
|
2047
|
+
if (!status || status === "all") return true;
|
|
2048
|
+
const failed = !!e.error || e.status !== void 0 && e.status >= 400;
|
|
2049
|
+
if (status === "errors") return failed;
|
|
2050
|
+
if (status === "pending") return e.status === void 0 && !e.error;
|
|
2051
|
+
if (status === "success") return !failed && e.status !== void 0;
|
|
2052
|
+
return true;
|
|
2053
|
+
}
|
|
2054
|
+
function truncate2(v, n = 600) {
|
|
2055
|
+
if (v === null || v === void 0) return "(none)";
|
|
2056
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
2057
|
+
return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
|
|
2058
|
+
}
|
|
2059
|
+
function formatRow(index, e, markers) {
|
|
2060
|
+
const where = e.path || e.url;
|
|
2061
|
+
const bits = [
|
|
2062
|
+
`${e.duration !== void 0 ? `${Math.round(e.duration)}ms` : "\u2014"}`,
|
|
2063
|
+
formatBytes(e.responseSize),
|
|
2064
|
+
e.host ?? "",
|
|
2065
|
+
e.requestClient ?? ""
|
|
2066
|
+
].filter(Boolean);
|
|
2067
|
+
const op = e.operationName ? ` (${e.operationName})` : "";
|
|
2068
|
+
return `${index}. \`${e.id}\` ${statusIcon(e)} ${statusLabel(e)} ${e.method} ${where}${op}${markers ? ` ${markers}` : ""}
|
|
2069
|
+
\u2514\u2500 ${bits.join(" \xB7 ")}`;
|
|
2070
|
+
}
|
|
2071
|
+
function registerNetworkTools(server, sync) {
|
|
2072
|
+
server.registerTool(
|
|
2073
|
+
"get_network_requests",
|
|
2074
|
+
{
|
|
2075
|
+
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).",
|
|
2076
|
+
inputSchema: {
|
|
2077
|
+
limit: z12.number().optional().describe("Most-recent N requests. Default 25."),
|
|
2078
|
+
pattern: z12.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
2079
|
+
status: z12.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
2080
|
+
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."),
|
|
2081
|
+
includeBodies: z12.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
2082
|
+
deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2083
|
+
}
|
|
2084
|
+
},
|
|
2085
|
+
async (args) => {
|
|
2086
|
+
try {
|
|
2087
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2088
|
+
const { payload } = await sync.getSnapshot(NETWORK_TOOL_ID, target);
|
|
2089
|
+
const { events, saved } = parseSnapshot(payload);
|
|
2090
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
|
|
2091
|
+
const pattern = args.pattern?.toLowerCase();
|
|
2092
|
+
const pinnedIds = /* @__PURE__ */ new Set();
|
|
2093
|
+
const savedIds = /* @__PURE__ */ new Set();
|
|
2094
|
+
for (const r of saved) {
|
|
2095
|
+
if (r.pinned) pinnedIds.add(r.event?.id);
|
|
2096
|
+
if (r.saved) savedIds.add(r.event?.id);
|
|
2097
|
+
if (r.pinned) pinnedIds.add(r.liveId);
|
|
2098
|
+
if (r.saved) savedIds.add(r.liveId);
|
|
2099
|
+
}
|
|
2100
|
+
if (args.flagged) {
|
|
2101
|
+
const wanted = saved.filter(
|
|
2102
|
+
(r) => args.flagged === "pinned" ? r.pinned : args.flagged === "saved" ? r.saved : r.pinned || r.saved
|
|
2103
|
+
);
|
|
2104
|
+
const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
2105
|
+
if (rows2.length === 0) {
|
|
2106
|
+
return text10(
|
|
2107
|
+
`No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
|
|
2108
|
+
|
|
2109
|
+
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.`
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
const body2 = rows2.map((r, i) => {
|
|
2113
|
+
const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
|
|
2114
|
+
const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
|
|
2115
|
+
const trunc = r.bodyTruncated ? " \xB7 body truncated" : "";
|
|
2116
|
+
let out = formatRow(i + 1, r.event, marks);
|
|
2117
|
+
out += `
|
|
2118
|
+
\u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
|
|
2119
|
+
if (args.includeBodies) {
|
|
2120
|
+
out += `
|
|
2121
|
+
\u2514\u2500 request: ${truncate2(r.event.requestData)}`;
|
|
2122
|
+
out += `
|
|
2123
|
+
\u2514\u2500 response: ${truncate2(r.event.responseData)}`;
|
|
2124
|
+
}
|
|
2125
|
+
return out;
|
|
2126
|
+
}).join("\n");
|
|
2127
|
+
return text10(
|
|
2128
|
+
`**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
|
|
2129
|
+
|
|
2130
|
+
${body2}
|
|
2131
|
+
|
|
2132
|
+
_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._`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
2136
|
+
if (rows.length === 0) {
|
|
2137
|
+
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.";
|
|
2138
|
+
return text10(`No requests on ${target}. ${why}`);
|
|
2139
|
+
}
|
|
2140
|
+
const body = rows.map((e, i) => {
|
|
2141
|
+
const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
|
|
2142
|
+
let out = formatRow(i + 1, e, marks);
|
|
2143
|
+
if (args.includeBodies) {
|
|
2144
|
+
out += `
|
|
2145
|
+
\u2514\u2500 request: ${truncate2(e.requestData)}`;
|
|
2146
|
+
out += `
|
|
2147
|
+
\u2514\u2500 response: ${truncate2(e.responseData)}`;
|
|
2148
|
+
}
|
|
2149
|
+
return out;
|
|
2150
|
+
}).join("\n");
|
|
2151
|
+
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
2152
|
+
return text10(
|
|
2153
|
+
`**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
|
|
2154
|
+
|
|
2155
|
+
${body}
|
|
2156
|
+
|
|
2157
|
+
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
|
|
2158
|
+
);
|
|
2159
|
+
} catch (e) {
|
|
2160
|
+
return fail12(errMsg12(e));
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
server.registerTool(
|
|
2165
|
+
"network_action",
|
|
2166
|
+
{
|
|
2167
|
+
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.",
|
|
2168
|
+
inputSchema: {
|
|
2169
|
+
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)."),
|
|
2170
|
+
id: z12.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
2171
|
+
key: z12.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
|
|
2172
|
+
deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2173
|
+
}
|
|
2174
|
+
},
|
|
2175
|
+
async (args) => {
|
|
2176
|
+
try {
|
|
2177
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2178
|
+
const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
|
|
2179
|
+
if (needsId && !args.id) {
|
|
2180
|
+
return fail12(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
|
|
2181
|
+
}
|
|
2182
|
+
if (args.action === "remove" && !args.key) {
|
|
2183
|
+
return fail12('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
|
|
2184
|
+
}
|
|
2185
|
+
switch (args.action) {
|
|
2186
|
+
case "pin":
|
|
2187
|
+
case "unpin": {
|
|
2188
|
+
const res = await sync.callAction(
|
|
2189
|
+
NETWORK_TOOL_ID,
|
|
2190
|
+
"setPinned",
|
|
2191
|
+
{ id: args.id, pinned: args.action === "pin" },
|
|
2192
|
+
target
|
|
2193
|
+
);
|
|
2194
|
+
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.`);
|
|
2195
|
+
const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
|
|
2196
|
+
if (capped) return fail12(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
|
|
2197
|
+
return text10(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
|
|
2198
|
+
}
|
|
2199
|
+
case "save":
|
|
2200
|
+
case "unsave": {
|
|
2201
|
+
const res = await sync.callAction(
|
|
2202
|
+
NETWORK_TOOL_ID,
|
|
2203
|
+
"setSaved",
|
|
2204
|
+
{ id: args.id, saved: args.action === "save" },
|
|
2205
|
+
target
|
|
2206
|
+
);
|
|
2207
|
+
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.`);
|
|
2208
|
+
const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
|
|
2209
|
+
if (capped) return fail12(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
|
|
2210
|
+
return text10(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
|
|
2211
|
+
}
|
|
2212
|
+
case "remove":
|
|
2213
|
+
await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
|
|
2214
|
+
return text10(`Removed kept record \`${args.key}\` on ${target}.`);
|
|
2215
|
+
case "clearPinned":
|
|
2216
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
|
|
2217
|
+
return text10(`Unpinned all requests on ${target} (saved ones kept).`);
|
|
2218
|
+
case "clearSaved":
|
|
2219
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
|
|
2220
|
+
return text10(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
2221
|
+
case "clearRequests":
|
|
2222
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
2223
|
+
return text10(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
|
|
2224
|
+
}
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
return fail12(errMsg12(e));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/tools/packs/interact.ts
|
|
2233
|
+
import { z as z13 } from "zod";
|
|
2234
|
+
var TOOL_ID2 = "highlight-updates";
|
|
2235
|
+
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2236
|
+
var fail13 = (message) => ({
|
|
2016
2237
|
isError: true,
|
|
2017
2238
|
content: [{ type: "text", text: message }]
|
|
2018
2239
|
});
|
|
2019
|
-
var
|
|
2240
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2020
2241
|
function formatElement(e) {
|
|
2021
2242
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2022
2243
|
line += e.name;
|
|
@@ -2024,6 +2245,7 @@ function formatElement(e) {
|
|
|
2024
2245
|
if (display) line += ` "${display}"`;
|
|
2025
2246
|
if (e.role) line += ` <${e.role}>`;
|
|
2026
2247
|
if (e.control) line += ` {${e.control}${e.value !== void 0 ? `=${JSON.stringify(e.value)}` : ""}}`;
|
|
2248
|
+
if (e.longPressable) line += " {long-press}";
|
|
2027
2249
|
if (e.testID) line += ` [testID=${e.testID}]`;
|
|
2028
2250
|
if (e.tap) line += ` (tap: ${e.tap.x},${e.tap.y})`;
|
|
2029
2251
|
if (e.nativeTag != null) line += ` #${e.nativeTag}`;
|
|
@@ -2035,8 +2257,8 @@ function registerInteractTools(server, sync) {
|
|
|
2035
2257
|
{
|
|
2036
2258
|
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
2259
|
inputSchema: {
|
|
2038
|
-
interactiveOnly:
|
|
2039
|
-
deviceId:
|
|
2260
|
+
interactiveOnly: z13.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
2261
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2040
2262
|
}
|
|
2041
2263
|
},
|
|
2042
2264
|
async (args) => {
|
|
@@ -2046,37 +2268,38 @@ function registerInteractTools(server, sync) {
|
|
|
2046
2268
|
const all = res?.elements ?? [];
|
|
2047
2269
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2048
2270
|
if (elements.length === 0) {
|
|
2049
|
-
return
|
|
2271
|
+
return text11(
|
|
2050
2272
|
"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
2273
|
);
|
|
2052
2274
|
}
|
|
2053
2275
|
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
|
|
2276
|
+
return text11(`${header}
|
|
2055
2277
|
|
|
2056
2278
|
${elements.map(formatElement).join("\n")}`);
|
|
2057
2279
|
} catch (e) {
|
|
2058
|
-
return
|
|
2280
|
+
return fail13(errMsg13(e));
|
|
2059
2281
|
}
|
|
2060
2282
|
}
|
|
2061
2283
|
);
|
|
2062
2284
|
server.registerTool(
|
|
2063
2285
|
"tap_element",
|
|
2064
2286
|
{
|
|
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.",
|
|
2287
|
+
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
2288
|
inputSchema: {
|
|
2067
|
-
nativeTag:
|
|
2068
|
-
testID:
|
|
2069
|
-
query:
|
|
2070
|
-
value:
|
|
2071
|
-
text:
|
|
2072
|
-
|
|
2073
|
-
|
|
2289
|
+
nativeTag: z13.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2290
|
+
testID: z13.string().optional().describe("Exact testID of the element."),
|
|
2291
|
+
query: z13.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2292
|
+
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)."),
|
|
2293
|
+
text: z13.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2294
|
+
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."),
|
|
2295
|
+
scrollIntoView: z13.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2296
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2074
2297
|
}
|
|
2075
2298
|
},
|
|
2076
2299
|
async (args) => {
|
|
2077
2300
|
try {
|
|
2078
2301
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2079
|
-
return
|
|
2302
|
+
return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2080
2303
|
}
|
|
2081
2304
|
const target = sync.requireProDevice(args.deviceId);
|
|
2082
2305
|
const res = await sync.callAction(
|
|
@@ -2088,6 +2311,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2088
2311
|
query: args.query,
|
|
2089
2312
|
value: args.value,
|
|
2090
2313
|
text: args.text,
|
|
2314
|
+
longPress: args.longPress,
|
|
2091
2315
|
scrollIntoView: args.scrollIntoView
|
|
2092
2316
|
},
|
|
2093
2317
|
target
|
|
@@ -2097,27 +2321,27 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2097
2321
|
const label = m.text ?? m.label ?? m.name;
|
|
2098
2322
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2099
2323
|
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}` :
|
|
2101
|
-
return
|
|
2324
|
+
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}`;
|
|
2325
|
+
return text11(`${verb}${scrolledNote}.`);
|
|
2102
2326
|
}
|
|
2103
2327
|
let msg = res?.reason ?? "Tap failed.";
|
|
2104
2328
|
if (res?.candidates?.length) {
|
|
2105
2329
|
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
2330
|
}
|
|
2107
|
-
return
|
|
2331
|
+
return fail13(msg);
|
|
2108
2332
|
} catch (e) {
|
|
2109
|
-
return
|
|
2333
|
+
return fail13(errMsg13(e));
|
|
2110
2334
|
}
|
|
2111
2335
|
}
|
|
2112
2336
|
);
|
|
2113
2337
|
}
|
|
2114
2338
|
|
|
2115
2339
|
// src/tools/packs/jsThread.ts
|
|
2116
|
-
import { z as
|
|
2340
|
+
import { z as z14 } from "zod";
|
|
2117
2341
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2118
|
-
var
|
|
2119
|
-
var
|
|
2120
|
-
var
|
|
2342
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2343
|
+
var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2344
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2121
2345
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2122
2346
|
function sparkline(values) {
|
|
2123
2347
|
const max = Math.max(1, ...values);
|
|
@@ -2188,9 +2412,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2188
2412
|
{
|
|
2189
2413
|
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
2414
|
inputSchema: {
|
|
2191
|
-
deviceId:
|
|
2192
|
-
durationMs:
|
|
2193
|
-
clearFirst:
|
|
2415
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2416
|
+
durationMs: z14.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2417
|
+
clearFirst: z14.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2194
2418
|
}
|
|
2195
2419
|
},
|
|
2196
2420
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2203,7 +2427,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2203
2427
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2204
2428
|
target
|
|
2205
2429
|
);
|
|
2206
|
-
if (!snap) return
|
|
2430
|
+
if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2207
2431
|
const md = formatTopMarkdown(snap, duration);
|
|
2208
2432
|
const slim = {
|
|
2209
2433
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2222,13 +2446,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2222
2446
|
worstFreezeMs: r.worstFreezeMs
|
|
2223
2447
|
}))
|
|
2224
2448
|
};
|
|
2225
|
-
return
|
|
2449
|
+
return text12(`${md}
|
|
2226
2450
|
|
|
2227
2451
|
\`\`\`json
|
|
2228
2452
|
${JSON.stringify(slim)}
|
|
2229
2453
|
\`\`\``);
|
|
2230
2454
|
} catch (e) {
|
|
2231
|
-
return
|
|
2455
|
+
return fail14(errMsg14(e));
|
|
2232
2456
|
}
|
|
2233
2457
|
}
|
|
2234
2458
|
);
|
|
@@ -2237,8 +2461,8 @@ ${JSON.stringify(slim)}
|
|
|
2237
2461
|
{
|
|
2238
2462
|
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
2463
|
inputSchema: {
|
|
2240
|
-
deviceId:
|
|
2241
|
-
key:
|
|
2464
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2465
|
+
key: z14.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2242
2466
|
}
|
|
2243
2467
|
},
|
|
2244
2468
|
async ({ deviceId, key }) => {
|
|
@@ -2251,18 +2475,18 @@ ${JSON.stringify(slim)}
|
|
|
2251
2475
|
target
|
|
2252
2476
|
);
|
|
2253
2477
|
if (!detail) {
|
|
2254
|
-
return
|
|
2478
|
+
return fail14(
|
|
2255
2479
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2256
2480
|
);
|
|
2257
2481
|
}
|
|
2258
2482
|
const md = formatDetailMarkdown(detail);
|
|
2259
|
-
return
|
|
2483
|
+
return text12(`${md}
|
|
2260
2484
|
|
|
2261
2485
|
\`\`\`json
|
|
2262
2486
|
${JSON.stringify(detail)}
|
|
2263
2487
|
\`\`\``);
|
|
2264
2488
|
} catch (e) {
|
|
2265
|
-
return
|
|
2489
|
+
return fail14(errMsg14(e));
|
|
2266
2490
|
}
|
|
2267
2491
|
}
|
|
2268
2492
|
);
|
|
@@ -2271,27 +2495,27 @@ ${JSON.stringify(detail)}
|
|
|
2271
2495
|
{
|
|
2272
2496
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2273
2497
|
inputSchema: {
|
|
2274
|
-
deviceId:
|
|
2498
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2275
2499
|
}
|
|
2276
2500
|
},
|
|
2277
2501
|
async ({ deviceId }) => {
|
|
2278
2502
|
try {
|
|
2279
2503
|
const target = sync.requireProDevice(deviceId);
|
|
2280
2504
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2281
|
-
return
|
|
2505
|
+
return text12("Cleared js-top stats.");
|
|
2282
2506
|
} catch (e) {
|
|
2283
|
-
return
|
|
2507
|
+
return fail14(errMsg14(e));
|
|
2284
2508
|
}
|
|
2285
2509
|
}
|
|
2286
2510
|
);
|
|
2287
2511
|
}
|
|
2288
2512
|
|
|
2289
2513
|
// src/tools/packs/images.ts
|
|
2290
|
-
import { z as
|
|
2514
|
+
import { z as z15 } from "zod";
|
|
2291
2515
|
var IMAGES_TOOL_ID = "images";
|
|
2292
|
-
var
|
|
2293
|
-
var
|
|
2294
|
-
var
|
|
2516
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2517
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2518
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2295
2519
|
function uriTail(uri) {
|
|
2296
2520
|
if (!uri) return "(no source)";
|
|
2297
2521
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2379,9 +2603,9 @@ function registerImagesTools(server, sync) {
|
|
|
2379
2603
|
{
|
|
2380
2604
|
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
2605
|
inputSchema: {
|
|
2382
|
-
deviceId:
|
|
2383
|
-
limit:
|
|
2384
|
-
status:
|
|
2606
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2607
|
+
limit: z15.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
2608
|
+
status: z15.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2385
2609
|
}
|
|
2386
2610
|
},
|
|
2387
2611
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2394,7 +2618,7 @@ function registerImagesTools(server, sync) {
|
|
|
2394
2618
|
{ limit: capped, status },
|
|
2395
2619
|
target
|
|
2396
2620
|
);
|
|
2397
|
-
if (!list) return
|
|
2621
|
+
if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2398
2622
|
const md = formatListMarkdown(list, capped);
|
|
2399
2623
|
const slim = {
|
|
2400
2624
|
stats: list.stats,
|
|
@@ -2419,13 +2643,13 @@ function registerImagesTools(server, sync) {
|
|
|
2419
2643
|
loadCount: r.loadCount
|
|
2420
2644
|
}))
|
|
2421
2645
|
};
|
|
2422
|
-
return
|
|
2646
|
+
return text13(`${md}
|
|
2423
2647
|
|
|
2424
2648
|
\`\`\`json
|
|
2425
2649
|
${JSON.stringify(slim)}
|
|
2426
2650
|
\`\`\``);
|
|
2427
2651
|
} catch (e) {
|
|
2428
|
-
return
|
|
2652
|
+
return fail15(errMsg15(e));
|
|
2429
2653
|
}
|
|
2430
2654
|
}
|
|
2431
2655
|
);
|
|
@@ -2434,9 +2658,9 @@ ${JSON.stringify(slim)}
|
|
|
2434
2658
|
{
|
|
2435
2659
|
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
2660
|
inputSchema: {
|
|
2437
|
-
deviceId:
|
|
2438
|
-
id:
|
|
2439
|
-
action:
|
|
2661
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2662
|
+
id: z15.number().describe("Record id from get_images."),
|
|
2663
|
+
action: z15.enum([
|
|
2440
2664
|
"hard_reload",
|
|
2441
2665
|
"retry",
|
|
2442
2666
|
"evict_disk",
|
|
@@ -2448,7 +2672,7 @@ ${JSON.stringify(slim)}
|
|
|
2448
2672
|
"swap_url",
|
|
2449
2673
|
"restore"
|
|
2450
2674
|
]).describe("What to do."),
|
|
2451
|
-
url:
|
|
2675
|
+
url: z15.string().optional().describe("Replacement URL (swap_url only).")
|
|
2452
2676
|
}
|
|
2453
2677
|
},
|
|
2454
2678
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2456,8 +2680,8 @@ ${JSON.stringify(slim)}
|
|
|
2456
2680
|
const target = sync.requireProDevice(deviceId);
|
|
2457
2681
|
if (action === "prove_savings") {
|
|
2458
2682
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2459
|
-
if (!savings) return
|
|
2460
|
-
if (!savings.ok) return
|
|
2683
|
+
if (!savings) return fail15("Device returned nothing for proveSavings.");
|
|
2684
|
+
if (!savings.ok) return fail15(savings.message);
|
|
2461
2685
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2462
2686
|
if (savings.originalBytes != null)
|
|
2463
2687
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -2469,11 +2693,11 @@ ${JSON.stringify(slim)}
|
|
|
2469
2693
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2470
2694
|
if (savings.optimizedUri)
|
|
2471
2695
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2472
|
-
return
|
|
2696
|
+
return text13(parts.join("\n"));
|
|
2473
2697
|
}
|
|
2474
2698
|
const entry = ACTION_MAP[action];
|
|
2475
|
-
if (!entry) return
|
|
2476
|
-
if (action === "swap_url" && !url) return
|
|
2699
|
+
if (!entry) return fail15(`Unknown action: ${action}`);
|
|
2700
|
+
if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
|
|
2477
2701
|
const result = await sync.callAction(
|
|
2478
2702
|
IMAGES_TOOL_ID,
|
|
2479
2703
|
entry.action,
|
|
@@ -2493,9 +2717,9 @@ ${JSON.stringify(slim)}
|
|
|
2493
2717
|
);
|
|
2494
2718
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2495
2719
|
}
|
|
2496
|
-
return
|
|
2720
|
+
return text13(lines.join("\n"));
|
|
2497
2721
|
} catch (e) {
|
|
2498
|
-
return
|
|
2722
|
+
return fail15(errMsg15(e));
|
|
2499
2723
|
}
|
|
2500
2724
|
}
|
|
2501
2725
|
);
|
|
@@ -2504,12 +2728,12 @@ ${JSON.stringify(slim)}
|
|
|
2504
2728
|
{
|
|
2505
2729
|
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
2730
|
inputSchema: {
|
|
2507
|
-
deviceId:
|
|
2508
|
-
network:
|
|
2509
|
-
blankImages:
|
|
2510
|
-
massAction:
|
|
2511
|
-
clearRegistry:
|
|
2512
|
-
clearExpoCaches:
|
|
2731
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2732
|
+
network: z15.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
2733
|
+
blankImages: z15.boolean().optional().describe("Render all images without a source."),
|
|
2734
|
+
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."),
|
|
2735
|
+
clearRegistry: z15.boolean().optional().describe("Clear the captured image log."),
|
|
2736
|
+
clearExpoCaches: z15.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
2513
2737
|
}
|
|
2514
2738
|
},
|
|
2515
2739
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -2536,10 +2760,94 @@ ${JSON.stringify(slim)}
|
|
|
2536
2760
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
2537
2761
|
done.push(r?.message ?? "expo caches cleared");
|
|
2538
2762
|
}
|
|
2539
|
-
if (done.length === 0) return
|
|
2540
|
-
return
|
|
2763
|
+
if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
2764
|
+
return text13(`Applied: ${done.join(" \xB7 ")}`);
|
|
2541
2765
|
} catch (e) {
|
|
2542
|
-
return
|
|
2766
|
+
return fail15(errMsg15(e));
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/tools/packs/reload.ts
|
|
2773
|
+
import { z as z16 } from "zod";
|
|
2774
|
+
var APP_TOOL_ID = "app";
|
|
2775
|
+
var RELOAD_ACTION = "reloadApp";
|
|
2776
|
+
var DEFAULT_WAIT_MS = 3e4;
|
|
2777
|
+
var TEARDOWN_GRACE_MS = 1200;
|
|
2778
|
+
var PROBE_TIMEOUT_MS = 1500;
|
|
2779
|
+
var PROBE_INTERVAL_MS = 400;
|
|
2780
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2781
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2782
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2783
|
+
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2784
|
+
async function ping(sync, deviceId, timeoutMs) {
|
|
2785
|
+
try {
|
|
2786
|
+
const pong = await sync.callAction(APP_TOOL_ID, "ping", {}, deviceId, { timeoutMs });
|
|
2787
|
+
return typeof pong?.startedAt === "number" ? pong.startedAt : null;
|
|
2788
|
+
} catch {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
function assertReloadSupported(sync, deviceId) {
|
|
2793
|
+
if (deviceId === ALL_DEVICES) return;
|
|
2794
|
+
const entry = sync.toolActions(APP_TOOL_ID).find((t) => t.deviceId === deviceId);
|
|
2795
|
+
if (!entry?.actions.includes(RELOAD_ACTION)) {
|
|
2796
|
+
throw new Error(
|
|
2797
|
+
`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.`
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
async function waitForAppBack(sync, deviceId, timeoutMs, previousRealm) {
|
|
2802
|
+
const waitStart = Date.now();
|
|
2803
|
+
await delay3(TEARDOWN_GRACE_MS);
|
|
2804
|
+
while (Date.now() - waitStart < timeoutMs) {
|
|
2805
|
+
const realm = await ping(sync, deviceId, PROBE_TIMEOUT_MS);
|
|
2806
|
+
if (realm !== null && realm !== previousRealm) return Date.now() - waitStart;
|
|
2807
|
+
await delay3(PROBE_INTERVAL_MS);
|
|
2808
|
+
}
|
|
2809
|
+
return null;
|
|
2810
|
+
}
|
|
2811
|
+
function registerReloadTools(server, sync) {
|
|
2812
|
+
server.registerTool(
|
|
2813
|
+
"reload_app",
|
|
2814
|
+
{
|
|
2815
|
+
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.",
|
|
2816
|
+
inputSchema: {
|
|
2817
|
+
strategy: z16.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
2818
|
+
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
2819
|
+
),
|
|
2820
|
+
wait: z16.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
2821
|
+
timeoutMs: z16.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
2822
|
+
deviceId: z16.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
2823
|
+
}
|
|
2824
|
+
},
|
|
2825
|
+
async (args) => {
|
|
2826
|
+
try {
|
|
2827
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2828
|
+
assertReloadSupported(sync, target);
|
|
2829
|
+
const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
|
|
2830
|
+
const ack = await sync.callAction(
|
|
2831
|
+
APP_TOOL_ID,
|
|
2832
|
+
RELOAD_ACTION,
|
|
2833
|
+
{ strategy: args.strategy ?? "auto" },
|
|
2834
|
+
target
|
|
2835
|
+
);
|
|
2836
|
+
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
2837
|
+
if (args.wait === false) {
|
|
2838
|
+
return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
2839
|
+
}
|
|
2840
|
+
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
2841
|
+
if (elapsed == null) {
|
|
2842
|
+
return fail16(
|
|
2843
|
+
`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.`
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
return text14(
|
|
2847
|
+
`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.`
|
|
2848
|
+
);
|
|
2849
|
+
} catch (e) {
|
|
2850
|
+
return fail16(errMsg16(e));
|
|
2543
2851
|
}
|
|
2544
2852
|
}
|
|
2545
2853
|
);
|
|
@@ -2558,6 +2866,13 @@ filter (e.g. sources:["network"] to list HTTP requests). It's the universal, tok
|
|
|
2558
2866
|
activity reader across ALL tools and is compact by default \u2014 reach for it before reading source
|
|
2559
2867
|
code, and prefer it over \`get_snapshot\` for any "what happened / show me the requests" question.
|
|
2560
2868
|
|
|
2869
|
+
Working on ONE HTTP request rather than skimming activity? \`get_events\` deliberately emits no
|
|
2870
|
+
request ids, so use \`get_network_requests\` \u2014 same compact style, but it leads with the id every
|
|
2871
|
+
network action is keyed by, and marks which requests are pinned/saved. \`flagged:'any'\` lists the
|
|
2872
|
+
requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-request cap and app
|
|
2873
|
+
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
2874
|
+
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
2875
|
+
|
|
2561
2876
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
2562
2877
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
2563
2878
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -2576,6 +2891,11 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
2576
2891
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
2577
2892
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
2578
2893
|
|
|
2894
|
+
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
2895
|
+
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
2896
|
+
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
2897
|
+
re-read anything you cached.
|
|
2898
|
+
|
|
2579
2899
|
For everything else:
|
|
2580
2900
|
- \`list_tool_actions\` to see what a tool can do,
|
|
2581
2901
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -2595,6 +2915,7 @@ function createMcpServer(sync) {
|
|
|
2595
2915
|
registerGenericTools(server, sync);
|
|
2596
2916
|
registerPerfMonitorTools(server, sync);
|
|
2597
2917
|
registerEventsTools(server, sync);
|
|
2918
|
+
registerNetworkTools(server, sync);
|
|
2598
2919
|
registerReactQueryTools(server, sync);
|
|
2599
2920
|
registerZustandTools(server, sync);
|
|
2600
2921
|
registerJotaiTools(server, sync);
|
|
@@ -2606,22 +2927,27 @@ function createMcpServer(sync) {
|
|
|
2606
2927
|
registerInteractTools(server, sync);
|
|
2607
2928
|
registerJsThreadTools(server, sync);
|
|
2608
2929
|
registerImagesTools(server, sync);
|
|
2930
|
+
registerReloadTools(server, sync);
|
|
2609
2931
|
return server;
|
|
2610
2932
|
}
|
|
2611
2933
|
var deviceIdField = {
|
|
2612
|
-
deviceId:
|
|
2934
|
+
deviceId: z17.string().optional().describe(
|
|
2613
2935
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
2614
2936
|
)
|
|
2615
2937
|
};
|
|
2616
2938
|
|
|
2617
2939
|
// src/init.ts
|
|
2940
|
+
import { spawnSync } from "child_process";
|
|
2618
2941
|
import fs3 from "fs";
|
|
2942
|
+
import os3 from "os";
|
|
2619
2943
|
import path3 from "path";
|
|
2944
|
+
var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
2945
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
2620
2946
|
function runInit(opts = {}) {
|
|
2621
2947
|
const cwd = opts.cwd ?? process.cwd();
|
|
2622
2948
|
const skillSrcDir = opts.skillSrcDir ?? path3.resolve(__dirname, "../skill");
|
|
2623
2949
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
2624
|
-
const entry = opts
|
|
2950
|
+
const entry = resolveEntry(opts, log);
|
|
2625
2951
|
const done = [];
|
|
2626
2952
|
upsertMcpConfig(path3.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
2627
2953
|
if (fs3.existsSync(path3.join(cwd, ".cursor"))) {
|
|
@@ -2638,12 +2964,125 @@ function runInit(opts = {}) {
|
|
|
2638
2964
|
log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
|
|
2639
2965
|
log(" 2. Open your app with Buoy devtools running on a device or simulator.");
|
|
2640
2966
|
log(' 3. Say "buoy optimize" to start a guided performance pass.');
|
|
2641
|
-
if (
|
|
2967
|
+
if (entry.command === "npx") {
|
|
2642
2968
|
log("");
|
|
2643
2969
|
log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
|
|
2644
2970
|
log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
|
|
2971
|
+
} else if (!opts.cliPath) {
|
|
2972
|
+
log("");
|
|
2973
|
+
log(`This uses a pinned local install (no network on startup). To update it later,`);
|
|
2974
|
+
log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
function resolveEntry(opts, log) {
|
|
2978
|
+
if (opts.cliPath) {
|
|
2979
|
+
return { type: "stdio", command: "node", args: [opts.cliPath] };
|
|
2980
|
+
}
|
|
2981
|
+
const mode = opts.mode ?? "auto";
|
|
2982
|
+
if (mode === "npx") {
|
|
2983
|
+
return npxEntry();
|
|
2984
|
+
}
|
|
2985
|
+
if (mode === "local") {
|
|
2986
|
+
return localEntry(opts, log);
|
|
2987
|
+
}
|
|
2988
|
+
const probe = probeRegistry();
|
|
2989
|
+
if (probe.reachable) {
|
|
2990
|
+
return npxEntry();
|
|
2991
|
+
}
|
|
2992
|
+
log(
|
|
2993
|
+
`\u26A0 npm registry ${probe.registry ? `(${probe.registry}) ` : ""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`
|
|
2994
|
+
);
|
|
2995
|
+
return localEntry(opts, log);
|
|
2996
|
+
}
|
|
2997
|
+
function npxEntry() {
|
|
2998
|
+
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
|
|
2999
|
+
}
|
|
3000
|
+
function localEntry(opts, log) {
|
|
3001
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? path3.join(os3.homedir(), ".buoy-mcp");
|
|
3002
|
+
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
3003
|
+
const cliPath = installLocalCopy(installDir, registry, log);
|
|
3004
|
+
if (cliPath) {
|
|
3005
|
+
return { type: "stdio", command: "node", args: [cliPath] };
|
|
3006
|
+
}
|
|
3007
|
+
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
3008
|
+
return npxEntry();
|
|
3009
|
+
}
|
|
3010
|
+
function installLocalCopy(installDir, registry, log) {
|
|
3011
|
+
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
3012
|
+
fs3.mkdirSync(installDir, { recursive: true });
|
|
3013
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3014
|
+
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
3015
|
+
const res = spawnSync(
|
|
3016
|
+
npm,
|
|
3017
|
+
[
|
|
3018
|
+
"install",
|
|
3019
|
+
spec,
|
|
3020
|
+
"--prefix",
|
|
3021
|
+
installDir,
|
|
3022
|
+
"--registry",
|
|
3023
|
+
registry,
|
|
3024
|
+
`--@buoy-gg:registry=${registry}`,
|
|
3025
|
+
"--no-audit",
|
|
3026
|
+
"--no-fund",
|
|
3027
|
+
"--loglevel=error"
|
|
3028
|
+
],
|
|
3029
|
+
{ stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
|
|
3030
|
+
);
|
|
3031
|
+
if (res.status !== 0) {
|
|
3032
|
+
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
3033
|
+
return null;
|
|
2645
3034
|
}
|
|
3035
|
+
const cliPath = path3.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
3036
|
+
if (!fs3.existsSync(cliPath)) {
|
|
3037
|
+
log(`\u26A0 install finished but ${cliPath} is missing.`);
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
return cliPath;
|
|
2646
3041
|
}
|
|
3042
|
+
function probeRegistry() {
|
|
3043
|
+
const registry = effectiveRegistry();
|
|
3044
|
+
if (!registry) return { reachable: false, registry: null };
|
|
3045
|
+
if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
|
|
3046
|
+
return { reachable: true, registry };
|
|
3047
|
+
}
|
|
3048
|
+
return { reachable: hostReachable(registry), registry };
|
|
3049
|
+
}
|
|
3050
|
+
function effectiveRegistry() {
|
|
3051
|
+
const scoped = npmConfigGet("@buoy-gg:registry");
|
|
3052
|
+
if (scoped) return scoped;
|
|
3053
|
+
const global = npmConfigGet("registry");
|
|
3054
|
+
return global ?? PUBLIC_REGISTRY;
|
|
3055
|
+
}
|
|
3056
|
+
function npmConfigGet(key) {
|
|
3057
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3058
|
+
const res = spawnSync(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 });
|
|
3059
|
+
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
3060
|
+
const val = res.stdout.trim();
|
|
3061
|
+
if (!val || val === "undefined" || val === "null") return null;
|
|
3062
|
+
return val;
|
|
3063
|
+
}
|
|
3064
|
+
function hostReachable(registry) {
|
|
3065
|
+
const done = spawnSync(
|
|
3066
|
+
process.execPath,
|
|
3067
|
+
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS2)],
|
|
3068
|
+
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 + 2e3 }
|
|
3069
|
+
);
|
|
3070
|
+
return done.status === 0;
|
|
3071
|
+
}
|
|
3072
|
+
var REACHABILITY_PROBE = `
|
|
3073
|
+
const url = process.argv[1];
|
|
3074
|
+
const ms = Number(process.argv[2]) || 3000;
|
|
3075
|
+
const c = new AbortController();
|
|
3076
|
+
const t = setTimeout(() => c.abort(), ms);
|
|
3077
|
+
fetch(url, { method: 'HEAD', signal: c.signal })
|
|
3078
|
+
.then(() => { clearTimeout(t); process.exit(0); })
|
|
3079
|
+
.catch(() => {
|
|
3080
|
+
// A HEAD may be rejected while the host is up; a bare TCP-level failure is
|
|
3081
|
+
// what we care about. fetch throwing a TypeError = network unreachable.
|
|
3082
|
+
clearTimeout(t);
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
});
|
|
3085
|
+
`;
|
|
2647
3086
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
2648
3087
|
let json3 = {};
|
|
2649
3088
|
if (fs3.existsSync(file)) {
|