@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
package/dist/cli.js
CHANGED
|
@@ -325,7 +325,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
325
325
|
|
|
326
326
|
// src/server.ts
|
|
327
327
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
328
|
-
var
|
|
328
|
+
var import_zod17 = require("zod");
|
|
329
329
|
|
|
330
330
|
// src/tools/generic.ts
|
|
331
331
|
var import_zod = require("zod");
|
|
@@ -336,7 +336,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
336
336
|
var import_node_path = __toESM(require("path"));
|
|
337
337
|
|
|
338
338
|
// src/version.ts
|
|
339
|
-
var VERSION = "7.0.
|
|
339
|
+
var VERSION = "7.0.5" ? "7.0.5" : "0.0.0-dev";
|
|
340
340
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
341
341
|
|
|
342
342
|
// src/update/checkForUpdate.ts
|
|
@@ -1285,12 +1285,233 @@ _Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
|
|
|
1285
1285
|
);
|
|
1286
1286
|
}
|
|
1287
1287
|
|
|
1288
|
-
// src/tools/packs/
|
|
1288
|
+
// src/tools/packs/network.ts
|
|
1289
1289
|
var import_zod4 = require("zod");
|
|
1290
|
-
var
|
|
1290
|
+
var NETWORK_TOOL_ID = "network";
|
|
1291
1291
|
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1292
1292
|
var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1293
1293
|
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1294
|
+
function parseSnapshot(payload) {
|
|
1295
|
+
if (Array.isArray(payload)) return { events: payload, saved: [] };
|
|
1296
|
+
if (payload && typeof payload === "object") {
|
|
1297
|
+
const shaped = payload;
|
|
1298
|
+
return {
|
|
1299
|
+
events: Array.isArray(shaped.events) ? shaped.events : [],
|
|
1300
|
+
saved: Array.isArray(shaped.saved) ? shaped.saved : []
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
return { events: [], saved: [] };
|
|
1304
|
+
}
|
|
1305
|
+
function statusIcon(e) {
|
|
1306
|
+
if (e.error) return "\u274C";
|
|
1307
|
+
if (e.status === void 0) return "\u23F3";
|
|
1308
|
+
if (e.status >= 400) return "\u274C";
|
|
1309
|
+
if (e.status >= 300) return "\u21AA\uFE0F";
|
|
1310
|
+
return "\u2705";
|
|
1311
|
+
}
|
|
1312
|
+
function statusLabel(e) {
|
|
1313
|
+
if (e.error) return `ERR ${e.error}`;
|
|
1314
|
+
if (e.status === void 0) return "pending";
|
|
1315
|
+
return String(e.status);
|
|
1316
|
+
}
|
|
1317
|
+
function formatBytes(n) {
|
|
1318
|
+
if (!n) return "";
|
|
1319
|
+
if (n < 1024) return `${n}B`;
|
|
1320
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)}KB`;
|
|
1321
|
+
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
1322
|
+
}
|
|
1323
|
+
function matchesStatus(e, status) {
|
|
1324
|
+
if (!status || status === "all") return true;
|
|
1325
|
+
const failed = !!e.error || e.status !== void 0 && e.status >= 400;
|
|
1326
|
+
if (status === "errors") return failed;
|
|
1327
|
+
if (status === "pending") return e.status === void 0 && !e.error;
|
|
1328
|
+
if (status === "success") return !failed && e.status !== void 0;
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
function truncate(v, n = 600) {
|
|
1332
|
+
if (v === null || v === void 0) return "(none)";
|
|
1333
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1334
|
+
return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
|
|
1335
|
+
}
|
|
1336
|
+
function formatRow(index, e, markers) {
|
|
1337
|
+
const where = e.path || e.url;
|
|
1338
|
+
const bits = [
|
|
1339
|
+
`${e.duration !== void 0 ? `${Math.round(e.duration)}ms` : "\u2014"}`,
|
|
1340
|
+
formatBytes(e.responseSize),
|
|
1341
|
+
e.host ?? "",
|
|
1342
|
+
e.requestClient ?? ""
|
|
1343
|
+
].filter(Boolean);
|
|
1344
|
+
const op = e.operationName ? ` (${e.operationName})` : "";
|
|
1345
|
+
return `${index}. \`${e.id}\` ${statusIcon(e)} ${statusLabel(e)} ${e.method} ${where}${op}${markers ? ` ${markers}` : ""}
|
|
1346
|
+
\u2514\u2500 ${bits.join(" \xB7 ")}`;
|
|
1347
|
+
}
|
|
1348
|
+
function registerNetworkTools(server, sync) {
|
|
1349
|
+
server.registerTool(
|
|
1350
|
+
"get_network_requests",
|
|
1351
|
+
{
|
|
1352
|
+
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).",
|
|
1353
|
+
inputSchema: {
|
|
1354
|
+
limit: import_zod4.z.number().optional().describe("Most-recent N requests. Default 25."),
|
|
1355
|
+
pattern: import_zod4.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
1356
|
+
status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
1357
|
+
flagged: import_zod4.z.enum(["pinned", "saved", "any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These survive Clear and app restarts."),
|
|
1358
|
+
includeBodies: import_zod4.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
1359
|
+
deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
async (args) => {
|
|
1363
|
+
try {
|
|
1364
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
1365
|
+
const { payload } = await sync.getSnapshot(NETWORK_TOOL_ID, target);
|
|
1366
|
+
const { events, saved } = parseSnapshot(payload);
|
|
1367
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
|
|
1368
|
+
const pattern = args.pattern?.toLowerCase();
|
|
1369
|
+
const pinnedIds = /* @__PURE__ */ new Set();
|
|
1370
|
+
const savedIds = /* @__PURE__ */ new Set();
|
|
1371
|
+
for (const r of saved) {
|
|
1372
|
+
if (r.pinned) pinnedIds.add(r.event?.id);
|
|
1373
|
+
if (r.saved) savedIds.add(r.event?.id);
|
|
1374
|
+
if (r.pinned) pinnedIds.add(r.liveId);
|
|
1375
|
+
if (r.saved) savedIds.add(r.liveId);
|
|
1376
|
+
}
|
|
1377
|
+
if (args.flagged) {
|
|
1378
|
+
const wanted = saved.filter(
|
|
1379
|
+
(r) => args.flagged === "pinned" ? r.pinned : args.flagged === "saved" ? r.saved : r.pinned || r.saved
|
|
1380
|
+
);
|
|
1381
|
+
const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1382
|
+
if (rows2.length === 0) {
|
|
1383
|
+
return text3(
|
|
1384
|
+
`No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
|
|
1385
|
+
|
|
1386
|
+
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.`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const body2 = rows2.map((r, i) => {
|
|
1390
|
+
const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
|
|
1391
|
+
const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
|
|
1392
|
+
const trunc = r.bodyTruncated ? " \xB7 body truncated" : "";
|
|
1393
|
+
let out = formatRow(i + 1, r.event, marks);
|
|
1394
|
+
out += `
|
|
1395
|
+
\u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
|
|
1396
|
+
if (args.includeBodies) {
|
|
1397
|
+
out += `
|
|
1398
|
+
\u2514\u2500 request: ${truncate(r.event.requestData)}`;
|
|
1399
|
+
out += `
|
|
1400
|
+
\u2514\u2500 response: ${truncate(r.event.responseData)}`;
|
|
1401
|
+
}
|
|
1402
|
+
return out;
|
|
1403
|
+
}).join("\n");
|
|
1404
|
+
return text3(
|
|
1405
|
+
`**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
|
|
1406
|
+
|
|
1407
|
+
${body2}
|
|
1408
|
+
|
|
1409
|
+
_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._`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1413
|
+
if (rows.length === 0) {
|
|
1414
|
+
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.";
|
|
1415
|
+
return text3(`No requests on ${target}. ${why}`);
|
|
1416
|
+
}
|
|
1417
|
+
const body = rows.map((e, i) => {
|
|
1418
|
+
const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
|
|
1419
|
+
let out = formatRow(i + 1, e, marks);
|
|
1420
|
+
if (args.includeBodies) {
|
|
1421
|
+
out += `
|
|
1422
|
+
\u2514\u2500 request: ${truncate(e.requestData)}`;
|
|
1423
|
+
out += `
|
|
1424
|
+
\u2514\u2500 response: ${truncate(e.responseData)}`;
|
|
1425
|
+
}
|
|
1426
|
+
return out;
|
|
1427
|
+
}).join("\n");
|
|
1428
|
+
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
1429
|
+
return text3(
|
|
1430
|
+
`**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
|
|
1431
|
+
|
|
1432
|
+
${body}
|
|
1433
|
+
|
|
1434
|
+
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
|
|
1435
|
+
);
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
return fail4(errMsg4(e));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
);
|
|
1441
|
+
server.registerTool(
|
|
1442
|
+
"network_action",
|
|
1443
|
+
{
|
|
1444
|
+
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.",
|
|
1445
|
+
inputSchema: {
|
|
1446
|
+
action: import_zod4.z.enum(["pin", "unpin", "save", "unsave", "remove", "clearPinned", "clearSaved", "clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),
|
|
1447
|
+
id: import_zod4.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
1448
|
+
key: import_zod4.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
|
|
1449
|
+
deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
async (args) => {
|
|
1453
|
+
try {
|
|
1454
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
1455
|
+
const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
|
|
1456
|
+
if (needsId && !args.id) {
|
|
1457
|
+
return fail4(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
|
|
1458
|
+
}
|
|
1459
|
+
if (args.action === "remove" && !args.key) {
|
|
1460
|
+
return fail4('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
|
|
1461
|
+
}
|
|
1462
|
+
switch (args.action) {
|
|
1463
|
+
case "pin":
|
|
1464
|
+
case "unpin": {
|
|
1465
|
+
const res = await sync.callAction(
|
|
1466
|
+
NETWORK_TOOL_ID,
|
|
1467
|
+
"setPinned",
|
|
1468
|
+
{ id: args.id, pinned: args.action === "pin" },
|
|
1469
|
+
target
|
|
1470
|
+
);
|
|
1471
|
+
if (res === null) return fail4(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
|
|
1472
|
+
const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
|
|
1473
|
+
if (capped) return fail4(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
|
|
1474
|
+
return text3(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
|
|
1475
|
+
}
|
|
1476
|
+
case "save":
|
|
1477
|
+
case "unsave": {
|
|
1478
|
+
const res = await sync.callAction(
|
|
1479
|
+
NETWORK_TOOL_ID,
|
|
1480
|
+
"setSaved",
|
|
1481
|
+
{ id: args.id, saved: args.action === "save" },
|
|
1482
|
+
target
|
|
1483
|
+
);
|
|
1484
|
+
if (res === null) return fail4(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
|
|
1485
|
+
const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
|
|
1486
|
+
if (capped) return fail4(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
|
|
1487
|
+
return text3(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
|
|
1488
|
+
}
|
|
1489
|
+
case "remove":
|
|
1490
|
+
await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
|
|
1491
|
+
return text3(`Removed kept record \`${args.key}\` on ${target}.`);
|
|
1492
|
+
case "clearPinned":
|
|
1493
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
|
|
1494
|
+
return text3(`Unpinned all requests on ${target} (saved ones kept).`);
|
|
1495
|
+
case "clearSaved":
|
|
1496
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
|
|
1497
|
+
return text3(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
1498
|
+
case "clearRequests":
|
|
1499
|
+
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
1500
|
+
return text3(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
|
|
1501
|
+
}
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
return fail4(errMsg4(e));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/tools/packs/reactQuery.ts
|
|
1510
|
+
var import_zod5 = require("zod");
|
|
1511
|
+
var RQ_TOOL_ID = "react-query";
|
|
1512
|
+
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1513
|
+
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1514
|
+
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
1294
1515
|
var STATUS_ICON = {
|
|
1295
1516
|
fresh: "\u{1F7E2}",
|
|
1296
1517
|
stale: "\u{1F7E1}",
|
|
@@ -1314,10 +1535,10 @@ function registerReactQueryTools(server, sync) {
|
|
|
1314
1535
|
{
|
|
1315
1536
|
description: "Read the app's CURRENT React Query cache \u2014 every query with its status (fresh/stale/fetching/error), staleness, observer count, last-updated time, and any error. Compact by DEFAULT (no cached payloads) so it's token-cheap; pass includeData:true for the actual cached data, or staleOnly:true to see just stale queries. Use this for 'what's in the cache / why is this stale / which queries errored'. (For the history of query updates over time, use get_events with sources:['react-query'].) Each row's hash is what you pass to react_query_action.",
|
|
1316
1537
|
inputSchema: {
|
|
1317
|
-
includeData:
|
|
1318
|
-
staleOnly:
|
|
1319
|
-
limit:
|
|
1320
|
-
deviceId:
|
|
1538
|
+
includeData: import_zod5.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
1539
|
+
staleOnly: import_zod5.z.boolean().optional().describe("Only stale queries. Default false."),
|
|
1540
|
+
limit: import_zod5.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
|
|
1541
|
+
deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1321
1542
|
}
|
|
1322
1543
|
},
|
|
1323
1544
|
async (args) => {
|
|
@@ -1330,12 +1551,12 @@ function registerReactQueryTools(server, sync) {
|
|
|
1330
1551
|
target
|
|
1331
1552
|
);
|
|
1332
1553
|
if (!res || !Array.isArray(res.queries)) {
|
|
1333
|
-
return
|
|
1554
|
+
return fail5(
|
|
1334
1555
|
"No react-query result from the device. The react-query tool may be inactive, or the app needs a newer Buoy (get_react_query requires the listQueries action)."
|
|
1335
1556
|
);
|
|
1336
1557
|
}
|
|
1337
1558
|
if (res.queries.length === 0) {
|
|
1338
|
-
return
|
|
1559
|
+
return text4(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
|
|
1339
1560
|
}
|
|
1340
1561
|
const lines = res.queries.map((q, i) => {
|
|
1341
1562
|
const icon = STATUS_ICON[q.status] ?? "\u2022";
|
|
@@ -1348,11 +1569,11 @@ function registerReactQueryTools(server, sync) {
|
|
|
1348
1569
|
const staleCount = res.queries.filter((q) => q.isStale).length;
|
|
1349
1570
|
const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
|
|
1350
1571
|
const note = res.includedData ? "" : "\n\n_Compact view. Pass includeData:true for cached payloads, staleOnly:true to narrow. Act on one with react_query_action using the `hash` in backticks._";
|
|
1351
|
-
return
|
|
1572
|
+
return text4(`${header}
|
|
1352
1573
|
|
|
1353
1574
|
${lines.join("\n")}${note}`);
|
|
1354
1575
|
} catch (e) {
|
|
1355
|
-
return
|
|
1576
|
+
return fail5(errMsg5(e));
|
|
1356
1577
|
}
|
|
1357
1578
|
}
|
|
1358
1579
|
);
|
|
@@ -1361,9 +1582,9 @@ ${lines.join("\n")}${note}`);
|
|
|
1361
1582
|
{
|
|
1362
1583
|
description: "Mutate the app's React Query cache: invalidate / refetch / reset / remove a single query (by queryHash, from get_react_query), or clear the entire query cache. Use this to fix stale data, force a refetch, or reset state during debugging.",
|
|
1363
1584
|
inputSchema: {
|
|
1364
|
-
action:
|
|
1365
|
-
queryHash:
|
|
1366
|
-
deviceId:
|
|
1585
|
+
action: import_zod5.z.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
|
|
1586
|
+
queryHash: import_zod5.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
|
|
1587
|
+
deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1367
1588
|
}
|
|
1368
1589
|
},
|
|
1369
1590
|
async (args) => {
|
|
@@ -1371,35 +1592,35 @@ ${lines.join("\n")}${note}`);
|
|
|
1371
1592
|
const target = sync.requireProDevice(args.deviceId);
|
|
1372
1593
|
if (args.action === "clear") {
|
|
1373
1594
|
await sync.callAction(RQ_TOOL_ID, "clearQueryCache", {}, target);
|
|
1374
|
-
return
|
|
1595
|
+
return text4("Cleared the entire React Query cache.");
|
|
1375
1596
|
}
|
|
1376
1597
|
if (!args.queryHash) {
|
|
1377
|
-
return
|
|
1598
|
+
return fail5(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
1378
1599
|
}
|
|
1379
1600
|
await sync.callAction(RQ_TOOL_ID, args.action, { queryHash: args.queryHash }, target);
|
|
1380
|
-
return
|
|
1601
|
+
return text4(`Ran ${args.action} on query \`${args.queryHash}\`.`);
|
|
1381
1602
|
} catch (e) {
|
|
1382
|
-
return
|
|
1603
|
+
return fail5(errMsg5(e));
|
|
1383
1604
|
}
|
|
1384
1605
|
}
|
|
1385
1606
|
);
|
|
1386
1607
|
}
|
|
1387
1608
|
|
|
1388
1609
|
// src/tools/packs/zustand.ts
|
|
1389
|
-
var
|
|
1610
|
+
var import_zod6 = require("zod");
|
|
1390
1611
|
var ZUSTAND_TOOL_ID = "zustand";
|
|
1391
|
-
var
|
|
1392
|
-
var
|
|
1393
|
-
var
|
|
1612
|
+
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1613
|
+
var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1614
|
+
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
1394
1615
|
function registerZustandTools(server, sync) {
|
|
1395
1616
|
server.registerTool(
|
|
1396
1617
|
"get_zustand_state",
|
|
1397
1618
|
{
|
|
1398
1619
|
description: "Read the app's CURRENT Zustand store state \u2014 each registered store with its name, change count, persistence, and (by default) just its top-level keys so it's token-cheap. Pass includeValues:true for the full state objects. Use this for 'what's in my zustand stores / what's the current count'. (For the history of state changes over time, use get_events with sources:['zustand'].)",
|
|
1399
1620
|
inputSchema: {
|
|
1400
|
-
includeValues:
|
|
1401
|
-
limit:
|
|
1402
|
-
deviceId:
|
|
1621
|
+
includeValues: import_zod6.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
1622
|
+
limit: import_zod6.z.number().optional().describe("First N stores. Default all."),
|
|
1623
|
+
deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1403
1624
|
}
|
|
1404
1625
|
},
|
|
1405
1626
|
async (args) => {
|
|
@@ -1412,12 +1633,12 @@ function registerZustandTools(server, sync) {
|
|
|
1412
1633
|
target
|
|
1413
1634
|
);
|
|
1414
1635
|
if (!res || !Array.isArray(res.stores)) {
|
|
1415
|
-
return
|
|
1636
|
+
return fail6(
|
|
1416
1637
|
"No zustand result from the device. The zustand tool may be inactive, or the app needs a newer Buoy (get_zustand_state requires the listStores action)."
|
|
1417
1638
|
);
|
|
1418
1639
|
}
|
|
1419
1640
|
if (res.stores.length === 0) {
|
|
1420
|
-
return
|
|
1641
|
+
return text5("No Zustand stores are registered (call watchStores to register them).");
|
|
1421
1642
|
}
|
|
1422
1643
|
const lines = res.stores.map((s, i) => {
|
|
1423
1644
|
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
@@ -1426,11 +1647,11 @@ function registerZustandTools(server, sync) {
|
|
|
1426
1647
|
return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
|
|
1427
1648
|
});
|
|
1428
1649
|
const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
|
|
1429
|
-
return
|
|
1650
|
+
return text5(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
|
|
1430
1651
|
|
|
1431
1652
|
${lines.join("\n")}${note}`);
|
|
1432
1653
|
} catch (e) {
|
|
1433
|
-
return
|
|
1654
|
+
return fail6(errMsg6(e));
|
|
1434
1655
|
}
|
|
1435
1656
|
}
|
|
1436
1657
|
);
|
|
@@ -1439,10 +1660,10 @@ ${lines.join("\n")}${note}`);
|
|
|
1439
1660
|
{
|
|
1440
1661
|
description: "Set the state of a Zustand store (time-travel / reset / tweak during debugging). Targets a store by name (from get_zustand_state). By default replaces the whole state; pass replace:false to merge a partial.",
|
|
1441
1662
|
inputSchema: {
|
|
1442
|
-
store:
|
|
1443
|
-
state:
|
|
1444
|
-
replace:
|
|
1445
|
-
deviceId:
|
|
1663
|
+
store: import_zod6.z.string().describe("Store name to target (from get_zustand_state)."),
|
|
1664
|
+
state: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).describe("The new state object."),
|
|
1665
|
+
replace: import_zod6.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
|
|
1666
|
+
deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1446
1667
|
}
|
|
1447
1668
|
},
|
|
1448
1669
|
async (args) => {
|
|
@@ -1454,29 +1675,29 @@ ${lines.join("\n")}${note}`);
|
|
|
1454
1675
|
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
1455
1676
|
target
|
|
1456
1677
|
);
|
|
1457
|
-
return
|
|
1678
|
+
return text5(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
1458
1679
|
} catch (e) {
|
|
1459
|
-
return
|
|
1680
|
+
return fail6(errMsg6(e));
|
|
1460
1681
|
}
|
|
1461
1682
|
}
|
|
1462
1683
|
);
|
|
1463
1684
|
}
|
|
1464
1685
|
|
|
1465
1686
|
// src/tools/packs/jotai.ts
|
|
1466
|
-
var
|
|
1687
|
+
var import_zod7 = require("zod");
|
|
1467
1688
|
var JOTAI_TOOL_ID = "jotai";
|
|
1468
|
-
var
|
|
1469
|
-
var
|
|
1470
|
-
var
|
|
1689
|
+
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1690
|
+
var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1691
|
+
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
1471
1692
|
function registerJotaiTools(server, sync) {
|
|
1472
1693
|
server.registerTool(
|
|
1473
1694
|
"get_jotai_state",
|
|
1474
1695
|
{
|
|
1475
1696
|
description: "Read the app's CURRENT Jotai atom values \u2014 each watched atom with its label, change count, and whether it's writable. Compact by DEFAULT (no values) so it's token-cheap; pass includeValues:true for the actual current values. Use this for 'what are my jotai atoms / what's the current value'. (For the history of atom changes over time, use get_events with sources:['jotai'].)",
|
|
1476
1697
|
inputSchema: {
|
|
1477
|
-
includeValues:
|
|
1478
|
-
limit:
|
|
1479
|
-
deviceId:
|
|
1698
|
+
includeValues: import_zod7.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
1699
|
+
limit: import_zod7.z.number().optional().describe("First N atoms. Default all."),
|
|
1700
|
+
deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1480
1701
|
}
|
|
1481
1702
|
},
|
|
1482
1703
|
async (args) => {
|
|
@@ -1489,12 +1710,12 @@ function registerJotaiTools(server, sync) {
|
|
|
1489
1710
|
target
|
|
1490
1711
|
);
|
|
1491
1712
|
if (!res || !Array.isArray(res.atoms)) {
|
|
1492
|
-
return
|
|
1713
|
+
return fail7(
|
|
1493
1714
|
"No jotai result from the device. The jotai tool may be inactive, or the app needs a newer Buoy (get_jotai_state requires the listAtoms action)."
|
|
1494
1715
|
);
|
|
1495
1716
|
}
|
|
1496
1717
|
if (res.atoms.length === 0) {
|
|
1497
|
-
return
|
|
1718
|
+
return text6("No Jotai atoms are being watched (call watchAtoms to register them).");
|
|
1498
1719
|
}
|
|
1499
1720
|
const lines = res.atoms.map((a, i) => {
|
|
1500
1721
|
const kind = a.writable ? "writable" : "read-only";
|
|
@@ -1503,11 +1724,11 @@ function registerJotaiTools(server, sync) {
|
|
|
1503
1724
|
return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
|
|
1504
1725
|
});
|
|
1505
1726
|
const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
|
|
1506
|
-
return
|
|
1727
|
+
return text6(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
|
|
1507
1728
|
|
|
1508
1729
|
${lines.join("\n")}${note}`);
|
|
1509
1730
|
} catch (e) {
|
|
1510
|
-
return
|
|
1731
|
+
return fail7(errMsg7(e));
|
|
1511
1732
|
}
|
|
1512
1733
|
}
|
|
1513
1734
|
);
|
|
@@ -1516,29 +1737,29 @@ ${lines.join("\n")}${note}`);
|
|
|
1516
1737
|
{
|
|
1517
1738
|
description: "Set the value of a writable Jotai atom (by label, from get_jotai_state). Read-only/derived atoms cannot be set (you'll get a clear error). Use for tweaking state during debugging.",
|
|
1518
1739
|
inputSchema: {
|
|
1519
|
-
atom:
|
|
1520
|
-
value:
|
|
1521
|
-
deviceId:
|
|
1740
|
+
atom: import_zod7.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
|
|
1741
|
+
value: import_zod7.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
|
|
1742
|
+
deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1522
1743
|
}
|
|
1523
1744
|
},
|
|
1524
1745
|
async (args) => {
|
|
1525
1746
|
try {
|
|
1526
1747
|
const target = sync.requireProDevice(args.deviceId);
|
|
1527
1748
|
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
1528
|
-
return
|
|
1749
|
+
return text6(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
1529
1750
|
} catch (e) {
|
|
1530
|
-
return
|
|
1751
|
+
return fail7(errMsg7(e));
|
|
1531
1752
|
}
|
|
1532
1753
|
}
|
|
1533
1754
|
);
|
|
1534
1755
|
}
|
|
1535
1756
|
|
|
1536
1757
|
// src/tools/packs/redux.ts
|
|
1537
|
-
var
|
|
1758
|
+
var import_zod8 = require("zod");
|
|
1538
1759
|
var REDUX_TOOL_ID = "redux";
|
|
1539
|
-
var
|
|
1540
|
-
var
|
|
1541
|
-
var
|
|
1760
|
+
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1761
|
+
var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1762
|
+
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
1542
1763
|
var NO_STORE = "No instrumented Redux store found. Open the app with Buoy redux devtools active (auto-instrumentation), then retry.";
|
|
1543
1764
|
function registerReduxTools(server, sync) {
|
|
1544
1765
|
server.registerTool(
|
|
@@ -1546,8 +1767,8 @@ function registerReduxTools(server, sync) {
|
|
|
1546
1767
|
{
|
|
1547
1768
|
description: "Read the app's CURRENT Redux state. Compact by DEFAULT \u2014 just the top-level slice names \u2014 so it's token-cheap; pass includeValues:true for the full state tree. Use this for 'what's in my redux store / current auth state'. (For the history of dispatched actions over time, use get_events with sources:['redux'].)",
|
|
1548
1769
|
inputSchema: {
|
|
1549
|
-
includeValues:
|
|
1550
|
-
deviceId:
|
|
1770
|
+
includeValues: import_zod8.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
1771
|
+
deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1551
1772
|
}
|
|
1552
1773
|
},
|
|
1553
1774
|
async (args) => {
|
|
@@ -1560,26 +1781,26 @@ function registerReduxTools(server, sync) {
|
|
|
1560
1781
|
target
|
|
1561
1782
|
);
|
|
1562
1783
|
if (!res) {
|
|
1563
|
-
return
|
|
1784
|
+
return fail8(
|
|
1564
1785
|
"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)."
|
|
1565
1786
|
);
|
|
1566
1787
|
}
|
|
1567
|
-
if (!res.available) return
|
|
1788
|
+
if (!res.available) return text7(NO_STORE);
|
|
1568
1789
|
const slices = res.slices.length ? res.slices.join(", ") : "(none)";
|
|
1569
1790
|
if (args.includeValues) {
|
|
1570
|
-
return
|
|
1791
|
+
return text7(`**Redux store \u2014 slices: ${slices}**
|
|
1571
1792
|
|
|
1572
1793
|
\`\`\`json
|
|
1573
1794
|
${JSON.stringify(res.state, null, 2)}
|
|
1574
1795
|
\`\`\``);
|
|
1575
1796
|
}
|
|
1576
|
-
return
|
|
1797
|
+
return text7(
|
|
1577
1798
|
`**Redux store \u2014 slices: ${slices}**
|
|
1578
1799
|
|
|
1579
1800
|
_Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._`
|
|
1580
1801
|
);
|
|
1581
1802
|
} catch (e) {
|
|
1582
|
-
return
|
|
1803
|
+
return fail8(errMsg8(e));
|
|
1583
1804
|
}
|
|
1584
1805
|
}
|
|
1585
1806
|
);
|
|
@@ -1588,9 +1809,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
1588
1809
|
{
|
|
1589
1810
|
description: "Dispatch a Redux action to the app's store (e.g. to trigger a state change while debugging). Provide the action type and optional payload \u2014 they're sent as { type, payload }.",
|
|
1590
1811
|
inputSchema: {
|
|
1591
|
-
type:
|
|
1592
|
-
payload:
|
|
1593
|
-
deviceId:
|
|
1812
|
+
type: import_zod8.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
|
|
1813
|
+
payload: import_zod8.z.unknown().optional().describe("Optional action payload (any JSON)."),
|
|
1814
|
+
deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1594
1815
|
}
|
|
1595
1816
|
},
|
|
1596
1817
|
async (args) => {
|
|
@@ -1599,22 +1820,22 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
1599
1820
|
const action = { type: args.type };
|
|
1600
1821
|
if (args.payload !== void 0) action.payload = args.payload;
|
|
1601
1822
|
const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
|
|
1602
|
-
if (!res || !res.dispatched) return
|
|
1603
|
-
return
|
|
1823
|
+
if (!res || !res.dispatched) return fail8(NO_STORE);
|
|
1824
|
+
return text7(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
|
|
1604
1825
|
} catch (e) {
|
|
1605
|
-
return
|
|
1826
|
+
return fail8(errMsg8(e));
|
|
1606
1827
|
}
|
|
1607
1828
|
}
|
|
1608
1829
|
);
|
|
1609
1830
|
}
|
|
1610
1831
|
|
|
1611
1832
|
// src/tools/packs/storage.ts
|
|
1612
|
-
var
|
|
1833
|
+
var import_zod9 = require("zod");
|
|
1613
1834
|
var STORAGE_TOOL_ID = "storage";
|
|
1614
|
-
var
|
|
1615
|
-
var
|
|
1616
|
-
var
|
|
1617
|
-
function
|
|
1835
|
+
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1836
|
+
var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1837
|
+
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
1838
|
+
function truncate2(v, n = 200) {
|
|
1618
1839
|
if (v === null || v === void 0) return "(null)";
|
|
1619
1840
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1620
1841
|
return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
|
|
@@ -1625,10 +1846,10 @@ function registerStorageTools(server, sync) {
|
|
|
1625
1846
|
{
|
|
1626
1847
|
description: "Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out. Use for 'what's in storage / read this key'.",
|
|
1627
1848
|
inputSchema: {
|
|
1628
|
-
pattern:
|
|
1629
|
-
includeValues:
|
|
1630
|
-
limit:
|
|
1631
|
-
deviceId:
|
|
1849
|
+
pattern: import_zod9.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
|
|
1850
|
+
includeValues: import_zod9.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
|
|
1851
|
+
limit: import_zod9.z.number().optional().describe("Max keys per backend section. Default 100."),
|
|
1852
|
+
deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1632
1853
|
}
|
|
1633
1854
|
},
|
|
1634
1855
|
async (args) => {
|
|
@@ -1640,7 +1861,7 @@ function registerStorageTools(server, sync) {
|
|
|
1640
1861
|
const sections = [];
|
|
1641
1862
|
const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
|
|
1642
1863
|
if (!Array.isArray(allKeys)) {
|
|
1643
|
-
return
|
|
1864
|
+
return fail9("No storage result from the device. The storage tool may be inactive.");
|
|
1644
1865
|
}
|
|
1645
1866
|
const asyncKeys = allKeys.filter(matches).slice(0, limit);
|
|
1646
1867
|
if (asyncKeys.length > 0) {
|
|
@@ -1648,7 +1869,7 @@ function registerStorageTools(server, sync) {
|
|
|
1648
1869
|
if (args.includeValues) {
|
|
1649
1870
|
const pairs = await sync.callAction(STORAGE_TOOL_ID, "async.multiGet", { keys: asyncKeys }, target);
|
|
1650
1871
|
const map = new Map((pairs ?? []).map(([k, v]) => [k, v]));
|
|
1651
|
-
lines = asyncKeys.map((k) => `- \`${k}\` = ${
|
|
1872
|
+
lines = asyncKeys.map((k) => `- \`${k}\` = ${truncate2(map.get(k) ?? null)}`);
|
|
1652
1873
|
} else {
|
|
1653
1874
|
lines = asyncKeys.map((k) => `- \`${k}\``);
|
|
1654
1875
|
}
|
|
@@ -1660,7 +1881,7 @@ ${lines.join("\n")}`);
|
|
|
1660
1881
|
const entries = inst.entries.filter((e) => matches(e.key)).slice(0, limit);
|
|
1661
1882
|
if (entries.length === 0) continue;
|
|
1662
1883
|
const lines = entries.map(
|
|
1663
|
-
(e) => args.includeValues ? `- \`${e.key}\` (${e.valueType}) = ${
|
|
1884
|
+
(e) => args.includeValues ? `- \`${e.key}\` (${e.valueType}) = ${truncate2(e.value)}` : `- \`${e.key}\``
|
|
1664
1885
|
);
|
|
1665
1886
|
const flags = [inst.encrypted && "encrypted", inst.readOnly && "read-only"].filter(Boolean).join(", ");
|
|
1666
1887
|
sections.push(`**MMKV \`${inst.id}\`${flags ? ` (${flags})` : ""} (${entries.length}/${inst.entries.length} keys):**
|
|
@@ -1674,19 +1895,19 @@ ${lines.join("\n")}`);
|
|
|
1674
1895
|
if (d.requireAuthentication) return `- \`${d.key}\` \u{1F512} (biometric-protected \u2014 not read)`;
|
|
1675
1896
|
if (!args.includeValues) return `- \`${d.key}\``;
|
|
1676
1897
|
const v = await sync.callAction(STORAGE_TOOL_ID, "secure.get", { key: d.key }, target).catch(() => null);
|
|
1677
|
-
return `- \`${d.key}\` = ${
|
|
1898
|
+
return `- \`${d.key}\` = ${truncate2(v)}`;
|
|
1678
1899
|
})
|
|
1679
1900
|
);
|
|
1680
1901
|
sections.push(`**SecureStore (${secureKeys.length} registered keys):**
|
|
1681
1902
|
${lines.join("\n")}`);
|
|
1682
1903
|
}
|
|
1683
1904
|
if (sections.length === 0) {
|
|
1684
|
-
return
|
|
1905
|
+
return text8(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
|
|
1685
1906
|
}
|
|
1686
1907
|
const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
|
|
1687
|
-
return
|
|
1908
|
+
return text8(`${sections.join("\n\n")}${note}`);
|
|
1688
1909
|
} catch (e) {
|
|
1689
|
-
return
|
|
1910
|
+
return fail9(errMsg9(e));
|
|
1690
1911
|
}
|
|
1691
1912
|
}
|
|
1692
1913
|
);
|
|
@@ -1695,10 +1916,10 @@ ${lines.join("\n")}`);
|
|
|
1695
1916
|
{
|
|
1696
1917
|
description: "Modify the app's AsyncStorage: set a key, remove a key, or clear all app storage (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",
|
|
1697
1918
|
inputSchema: {
|
|
1698
|
-
action:
|
|
1699
|
-
key:
|
|
1700
|
-
value:
|
|
1701
|
-
deviceId:
|
|
1919
|
+
action: import_zod9.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
|
|
1920
|
+
key: import_zod9.z.string().optional().describe("The key (required for set and remove)."),
|
|
1921
|
+
value: import_zod9.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
1922
|
+
deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1702
1923
|
}
|
|
1703
1924
|
},
|
|
1704
1925
|
async (args) => {
|
|
@@ -1706,33 +1927,33 @@ ${lines.join("\n")}`);
|
|
|
1706
1927
|
const target = sync.requireProDevice(args.deviceId);
|
|
1707
1928
|
if (args.action === "clear") {
|
|
1708
1929
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
1709
|
-
return
|
|
1930
|
+
return text8("Cleared all app storage (Buoy's own devtool keys preserved).");
|
|
1710
1931
|
}
|
|
1711
1932
|
if (!args.key) {
|
|
1712
|
-
return
|
|
1933
|
+
return fail9(`'${args.action}' needs a key.`);
|
|
1713
1934
|
}
|
|
1714
1935
|
if (args.action === "remove") {
|
|
1715
1936
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
1716
|
-
return
|
|
1937
|
+
return text8(`Removed storage key "${args.key}".`);
|
|
1717
1938
|
}
|
|
1718
1939
|
if (args.value === void 0) {
|
|
1719
|
-
return
|
|
1940
|
+
return fail9("'set' needs a value (a string).");
|
|
1720
1941
|
}
|
|
1721
1942
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
1722
|
-
return
|
|
1943
|
+
return text8(`Set storage key "${args.key}".`);
|
|
1723
1944
|
} catch (e) {
|
|
1724
|
-
return
|
|
1945
|
+
return fail9(errMsg9(e));
|
|
1725
1946
|
}
|
|
1726
1947
|
}
|
|
1727
1948
|
);
|
|
1728
1949
|
}
|
|
1729
1950
|
|
|
1730
1951
|
// src/tools/packs/routes.ts
|
|
1731
|
-
var
|
|
1952
|
+
var import_zod10 = require("zod");
|
|
1732
1953
|
var ROUTES_TOOL_ID = "route-events";
|
|
1733
|
-
var
|
|
1734
|
-
var
|
|
1735
|
-
var
|
|
1954
|
+
var text9 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1955
|
+
var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1956
|
+
var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
|
|
1736
1957
|
function flattenRoutes(routes, acc = []) {
|
|
1737
1958
|
for (const r of routes ?? []) {
|
|
1738
1959
|
if (r.path) acc.push(r.path);
|
|
@@ -1746,8 +1967,8 @@ function registerRoutesTools(server, sync) {
|
|
|
1746
1967
|
{
|
|
1747
1968
|
description: "Read the app's CURRENT route + navigation stack (where the user is and how they got there). Compact by DEFAULT; pass includeSitemap:true to also list every route path the app defines (useful before navigate). Use for 'what screen am I on / what's the nav stack / what routes exist'. (For navigation history over time, use get_events with sources:['route'].)",
|
|
1748
1969
|
inputSchema: {
|
|
1749
|
-
includeSitemap:
|
|
1750
|
-
deviceId:
|
|
1970
|
+
includeSitemap: import_zod10.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
|
|
1971
|
+
deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1751
1972
|
}
|
|
1752
1973
|
},
|
|
1753
1974
|
async (args) => {
|
|
@@ -1780,9 +2001,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
1780
2001
|
**Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
|
|
1781
2002
|
}
|
|
1782
2003
|
parts.push("\n_Navigate with navigate({ path })._");
|
|
1783
|
-
return
|
|
2004
|
+
return text9(parts.join("\n"));
|
|
1784
2005
|
} catch (e) {
|
|
1785
|
-
return
|
|
2006
|
+
return fail10(errMsg10(e));
|
|
1786
2007
|
}
|
|
1787
2008
|
}
|
|
1788
2009
|
);
|
|
@@ -1791,28 +2012,28 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
1791
2012
|
{
|
|
1792
2013
|
description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths.",
|
|
1793
2014
|
inputSchema: {
|
|
1794
|
-
path:
|
|
1795
|
-
deviceId:
|
|
2015
|
+
path: import_zod10.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
|
|
2016
|
+
deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1796
2017
|
}
|
|
1797
2018
|
},
|
|
1798
2019
|
async (args) => {
|
|
1799
2020
|
try {
|
|
1800
2021
|
const target = sync.requireProDevice(args.deviceId);
|
|
1801
2022
|
await sync.callAction(ROUTES_TOOL_ID, "navigate", { path: args.path }, target);
|
|
1802
|
-
return
|
|
2023
|
+
return text9(`Navigated to \`${args.path}\`.`);
|
|
1803
2024
|
} catch (e) {
|
|
1804
|
-
return
|
|
2025
|
+
return fail10(errMsg10(e));
|
|
1805
2026
|
}
|
|
1806
2027
|
}
|
|
1807
2028
|
);
|
|
1808
2029
|
}
|
|
1809
2030
|
|
|
1810
2031
|
// src/tools/packs/console.ts
|
|
1811
|
-
var
|
|
2032
|
+
var import_zod11 = require("zod");
|
|
1812
2033
|
var CONSOLE_TOOL_ID = "console";
|
|
1813
|
-
var
|
|
1814
|
-
var
|
|
1815
|
-
var
|
|
2034
|
+
var text10 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2035
|
+
var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2036
|
+
var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
|
|
1816
2037
|
var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
|
|
1817
2038
|
var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
|
|
1818
2039
|
function hhmmss(ts) {
|
|
@@ -1826,10 +2047,10 @@ function registerConsoleTools(server, sync) {
|
|
|
1826
2047
|
{
|
|
1827
2048
|
description: "Read the app's captured console logs (console.log/warn/error/\u2026) as a compact tail \u2014 time \xB7 level \xB7 message. Token-cheap (uses each entry's pre-rendered message, not raw args). Filter by minimum level (e.g. level:'warning' for warnings+errors) or a message substring. Use for 'what's logging / any console errors'. For full args/stack of an entry, fall back to get_snapshot('console').",
|
|
1828
2049
|
inputSchema: {
|
|
1829
|
-
level:
|
|
1830
|
-
limit:
|
|
1831
|
-
pattern:
|
|
1832
|
-
deviceId:
|
|
2050
|
+
level: import_zod11.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2051
|
+
limit: import_zod11.z.number().optional().describe("Most-recent N entries. Default 50."),
|
|
2052
|
+
pattern: import_zod11.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
|
|
2053
|
+
deviceId: import_zod11.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1833
2054
|
}
|
|
1834
2055
|
},
|
|
1835
2056
|
async (args) => {
|
|
@@ -1847,12 +2068,12 @@ function registerConsoleTools(server, sync) {
|
|
|
1847
2068
|
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
|
|
1848
2069
|
const shown = sorted.slice(-limit);
|
|
1849
2070
|
if (shown.length === 0) {
|
|
1850
|
-
return
|
|
2071
|
+
return text10(`No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
|
|
1851
2072
|
}
|
|
1852
2073
|
const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
|
|
1853
2074
|
const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
|
|
1854
2075
|
const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
|
|
1855
|
-
return
|
|
2076
|
+
return text10(`${header}
|
|
1856
2077
|
|
|
1857
2078
|
\`\`\`
|
|
1858
2079
|
${lines.join("\n")}
|
|
@@ -1860,14 +2081,14 @@ ${lines.join("\n")}
|
|
|
1860
2081
|
|
|
1861
2082
|
_Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
1862
2083
|
} catch (e) {
|
|
1863
|
-
return
|
|
2084
|
+
return fail11(errMsg11(e));
|
|
1864
2085
|
}
|
|
1865
2086
|
}
|
|
1866
2087
|
);
|
|
1867
2088
|
}
|
|
1868
2089
|
|
|
1869
2090
|
// src/tools/packs/screenshot.ts
|
|
1870
|
-
var
|
|
2091
|
+
var import_zod12 = require("zod");
|
|
1871
2092
|
|
|
1872
2093
|
// src/capture/simctl.ts
|
|
1873
2094
|
var import_node_child_process = require("child_process");
|
|
@@ -1947,8 +2168,8 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
1947
2168
|
var TOOL_ID = "highlight-updates";
|
|
1948
2169
|
var MARGIN_PT = 12;
|
|
1949
2170
|
var CAPTURE_PADDING_PT = 6;
|
|
1950
|
-
var
|
|
1951
|
-
var
|
|
2171
|
+
var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2172
|
+
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
1952
2173
|
function reasonMessage(res, query) {
|
|
1953
2174
|
switch (res.reason) {
|
|
1954
2175
|
case "no-renders":
|
|
@@ -1978,10 +2199,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
1978
2199
|
{
|
|
1979
2200
|
description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. The component must have rendered at least once since the app opened for it to be trackable \u2014 if nothing matches, navigate to the screen that renders it and retry. iOS Simulator only, and the MCP server must be running on the same Mac as the booted Simulator (uses `xcrun simctl` + `sips`). Requires Buoy Pro.",
|
|
1980
2201
|
inputSchema: {
|
|
1981
|
-
query:
|
|
1982
|
-
nativeTag:
|
|
1983
|
-
scrollIntoView:
|
|
1984
|
-
deviceId:
|
|
2202
|
+
query: import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2203
|
+
nativeTag: import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
2204
|
+
scrollIntoView: import_zod12.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
2205
|
+
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
1985
2206
|
}
|
|
1986
2207
|
},
|
|
1987
2208
|
async (args) => {
|
|
@@ -2001,11 +2222,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2001
2222
|
target
|
|
2002
2223
|
);
|
|
2003
2224
|
if (!res.matched || !res.rect || !res.screen) {
|
|
2004
|
-
return
|
|
2225
|
+
return fail12(reasonMessage(res, args.query) + candidatesText(res.candidates));
|
|
2005
2226
|
}
|
|
2006
2227
|
const shot = await captureIosFramebuffer();
|
|
2007
2228
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
2008
|
-
return
|
|
2229
|
+
return fail12(shot.error ?? "Screenshot capture failed.");
|
|
2009
2230
|
}
|
|
2010
2231
|
const pxScale = shot.width / res.screen.width;
|
|
2011
2232
|
const r = res.rect;
|
|
@@ -2021,7 +2242,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2021
2242
|
}
|
|
2022
2243
|
);
|
|
2023
2244
|
if (!crop.ok || !crop.buffer) {
|
|
2024
|
-
return
|
|
2245
|
+
return fail12(crop.error ?? "Crop failed.");
|
|
2025
2246
|
}
|
|
2026
2247
|
const label = res.componentName || "component";
|
|
2027
2248
|
const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
|
|
@@ -2034,21 +2255,21 @@ function registerScreenshotTools(server, sync) {
|
|
|
2034
2255
|
]
|
|
2035
2256
|
};
|
|
2036
2257
|
} catch (e) {
|
|
2037
|
-
return
|
|
2258
|
+
return fail12(errMsg12(e));
|
|
2038
2259
|
}
|
|
2039
2260
|
}
|
|
2040
2261
|
);
|
|
2041
2262
|
}
|
|
2042
2263
|
|
|
2043
2264
|
// src/tools/packs/interact.ts
|
|
2044
|
-
var
|
|
2265
|
+
var import_zod13 = require("zod");
|
|
2045
2266
|
var TOOL_ID2 = "highlight-updates";
|
|
2046
|
-
var
|
|
2047
|
-
var
|
|
2267
|
+
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2268
|
+
var fail13 = (message) => ({
|
|
2048
2269
|
isError: true,
|
|
2049
2270
|
content: [{ type: "text", text: message }]
|
|
2050
2271
|
});
|
|
2051
|
-
var
|
|
2272
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2052
2273
|
function formatElement(e) {
|
|
2053
2274
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2054
2275
|
line += e.name;
|
|
@@ -2056,6 +2277,7 @@ function formatElement(e) {
|
|
|
2056
2277
|
if (display) line += ` "${display}"`;
|
|
2057
2278
|
if (e.role) line += ` <${e.role}>`;
|
|
2058
2279
|
if (e.control) line += ` {${e.control}${e.value !== void 0 ? `=${JSON.stringify(e.value)}` : ""}}`;
|
|
2280
|
+
if (e.longPressable) line += " {long-press}";
|
|
2059
2281
|
if (e.testID) line += ` [testID=${e.testID}]`;
|
|
2060
2282
|
if (e.tap) line += ` (tap: ${e.tap.x},${e.tap.y})`;
|
|
2061
2283
|
if (e.nativeTag != null) line += ` #${e.nativeTag}`;
|
|
@@ -2067,8 +2289,8 @@ function registerInteractTools(server, sync) {
|
|
|
2067
2289
|
{
|
|
2068
2290
|
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.",
|
|
2069
2291
|
inputSchema: {
|
|
2070
|
-
interactiveOnly:
|
|
2071
|
-
deviceId:
|
|
2292
|
+
interactiveOnly: import_zod13.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
2293
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2072
2294
|
}
|
|
2073
2295
|
},
|
|
2074
2296
|
async (args) => {
|
|
@@ -2078,37 +2300,38 @@ function registerInteractTools(server, sync) {
|
|
|
2078
2300
|
const all = res?.elements ?? [];
|
|
2079
2301
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2080
2302
|
if (elements.length === 0) {
|
|
2081
|
-
return
|
|
2303
|
+
return text11(
|
|
2082
2304
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
2083
2305
|
);
|
|
2084
2306
|
}
|
|
2085
2307
|
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 }).`;
|
|
2086
|
-
return
|
|
2308
|
+
return text11(`${header}
|
|
2087
2309
|
|
|
2088
2310
|
${elements.map(formatElement).join("\n")}`);
|
|
2089
2311
|
} catch (e) {
|
|
2090
|
-
return
|
|
2312
|
+
return fail13(errMsg13(e));
|
|
2091
2313
|
}
|
|
2092
2314
|
}
|
|
2093
2315
|
);
|
|
2094
2316
|
server.registerTool(
|
|
2095
2317
|
"tap_element",
|
|
2096
2318
|
{
|
|
2097
|
-
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.",
|
|
2319
|
+
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.",
|
|
2098
2320
|
inputSchema: {
|
|
2099
|
-
nativeTag:
|
|
2100
|
-
testID:
|
|
2101
|
-
query:
|
|
2102
|
-
value:
|
|
2103
|
-
text:
|
|
2104
|
-
|
|
2105
|
-
|
|
2321
|
+
nativeTag: import_zod13.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2322
|
+
testID: import_zod13.z.string().optional().describe("Exact testID of the element."),
|
|
2323
|
+
query: import_zod13.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2324
|
+
value: import_zod13.z.union([import_zod13.z.number(), import_zod13.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
2325
|
+
text: import_zod13.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2326
|
+
longPress: import_zod13.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
2327
|
+
scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2328
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2106
2329
|
}
|
|
2107
2330
|
},
|
|
2108
2331
|
async (args) => {
|
|
2109
2332
|
try {
|
|
2110
2333
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2111
|
-
return
|
|
2334
|
+
return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2112
2335
|
}
|
|
2113
2336
|
const target = sync.requireProDevice(args.deviceId);
|
|
2114
2337
|
const res = await sync.callAction(
|
|
@@ -2120,6 +2343,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2120
2343
|
query: args.query,
|
|
2121
2344
|
value: args.value,
|
|
2122
2345
|
text: args.text,
|
|
2346
|
+
longPress: args.longPress,
|
|
2123
2347
|
scrollIntoView: args.scrollIntoView
|
|
2124
2348
|
},
|
|
2125
2349
|
target
|
|
@@ -2129,27 +2353,27 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2129
2353
|
const label = m.text ?? m.label ?? m.name;
|
|
2130
2354
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2131
2355
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
2132
|
-
const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` :
|
|
2133
|
-
return
|
|
2356
|
+
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}`;
|
|
2357
|
+
return text11(`${verb}${scrolledNote}.`);
|
|
2134
2358
|
}
|
|
2135
2359
|
let msg = res?.reason ?? "Tap failed.";
|
|
2136
2360
|
if (res?.candidates?.length) {
|
|
2137
2361
|
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");
|
|
2138
2362
|
}
|
|
2139
|
-
return
|
|
2363
|
+
return fail13(msg);
|
|
2140
2364
|
} catch (e) {
|
|
2141
|
-
return
|
|
2365
|
+
return fail13(errMsg13(e));
|
|
2142
2366
|
}
|
|
2143
2367
|
}
|
|
2144
2368
|
);
|
|
2145
2369
|
}
|
|
2146
2370
|
|
|
2147
2371
|
// src/tools/packs/jsThread.ts
|
|
2148
|
-
var
|
|
2372
|
+
var import_zod14 = require("zod");
|
|
2149
2373
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2150
|
-
var
|
|
2151
|
-
var
|
|
2152
|
-
var
|
|
2374
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2375
|
+
var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2376
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2153
2377
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2154
2378
|
function sparkline(values) {
|
|
2155
2379
|
const max = Math.max(1, ...values);
|
|
@@ -2220,9 +2444,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2220
2444
|
{
|
|
2221
2445
|
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.",
|
|
2222
2446
|
inputSchema: {
|
|
2223
|
-
deviceId:
|
|
2224
|
-
durationMs:
|
|
2225
|
-
clearFirst:
|
|
2447
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2448
|
+
durationMs: import_zod14.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2449
|
+
clearFirst: import_zod14.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2226
2450
|
}
|
|
2227
2451
|
},
|
|
2228
2452
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2235,7 +2459,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2235
2459
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2236
2460
|
target
|
|
2237
2461
|
);
|
|
2238
|
-
if (!snap) return
|
|
2462
|
+
if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2239
2463
|
const md = formatTopMarkdown(snap, duration);
|
|
2240
2464
|
const slim = {
|
|
2241
2465
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2254,13 +2478,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2254
2478
|
worstFreezeMs: r.worstFreezeMs
|
|
2255
2479
|
}))
|
|
2256
2480
|
};
|
|
2257
|
-
return
|
|
2481
|
+
return text12(`${md}
|
|
2258
2482
|
|
|
2259
2483
|
\`\`\`json
|
|
2260
2484
|
${JSON.stringify(slim)}
|
|
2261
2485
|
\`\`\``);
|
|
2262
2486
|
} catch (e) {
|
|
2263
|
-
return
|
|
2487
|
+
return fail14(errMsg14(e));
|
|
2264
2488
|
}
|
|
2265
2489
|
}
|
|
2266
2490
|
);
|
|
@@ -2269,8 +2493,8 @@ ${JSON.stringify(slim)}
|
|
|
2269
2493
|
{
|
|
2270
2494
|
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).',
|
|
2271
2495
|
inputSchema: {
|
|
2272
|
-
deviceId:
|
|
2273
|
-
key:
|
|
2496
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2497
|
+
key: import_zod14.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2274
2498
|
}
|
|
2275
2499
|
},
|
|
2276
2500
|
async ({ deviceId, key }) => {
|
|
@@ -2283,18 +2507,18 @@ ${JSON.stringify(slim)}
|
|
|
2283
2507
|
target
|
|
2284
2508
|
);
|
|
2285
2509
|
if (!detail) {
|
|
2286
|
-
return
|
|
2510
|
+
return fail14(
|
|
2287
2511
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2288
2512
|
);
|
|
2289
2513
|
}
|
|
2290
2514
|
const md = formatDetailMarkdown(detail);
|
|
2291
|
-
return
|
|
2515
|
+
return text12(`${md}
|
|
2292
2516
|
|
|
2293
2517
|
\`\`\`json
|
|
2294
2518
|
${JSON.stringify(detail)}
|
|
2295
2519
|
\`\`\``);
|
|
2296
2520
|
} catch (e) {
|
|
2297
|
-
return
|
|
2521
|
+
return fail14(errMsg14(e));
|
|
2298
2522
|
}
|
|
2299
2523
|
}
|
|
2300
2524
|
);
|
|
@@ -2303,27 +2527,27 @@ ${JSON.stringify(detail)}
|
|
|
2303
2527
|
{
|
|
2304
2528
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2305
2529
|
inputSchema: {
|
|
2306
|
-
deviceId:
|
|
2530
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2307
2531
|
}
|
|
2308
2532
|
},
|
|
2309
2533
|
async ({ deviceId }) => {
|
|
2310
2534
|
try {
|
|
2311
2535
|
const target = sync.requireProDevice(deviceId);
|
|
2312
2536
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2313
|
-
return
|
|
2537
|
+
return text12("Cleared js-top stats.");
|
|
2314
2538
|
} catch (e) {
|
|
2315
|
-
return
|
|
2539
|
+
return fail14(errMsg14(e));
|
|
2316
2540
|
}
|
|
2317
2541
|
}
|
|
2318
2542
|
);
|
|
2319
2543
|
}
|
|
2320
2544
|
|
|
2321
2545
|
// src/tools/packs/images.ts
|
|
2322
|
-
var
|
|
2546
|
+
var import_zod15 = require("zod");
|
|
2323
2547
|
var IMAGES_TOOL_ID = "images";
|
|
2324
|
-
var
|
|
2325
|
-
var
|
|
2326
|
-
var
|
|
2548
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2549
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2550
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2327
2551
|
function uriTail(uri) {
|
|
2328
2552
|
if (!uri) return "(no source)";
|
|
2329
2553
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2411,9 +2635,9 @@ function registerImagesTools(server, sync) {
|
|
|
2411
2635
|
{
|
|
2412
2636
|
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.",
|
|
2413
2637
|
inputSchema: {
|
|
2414
|
-
deviceId:
|
|
2415
|
-
limit:
|
|
2416
|
-
status:
|
|
2638
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2639
|
+
limit: import_zod15.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
2640
|
+
status: import_zod15.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2417
2641
|
}
|
|
2418
2642
|
},
|
|
2419
2643
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2426,7 +2650,7 @@ function registerImagesTools(server, sync) {
|
|
|
2426
2650
|
{ limit: capped, status },
|
|
2427
2651
|
target
|
|
2428
2652
|
);
|
|
2429
|
-
if (!list) return
|
|
2653
|
+
if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2430
2654
|
const md = formatListMarkdown(list, capped);
|
|
2431
2655
|
const slim = {
|
|
2432
2656
|
stats: list.stats,
|
|
@@ -2451,13 +2675,13 @@ function registerImagesTools(server, sync) {
|
|
|
2451
2675
|
loadCount: r.loadCount
|
|
2452
2676
|
}))
|
|
2453
2677
|
};
|
|
2454
|
-
return
|
|
2678
|
+
return text13(`${md}
|
|
2455
2679
|
|
|
2456
2680
|
\`\`\`json
|
|
2457
2681
|
${JSON.stringify(slim)}
|
|
2458
2682
|
\`\`\``);
|
|
2459
2683
|
} catch (e) {
|
|
2460
|
-
return
|
|
2684
|
+
return fail15(errMsg15(e));
|
|
2461
2685
|
}
|
|
2462
2686
|
}
|
|
2463
2687
|
);
|
|
@@ -2466,9 +2690,9 @@ ${JSON.stringify(slim)}
|
|
|
2466
2690
|
{
|
|
2467
2691
|
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.",
|
|
2468
2692
|
inputSchema: {
|
|
2469
|
-
deviceId:
|
|
2470
|
-
id:
|
|
2471
|
-
action:
|
|
2693
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2694
|
+
id: import_zod15.z.number().describe("Record id from get_images."),
|
|
2695
|
+
action: import_zod15.z.enum([
|
|
2472
2696
|
"hard_reload",
|
|
2473
2697
|
"retry",
|
|
2474
2698
|
"evict_disk",
|
|
@@ -2480,7 +2704,7 @@ ${JSON.stringify(slim)}
|
|
|
2480
2704
|
"swap_url",
|
|
2481
2705
|
"restore"
|
|
2482
2706
|
]).describe("What to do."),
|
|
2483
|
-
url:
|
|
2707
|
+
url: import_zod15.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
2484
2708
|
}
|
|
2485
2709
|
},
|
|
2486
2710
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2488,8 +2712,8 @@ ${JSON.stringify(slim)}
|
|
|
2488
2712
|
const target = sync.requireProDevice(deviceId);
|
|
2489
2713
|
if (action === "prove_savings") {
|
|
2490
2714
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2491
|
-
if (!savings) return
|
|
2492
|
-
if (!savings.ok) return
|
|
2715
|
+
if (!savings) return fail15("Device returned nothing for proveSavings.");
|
|
2716
|
+
if (!savings.ok) return fail15(savings.message);
|
|
2493
2717
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2494
2718
|
if (savings.originalBytes != null)
|
|
2495
2719
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -2501,11 +2725,11 @@ ${JSON.stringify(slim)}
|
|
|
2501
2725
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2502
2726
|
if (savings.optimizedUri)
|
|
2503
2727
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2504
|
-
return
|
|
2728
|
+
return text13(parts.join("\n"));
|
|
2505
2729
|
}
|
|
2506
2730
|
const entry = ACTION_MAP[action];
|
|
2507
|
-
if (!entry) return
|
|
2508
|
-
if (action === "swap_url" && !url) return
|
|
2731
|
+
if (!entry) return fail15(`Unknown action: ${action}`);
|
|
2732
|
+
if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
|
|
2509
2733
|
const result = await sync.callAction(
|
|
2510
2734
|
IMAGES_TOOL_ID,
|
|
2511
2735
|
entry.action,
|
|
@@ -2525,9 +2749,9 @@ ${JSON.stringify(slim)}
|
|
|
2525
2749
|
);
|
|
2526
2750
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2527
2751
|
}
|
|
2528
|
-
return
|
|
2752
|
+
return text13(lines.join("\n"));
|
|
2529
2753
|
} catch (e) {
|
|
2530
|
-
return
|
|
2754
|
+
return fail15(errMsg15(e));
|
|
2531
2755
|
}
|
|
2532
2756
|
}
|
|
2533
2757
|
);
|
|
@@ -2536,12 +2760,12 @@ ${JSON.stringify(slim)}
|
|
|
2536
2760
|
{
|
|
2537
2761
|
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.",
|
|
2538
2762
|
inputSchema: {
|
|
2539
|
-
deviceId:
|
|
2540
|
-
network:
|
|
2541
|
-
blankImages:
|
|
2542
|
-
massAction:
|
|
2543
|
-
clearRegistry:
|
|
2544
|
-
clearExpoCaches:
|
|
2763
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2764
|
+
network: import_zod15.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
2765
|
+
blankImages: import_zod15.z.boolean().optional().describe("Render all images without a source."),
|
|
2766
|
+
massAction: import_zod15.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
2767
|
+
clearRegistry: import_zod15.z.boolean().optional().describe("Clear the captured image log."),
|
|
2768
|
+
clearExpoCaches: import_zod15.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
2545
2769
|
}
|
|
2546
2770
|
},
|
|
2547
2771
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -2568,10 +2792,94 @@ ${JSON.stringify(slim)}
|
|
|
2568
2792
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
2569
2793
|
done.push(r?.message ?? "expo caches cleared");
|
|
2570
2794
|
}
|
|
2571
|
-
if (done.length === 0) return
|
|
2572
|
-
return
|
|
2795
|
+
if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
2796
|
+
return text13(`Applied: ${done.join(" \xB7 ")}`);
|
|
2573
2797
|
} catch (e) {
|
|
2574
|
-
return
|
|
2798
|
+
return fail15(errMsg15(e));
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// src/tools/packs/reload.ts
|
|
2805
|
+
var import_zod16 = require("zod");
|
|
2806
|
+
var APP_TOOL_ID = "app";
|
|
2807
|
+
var RELOAD_ACTION = "reloadApp";
|
|
2808
|
+
var DEFAULT_WAIT_MS = 3e4;
|
|
2809
|
+
var TEARDOWN_GRACE_MS = 1200;
|
|
2810
|
+
var PROBE_TIMEOUT_MS = 1500;
|
|
2811
|
+
var PROBE_INTERVAL_MS = 400;
|
|
2812
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2813
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2814
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2815
|
+
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2816
|
+
async function ping(sync, deviceId, timeoutMs) {
|
|
2817
|
+
try {
|
|
2818
|
+
const pong = await sync.callAction(APP_TOOL_ID, "ping", {}, deviceId, { timeoutMs });
|
|
2819
|
+
return typeof pong?.startedAt === "number" ? pong.startedAt : null;
|
|
2820
|
+
} catch {
|
|
2821
|
+
return null;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function assertReloadSupported(sync, deviceId) {
|
|
2825
|
+
if (deviceId === ALL_DEVICES) return;
|
|
2826
|
+
const entry = sync.toolActions(APP_TOOL_ID).find((t) => t.deviceId === deviceId);
|
|
2827
|
+
if (!entry?.actions.includes(RELOAD_ACTION)) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`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.`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
async function waitForAppBack(sync, deviceId, timeoutMs, previousRealm) {
|
|
2834
|
+
const waitStart = Date.now();
|
|
2835
|
+
await delay3(TEARDOWN_GRACE_MS);
|
|
2836
|
+
while (Date.now() - waitStart < timeoutMs) {
|
|
2837
|
+
const realm = await ping(sync, deviceId, PROBE_TIMEOUT_MS);
|
|
2838
|
+
if (realm !== null && realm !== previousRealm) return Date.now() - waitStart;
|
|
2839
|
+
await delay3(PROBE_INTERVAL_MS);
|
|
2840
|
+
}
|
|
2841
|
+
return null;
|
|
2842
|
+
}
|
|
2843
|
+
function registerReloadTools(server, sync) {
|
|
2844
|
+
server.registerTool(
|
|
2845
|
+
"reload_app",
|
|
2846
|
+
{
|
|
2847
|
+
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.",
|
|
2848
|
+
inputSchema: {
|
|
2849
|
+
strategy: import_zod16.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
2850
|
+
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
2851
|
+
),
|
|
2852
|
+
wait: import_zod16.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
2853
|
+
timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
2854
|
+
deviceId: import_zod16.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
2855
|
+
}
|
|
2856
|
+
},
|
|
2857
|
+
async (args) => {
|
|
2858
|
+
try {
|
|
2859
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2860
|
+
assertReloadSupported(sync, target);
|
|
2861
|
+
const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
|
|
2862
|
+
const ack = await sync.callAction(
|
|
2863
|
+
APP_TOOL_ID,
|
|
2864
|
+
RELOAD_ACTION,
|
|
2865
|
+
{ strategy: args.strategy ?? "auto" },
|
|
2866
|
+
target
|
|
2867
|
+
);
|
|
2868
|
+
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
2869
|
+
if (args.wait === false) {
|
|
2870
|
+
return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
2871
|
+
}
|
|
2872
|
+
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
2873
|
+
if (elapsed == null) {
|
|
2874
|
+
return fail16(
|
|
2875
|
+
`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.`
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2878
|
+
return text14(
|
|
2879
|
+
`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.`
|
|
2880
|
+
);
|
|
2881
|
+
} catch (e) {
|
|
2882
|
+
return fail16(errMsg16(e));
|
|
2575
2883
|
}
|
|
2576
2884
|
}
|
|
2577
2885
|
);
|
|
@@ -2590,6 +2898,13 @@ filter (e.g. sources:["network"] to list HTTP requests). It's the universal, tok
|
|
|
2590
2898
|
activity reader across ALL tools and is compact by default \u2014 reach for it before reading source
|
|
2591
2899
|
code, and prefer it over \`get_snapshot\` for any "what happened / show me the requests" question.
|
|
2592
2900
|
|
|
2901
|
+
Working on ONE HTTP request rather than skimming activity? \`get_events\` deliberately emits no
|
|
2902
|
+
request ids, so use \`get_network_requests\` \u2014 same compact style, but it leads with the id every
|
|
2903
|
+
network action is keyed by, and marks which requests are pinned/saved. \`flagged:'any'\` lists the
|
|
2904
|
+
requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-request cap and app
|
|
2905
|
+
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
2906
|
+
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
2907
|
+
|
|
2593
2908
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
2594
2909
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
2595
2910
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -2608,6 +2923,11 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
2608
2923
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
2609
2924
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
2610
2925
|
|
|
2926
|
+
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
2927
|
+
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
2928
|
+
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
2929
|
+
re-read anything you cached.
|
|
2930
|
+
|
|
2611
2931
|
For everything else:
|
|
2612
2932
|
- \`list_tool_actions\` to see what a tool can do,
|
|
2613
2933
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -2627,6 +2947,7 @@ function createMcpServer(sync) {
|
|
|
2627
2947
|
registerGenericTools(server, sync);
|
|
2628
2948
|
registerPerfMonitorTools(server, sync);
|
|
2629
2949
|
registerEventsTools(server, sync);
|
|
2950
|
+
registerNetworkTools(server, sync);
|
|
2630
2951
|
registerReactQueryTools(server, sync);
|
|
2631
2952
|
registerZustandTools(server, sync);
|
|
2632
2953
|
registerJotaiTools(server, sync);
|
|
@@ -2638,22 +2959,27 @@ function createMcpServer(sync) {
|
|
|
2638
2959
|
registerInteractTools(server, sync);
|
|
2639
2960
|
registerJsThreadTools(server, sync);
|
|
2640
2961
|
registerImagesTools(server, sync);
|
|
2962
|
+
registerReloadTools(server, sync);
|
|
2641
2963
|
return server;
|
|
2642
2964
|
}
|
|
2643
2965
|
var deviceIdField = {
|
|
2644
|
-
deviceId:
|
|
2966
|
+
deviceId: import_zod17.z.string().optional().describe(
|
|
2645
2967
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
2646
2968
|
)
|
|
2647
2969
|
};
|
|
2648
2970
|
|
|
2649
2971
|
// src/init.ts
|
|
2972
|
+
var import_node_child_process2 = require("child_process");
|
|
2650
2973
|
var import_node_fs3 = __toESM(require("fs"));
|
|
2974
|
+
var import_node_os3 = __toESM(require("os"));
|
|
2651
2975
|
var import_node_path3 = __toESM(require("path"));
|
|
2976
|
+
var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
2977
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
2652
2978
|
function runInit(opts = {}) {
|
|
2653
2979
|
const cwd = opts.cwd ?? process.cwd();
|
|
2654
2980
|
const skillSrcDir = opts.skillSrcDir ?? import_node_path3.default.resolve(__dirname, "../skill");
|
|
2655
2981
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
2656
|
-
const entry = opts
|
|
2982
|
+
const entry = resolveEntry(opts, log);
|
|
2657
2983
|
const done = [];
|
|
2658
2984
|
upsertMcpConfig(import_node_path3.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
2659
2985
|
if (import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, ".cursor"))) {
|
|
@@ -2670,12 +2996,125 @@ function runInit(opts = {}) {
|
|
|
2670
2996
|
log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
|
|
2671
2997
|
log(" 2. Open your app with Buoy devtools running on a device or simulator.");
|
|
2672
2998
|
log(' 3. Say "buoy optimize" to start a guided performance pass.');
|
|
2673
|
-
if (
|
|
2999
|
+
if (entry.command === "npx") {
|
|
2674
3000
|
log("");
|
|
2675
3001
|
log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
|
|
2676
3002
|
log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
|
|
3003
|
+
} else if (!opts.cliPath) {
|
|
3004
|
+
log("");
|
|
3005
|
+
log(`This uses a pinned local install (no network on startup). To update it later,`);
|
|
3006
|
+
log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
|
|
2677
3007
|
}
|
|
2678
3008
|
}
|
|
3009
|
+
function resolveEntry(opts, log) {
|
|
3010
|
+
if (opts.cliPath) {
|
|
3011
|
+
return { type: "stdio", command: "node", args: [opts.cliPath] };
|
|
3012
|
+
}
|
|
3013
|
+
const mode = opts.mode ?? "auto";
|
|
3014
|
+
if (mode === "npx") {
|
|
3015
|
+
return npxEntry();
|
|
3016
|
+
}
|
|
3017
|
+
if (mode === "local") {
|
|
3018
|
+
return localEntry(opts, log);
|
|
3019
|
+
}
|
|
3020
|
+
const probe = probeRegistry();
|
|
3021
|
+
if (probe.reachable) {
|
|
3022
|
+
return npxEntry();
|
|
3023
|
+
}
|
|
3024
|
+
log(
|
|
3025
|
+
`\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.`
|
|
3026
|
+
);
|
|
3027
|
+
return localEntry(opts, log);
|
|
3028
|
+
}
|
|
3029
|
+
function npxEntry() {
|
|
3030
|
+
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
|
|
3031
|
+
}
|
|
3032
|
+
function localEntry(opts, log) {
|
|
3033
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path3.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
|
|
3034
|
+
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
3035
|
+
const cliPath = installLocalCopy(installDir, registry, log);
|
|
3036
|
+
if (cliPath) {
|
|
3037
|
+
return { type: "stdio", command: "node", args: [cliPath] };
|
|
3038
|
+
}
|
|
3039
|
+
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
3040
|
+
return npxEntry();
|
|
3041
|
+
}
|
|
3042
|
+
function installLocalCopy(installDir, registry, log) {
|
|
3043
|
+
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
3044
|
+
import_node_fs3.default.mkdirSync(installDir, { recursive: true });
|
|
3045
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3046
|
+
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
3047
|
+
const res = (0, import_node_child_process2.spawnSync)(
|
|
3048
|
+
npm,
|
|
3049
|
+
[
|
|
3050
|
+
"install",
|
|
3051
|
+
spec,
|
|
3052
|
+
"--prefix",
|
|
3053
|
+
installDir,
|
|
3054
|
+
"--registry",
|
|
3055
|
+
registry,
|
|
3056
|
+
`--@buoy-gg:registry=${registry}`,
|
|
3057
|
+
"--no-audit",
|
|
3058
|
+
"--no-fund",
|
|
3059
|
+
"--loglevel=error"
|
|
3060
|
+
],
|
|
3061
|
+
{ stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
|
|
3062
|
+
);
|
|
3063
|
+
if (res.status !== 0) {
|
|
3064
|
+
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
3065
|
+
return null;
|
|
3066
|
+
}
|
|
3067
|
+
const cliPath = import_node_path3.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
3068
|
+
if (!import_node_fs3.default.existsSync(cliPath)) {
|
|
3069
|
+
log(`\u26A0 install finished but ${cliPath} is missing.`);
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
return cliPath;
|
|
3073
|
+
}
|
|
3074
|
+
function probeRegistry() {
|
|
3075
|
+
const registry = effectiveRegistry();
|
|
3076
|
+
if (!registry) return { reachable: false, registry: null };
|
|
3077
|
+
if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
|
|
3078
|
+
return { reachable: true, registry };
|
|
3079
|
+
}
|
|
3080
|
+
return { reachable: hostReachable(registry), registry };
|
|
3081
|
+
}
|
|
3082
|
+
function effectiveRegistry() {
|
|
3083
|
+
const scoped = npmConfigGet("@buoy-gg:registry");
|
|
3084
|
+
if (scoped) return scoped;
|
|
3085
|
+
const global = npmConfigGet("registry");
|
|
3086
|
+
return global ?? PUBLIC_REGISTRY;
|
|
3087
|
+
}
|
|
3088
|
+
function npmConfigGet(key) {
|
|
3089
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3090
|
+
const res = (0, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 });
|
|
3091
|
+
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
3092
|
+
const val = res.stdout.trim();
|
|
3093
|
+
if (!val || val === "undefined" || val === "null") return null;
|
|
3094
|
+
return val;
|
|
3095
|
+
}
|
|
3096
|
+
function hostReachable(registry) {
|
|
3097
|
+
const done = (0, import_node_child_process2.spawnSync)(
|
|
3098
|
+
process.execPath,
|
|
3099
|
+
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS2)],
|
|
3100
|
+
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 + 2e3 }
|
|
3101
|
+
);
|
|
3102
|
+
return done.status === 0;
|
|
3103
|
+
}
|
|
3104
|
+
var REACHABILITY_PROBE = `
|
|
3105
|
+
const url = process.argv[1];
|
|
3106
|
+
const ms = Number(process.argv[2]) || 3000;
|
|
3107
|
+
const c = new AbortController();
|
|
3108
|
+
const t = setTimeout(() => c.abort(), ms);
|
|
3109
|
+
fetch(url, { method: 'HEAD', signal: c.signal })
|
|
3110
|
+
.then(() => { clearTimeout(t); process.exit(0); })
|
|
3111
|
+
.catch(() => {
|
|
3112
|
+
// A HEAD may be rejected while the host is up; a bare TCP-level failure is
|
|
3113
|
+
// what we care about. fetch throwing a TypeError = network unreachable.
|
|
3114
|
+
clearTimeout(t);
|
|
3115
|
+
process.exit(1);
|
|
3116
|
+
});
|
|
3117
|
+
`;
|
|
2679
3118
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
2680
3119
|
let json3 = {};
|
|
2681
3120
|
if (import_node_fs3.default.existsSync(file)) {
|
|
@@ -2762,7 +3201,13 @@ async function main() {
|
|
|
2762
3201
|
}
|
|
2763
3202
|
if (process.argv[2] === "init") {
|
|
2764
3203
|
try {
|
|
2765
|
-
|
|
3204
|
+
const args = process.argv.slice(3);
|
|
3205
|
+
let mode = "auto";
|
|
3206
|
+
if (args.includes("--local")) mode = "local";
|
|
3207
|
+
else if (args.includes("--npx")) mode = "npx";
|
|
3208
|
+
const ri = args.indexOf("--registry");
|
|
3209
|
+
const registry = ri !== -1 ? args[ri + 1] : void 0;
|
|
3210
|
+
runInit({ mode, registry });
|
|
2766
3211
|
process.exit(0);
|
|
2767
3212
|
} catch (err) {
|
|
2768
3213
|
process.stderr.write(`[buoy-mcp] init failed: ${err?.message ?? err}
|