@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/dist/index.js CHANGED
@@ -360,7 +360,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
360
360
 
361
361
  // src/server.ts
362
362
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
363
- var import_zod15 = require("zod");
363
+ var import_zod17 = require("zod");
364
364
 
365
365
  // src/tools/generic.ts
366
366
  var import_zod = require("zod");
@@ -371,7 +371,7 @@ var import_node_os = __toESM(require("os"));
371
371
  var import_node_path = __toESM(require("path"));
372
372
 
373
373
  // src/version.ts
374
- var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
374
+ var VERSION = "7.0.5" ? "7.0.5" : "0.0.0-dev";
375
375
  var PACKAGE_NAME = "@buoy-gg/mcp";
376
376
 
377
377
  // src/update/checkForUpdate.ts
@@ -1320,12 +1320,233 @@ _Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
1320
1320
  );
1321
1321
  }
1322
1322
 
1323
- // src/tools/packs/reactQuery.ts
1323
+ // src/tools/packs/network.ts
1324
1324
  var import_zod4 = require("zod");
1325
- var RQ_TOOL_ID = "react-query";
1325
+ var NETWORK_TOOL_ID = "network";
1326
1326
  var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1327
1327
  var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1328
1328
  var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1329
+ function parseSnapshot(payload) {
1330
+ if (Array.isArray(payload)) return { events: payload, saved: [] };
1331
+ if (payload && typeof payload === "object") {
1332
+ const shaped = payload;
1333
+ return {
1334
+ events: Array.isArray(shaped.events) ? shaped.events : [],
1335
+ saved: Array.isArray(shaped.saved) ? shaped.saved : []
1336
+ };
1337
+ }
1338
+ return { events: [], saved: [] };
1339
+ }
1340
+ function statusIcon(e) {
1341
+ if (e.error) return "\u274C";
1342
+ if (e.status === void 0) return "\u23F3";
1343
+ if (e.status >= 400) return "\u274C";
1344
+ if (e.status >= 300) return "\u21AA\uFE0F";
1345
+ return "\u2705";
1346
+ }
1347
+ function statusLabel(e) {
1348
+ if (e.error) return `ERR ${e.error}`;
1349
+ if (e.status === void 0) return "pending";
1350
+ return String(e.status);
1351
+ }
1352
+ function formatBytes(n) {
1353
+ if (!n) return "";
1354
+ if (n < 1024) return `${n}B`;
1355
+ if (n < 1024 * 1024) return `${Math.round(n / 1024)}KB`;
1356
+ return `${(n / (1024 * 1024)).toFixed(1)}MB`;
1357
+ }
1358
+ function matchesStatus(e, status) {
1359
+ if (!status || status === "all") return true;
1360
+ const failed = !!e.error || e.status !== void 0 && e.status >= 400;
1361
+ if (status === "errors") return failed;
1362
+ if (status === "pending") return e.status === void 0 && !e.error;
1363
+ if (status === "success") return !failed && e.status !== void 0;
1364
+ return true;
1365
+ }
1366
+ function truncate(v, n = 600) {
1367
+ if (v === null || v === void 0) return "(none)";
1368
+ const s = typeof v === "string" ? v : JSON.stringify(v);
1369
+ return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
1370
+ }
1371
+ function formatRow(index, e, markers) {
1372
+ const where = e.path || e.url;
1373
+ const bits = [
1374
+ `${e.duration !== void 0 ? `${Math.round(e.duration)}ms` : "\u2014"}`,
1375
+ formatBytes(e.responseSize),
1376
+ e.host ?? "",
1377
+ e.requestClient ?? ""
1378
+ ].filter(Boolean);
1379
+ const op = e.operationName ? ` (${e.operationName})` : "";
1380
+ return `${index}. \`${e.id}\` ${statusIcon(e)} ${statusLabel(e)} ${e.method} ${where}${op}${markers ? ` ${markers}` : ""}
1381
+ \u2514\u2500 ${bits.join(" \xB7 ")}`;
1382
+ }
1383
+ function registerNetworkTools(server, sync) {
1384
+ server.registerTool(
1385
+ "get_network_requests",
1386
+ {
1387
+ 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).",
1388
+ inputSchema: {
1389
+ limit: import_zod4.z.number().optional().describe("Most-recent N requests. Default 25."),
1390
+ pattern: import_zod4.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
1391
+ status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
1392
+ 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."),
1393
+ includeBodies: import_zod4.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
1394
+ deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1395
+ }
1396
+ },
1397
+ async (args) => {
1398
+ try {
1399
+ const target = sync.requireProDevice(args.deviceId);
1400
+ const { payload } = await sync.getSnapshot(NETWORK_TOOL_ID, target);
1401
+ const { events, saved } = parseSnapshot(payload);
1402
+ const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
1403
+ const pattern = args.pattern?.toLowerCase();
1404
+ const pinnedIds = /* @__PURE__ */ new Set();
1405
+ const savedIds = /* @__PURE__ */ new Set();
1406
+ for (const r of saved) {
1407
+ if (r.pinned) pinnedIds.add(r.event?.id);
1408
+ if (r.saved) savedIds.add(r.event?.id);
1409
+ if (r.pinned) pinnedIds.add(r.liveId);
1410
+ if (r.saved) savedIds.add(r.liveId);
1411
+ }
1412
+ if (args.flagged) {
1413
+ const wanted = saved.filter(
1414
+ (r) => args.flagged === "pinned" ? r.pinned : args.flagged === "saved" ? r.saved : r.pinned || r.saved
1415
+ );
1416
+ const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1417
+ if (rows2.length === 0) {
1418
+ return text3(
1419
+ `No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
1420
+
1421
+ 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.`
1422
+ );
1423
+ }
1424
+ const body2 = rows2.map((r, i) => {
1425
+ const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
1426
+ const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
1427
+ const trunc = r.bodyTruncated ? " \xB7 body truncated" : "";
1428
+ let out = formatRow(i + 1, r.event, marks);
1429
+ out += `
1430
+ \u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
1431
+ if (args.includeBodies) {
1432
+ out += `
1433
+ \u2514\u2500 request: ${truncate(r.event.requestData)}`;
1434
+ out += `
1435
+ \u2514\u2500 response: ${truncate(r.event.responseData)}`;
1436
+ }
1437
+ return out;
1438
+ }).join("\n");
1439
+ return text3(
1440
+ `**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
1441
+
1442
+ ${body2}
1443
+
1444
+ _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._`
1445
+ );
1446
+ }
1447
+ const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1448
+ if (rows.length === 0) {
1449
+ 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.";
1450
+ return text3(`No requests on ${target}. ${why}`);
1451
+ }
1452
+ const body = rows.map((e, i) => {
1453
+ const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
1454
+ let out = formatRow(i + 1, e, marks);
1455
+ if (args.includeBodies) {
1456
+ out += `
1457
+ \u2514\u2500 request: ${truncate(e.requestData)}`;
1458
+ out += `
1459
+ \u2514\u2500 response: ${truncate(e.responseData)}`;
1460
+ }
1461
+ return out;
1462
+ }).join("\n");
1463
+ const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
1464
+ return text3(
1465
+ `**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
1466
+
1467
+ ${body}
1468
+
1469
+ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
1470
+ );
1471
+ } catch (e) {
1472
+ return fail4(errMsg4(e));
1473
+ }
1474
+ }
1475
+ );
1476
+ server.registerTool(
1477
+ "network_action",
1478
+ {
1479
+ 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.",
1480
+ inputSchema: {
1481
+ 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)."),
1482
+ id: import_zod4.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
1483
+ key: import_zod4.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
1484
+ deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1485
+ }
1486
+ },
1487
+ async (args) => {
1488
+ try {
1489
+ const target = sync.requireProDevice(args.deviceId);
1490
+ const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
1491
+ if (needsId && !args.id) {
1492
+ return fail4(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
1493
+ }
1494
+ if (args.action === "remove" && !args.key) {
1495
+ return fail4('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
1496
+ }
1497
+ switch (args.action) {
1498
+ case "pin":
1499
+ case "unpin": {
1500
+ const res = await sync.callAction(
1501
+ NETWORK_TOOL_ID,
1502
+ "setPinned",
1503
+ { id: args.id, pinned: args.action === "pin" },
1504
+ target
1505
+ );
1506
+ 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.`);
1507
+ const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
1508
+ if (capped) return fail4(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
1509
+ return text3(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
1510
+ }
1511
+ case "save":
1512
+ case "unsave": {
1513
+ const res = await sync.callAction(
1514
+ NETWORK_TOOL_ID,
1515
+ "setSaved",
1516
+ { id: args.id, saved: args.action === "save" },
1517
+ target
1518
+ );
1519
+ 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.`);
1520
+ const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
1521
+ if (capped) return fail4(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
1522
+ return text3(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
1523
+ }
1524
+ case "remove":
1525
+ await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
1526
+ return text3(`Removed kept record \`${args.key}\` on ${target}.`);
1527
+ case "clearPinned":
1528
+ await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
1529
+ return text3(`Unpinned all requests on ${target} (saved ones kept).`);
1530
+ case "clearSaved":
1531
+ await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
1532
+ return text3(`Cleared saved requests on ${target} (pinned ones kept).`);
1533
+ case "clearRequests":
1534
+ await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
1535
+ return text3(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
1536
+ }
1537
+ } catch (e) {
1538
+ return fail4(errMsg4(e));
1539
+ }
1540
+ }
1541
+ );
1542
+ }
1543
+
1544
+ // src/tools/packs/reactQuery.ts
1545
+ var import_zod5 = require("zod");
1546
+ var RQ_TOOL_ID = "react-query";
1547
+ var text4 = (s) => ({ content: [{ type: "text", text: s }] });
1548
+ var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1549
+ var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
1329
1550
  var STATUS_ICON = {
1330
1551
  fresh: "\u{1F7E2}",
1331
1552
  stale: "\u{1F7E1}",
@@ -1349,10 +1570,10 @@ function registerReactQueryTools(server, sync) {
1349
1570
  {
1350
1571
  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.",
1351
1572
  inputSchema: {
1352
- includeData: import_zod4.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
1353
- staleOnly: import_zod4.z.boolean().optional().describe("Only stale queries. Default false."),
1354
- limit: import_zod4.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
1355
- deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1573
+ includeData: import_zod5.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
1574
+ staleOnly: import_zod5.z.boolean().optional().describe("Only stale queries. Default false."),
1575
+ limit: import_zod5.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
1576
+ deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1356
1577
  }
1357
1578
  },
1358
1579
  async (args) => {
@@ -1365,12 +1586,12 @@ function registerReactQueryTools(server, sync) {
1365
1586
  target
1366
1587
  );
1367
1588
  if (!res || !Array.isArray(res.queries)) {
1368
- return fail4(
1589
+ return fail5(
1369
1590
  "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)."
1370
1591
  );
1371
1592
  }
1372
1593
  if (res.queries.length === 0) {
1373
- return text3(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
1594
+ return text4(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
1374
1595
  }
1375
1596
  const lines = res.queries.map((q, i) => {
1376
1597
  const icon = STATUS_ICON[q.status] ?? "\u2022";
@@ -1383,11 +1604,11 @@ function registerReactQueryTools(server, sync) {
1383
1604
  const staleCount = res.queries.filter((q) => q.isStale).length;
1384
1605
  const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
1385
1606
  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._";
1386
- return text3(`${header}
1607
+ return text4(`${header}
1387
1608
 
1388
1609
  ${lines.join("\n")}${note}`);
1389
1610
  } catch (e) {
1390
- return fail4(errMsg4(e));
1611
+ return fail5(errMsg5(e));
1391
1612
  }
1392
1613
  }
1393
1614
  );
@@ -1396,9 +1617,9 @@ ${lines.join("\n")}${note}`);
1396
1617
  {
1397
1618
  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.",
1398
1619
  inputSchema: {
1399
- action: import_zod4.z.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
1400
- queryHash: import_zod4.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
1401
- deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1620
+ 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."),
1621
+ 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."),
1622
+ deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1402
1623
  }
1403
1624
  },
1404
1625
  async (args) => {
@@ -1406,35 +1627,35 @@ ${lines.join("\n")}${note}`);
1406
1627
  const target = sync.requireProDevice(args.deviceId);
1407
1628
  if (args.action === "clear") {
1408
1629
  await sync.callAction(RQ_TOOL_ID, "clearQueryCache", {}, target);
1409
- return text3("Cleared the entire React Query cache.");
1630
+ return text4("Cleared the entire React Query cache.");
1410
1631
  }
1411
1632
  if (!args.queryHash) {
1412
- return fail4(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
1633
+ return fail5(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
1413
1634
  }
1414
1635
  await sync.callAction(RQ_TOOL_ID, args.action, { queryHash: args.queryHash }, target);
1415
- return text3(`Ran ${args.action} on query \`${args.queryHash}\`.`);
1636
+ return text4(`Ran ${args.action} on query \`${args.queryHash}\`.`);
1416
1637
  } catch (e) {
1417
- return fail4(errMsg4(e));
1638
+ return fail5(errMsg5(e));
1418
1639
  }
1419
1640
  }
1420
1641
  );
1421
1642
  }
1422
1643
 
1423
1644
  // src/tools/packs/zustand.ts
1424
- var import_zod5 = require("zod");
1645
+ var import_zod6 = require("zod");
1425
1646
  var ZUSTAND_TOOL_ID = "zustand";
1426
- var text4 = (s) => ({ content: [{ type: "text", text: s }] });
1427
- var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1428
- var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
1647
+ var text5 = (s) => ({ content: [{ type: "text", text: s }] });
1648
+ var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1649
+ var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
1429
1650
  function registerZustandTools(server, sync) {
1430
1651
  server.registerTool(
1431
1652
  "get_zustand_state",
1432
1653
  {
1433
1654
  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'].)",
1434
1655
  inputSchema: {
1435
- includeValues: import_zod5.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
1436
- limit: import_zod5.z.number().optional().describe("First N stores. Default all."),
1437
- deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1656
+ includeValues: import_zod6.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
1657
+ limit: import_zod6.z.number().optional().describe("First N stores. Default all."),
1658
+ deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1438
1659
  }
1439
1660
  },
1440
1661
  async (args) => {
@@ -1447,12 +1668,12 @@ function registerZustandTools(server, sync) {
1447
1668
  target
1448
1669
  );
1449
1670
  if (!res || !Array.isArray(res.stores)) {
1450
- return fail5(
1671
+ return fail6(
1451
1672
  "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)."
1452
1673
  );
1453
1674
  }
1454
1675
  if (res.stores.length === 0) {
1455
- return text4("No Zustand stores are registered (call watchStores to register them).");
1676
+ return text5("No Zustand stores are registered (call watchStores to register them).");
1456
1677
  }
1457
1678
  const lines = res.stores.map((s, i) => {
1458
1679
  const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
@@ -1461,11 +1682,11 @@ function registerZustandTools(server, sync) {
1461
1682
  return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
1462
1683
  });
1463
1684
  const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
1464
- return text4(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
1685
+ return text5(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
1465
1686
 
1466
1687
  ${lines.join("\n")}${note}`);
1467
1688
  } catch (e) {
1468
- return fail5(errMsg5(e));
1689
+ return fail6(errMsg6(e));
1469
1690
  }
1470
1691
  }
1471
1692
  );
@@ -1474,10 +1695,10 @@ ${lines.join("\n")}${note}`);
1474
1695
  {
1475
1696
  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.",
1476
1697
  inputSchema: {
1477
- store: import_zod5.z.string().describe("Store name to target (from get_zustand_state)."),
1478
- state: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).describe("The new state object."),
1479
- replace: import_zod5.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
1480
- deviceId: import_zod5.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1698
+ store: import_zod6.z.string().describe("Store name to target (from get_zustand_state)."),
1699
+ state: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()).describe("The new state object."),
1700
+ replace: import_zod6.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
1701
+ deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1481
1702
  }
1482
1703
  },
1483
1704
  async (args) => {
@@ -1489,29 +1710,29 @@ ${lines.join("\n")}${note}`);
1489
1710
  { storeName: args.store, state: args.state, replace: args.replace ?? true },
1490
1711
  target
1491
1712
  );
1492
- return text4(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
1713
+ return text5(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
1493
1714
  } catch (e) {
1494
- return fail5(errMsg5(e));
1715
+ return fail6(errMsg6(e));
1495
1716
  }
1496
1717
  }
1497
1718
  );
1498
1719
  }
1499
1720
 
1500
1721
  // src/tools/packs/jotai.ts
1501
- var import_zod6 = require("zod");
1722
+ var import_zod7 = require("zod");
1502
1723
  var JOTAI_TOOL_ID = "jotai";
1503
- var text5 = (s) => ({ content: [{ type: "text", text: s }] });
1504
- var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1505
- var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
1724
+ var text6 = (s) => ({ content: [{ type: "text", text: s }] });
1725
+ var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1726
+ var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
1506
1727
  function registerJotaiTools(server, sync) {
1507
1728
  server.registerTool(
1508
1729
  "get_jotai_state",
1509
1730
  {
1510
1731
  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'].)",
1511
1732
  inputSchema: {
1512
- includeValues: import_zod6.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
1513
- limit: import_zod6.z.number().optional().describe("First N atoms. Default all."),
1514
- deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1733
+ includeValues: import_zod7.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
1734
+ limit: import_zod7.z.number().optional().describe("First N atoms. Default all."),
1735
+ deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1515
1736
  }
1516
1737
  },
1517
1738
  async (args) => {
@@ -1524,12 +1745,12 @@ function registerJotaiTools(server, sync) {
1524
1745
  target
1525
1746
  );
1526
1747
  if (!res || !Array.isArray(res.atoms)) {
1527
- return fail6(
1748
+ return fail7(
1528
1749
  "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)."
1529
1750
  );
1530
1751
  }
1531
1752
  if (res.atoms.length === 0) {
1532
- return text5("No Jotai atoms are being watched (call watchAtoms to register them).");
1753
+ return text6("No Jotai atoms are being watched (call watchAtoms to register them).");
1533
1754
  }
1534
1755
  const lines = res.atoms.map((a, i) => {
1535
1756
  const kind = a.writable ? "writable" : "read-only";
@@ -1538,11 +1759,11 @@ function registerJotaiTools(server, sync) {
1538
1759
  return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
1539
1760
  });
1540
1761
  const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
1541
- return text5(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
1762
+ return text6(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
1542
1763
 
1543
1764
  ${lines.join("\n")}${note}`);
1544
1765
  } catch (e) {
1545
- return fail6(errMsg6(e));
1766
+ return fail7(errMsg7(e));
1546
1767
  }
1547
1768
  }
1548
1769
  );
@@ -1551,29 +1772,29 @@ ${lines.join("\n")}${note}`);
1551
1772
  {
1552
1773
  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.",
1553
1774
  inputSchema: {
1554
- atom: import_zod6.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
1555
- value: import_zod6.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
1556
- deviceId: import_zod6.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1775
+ atom: import_zod7.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
1776
+ value: import_zod7.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
1777
+ deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1557
1778
  }
1558
1779
  },
1559
1780
  async (args) => {
1560
1781
  try {
1561
1782
  const target = sync.requireProDevice(args.deviceId);
1562
1783
  await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
1563
- return text5(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
1784
+ return text6(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
1564
1785
  } catch (e) {
1565
- return fail6(errMsg6(e));
1786
+ return fail7(errMsg7(e));
1566
1787
  }
1567
1788
  }
1568
1789
  );
1569
1790
  }
1570
1791
 
1571
1792
  // src/tools/packs/redux.ts
1572
- var import_zod7 = require("zod");
1793
+ var import_zod8 = require("zod");
1573
1794
  var REDUX_TOOL_ID = "redux";
1574
- var text6 = (s) => ({ content: [{ type: "text", text: s }] });
1575
- var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1576
- var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
1795
+ var text7 = (s) => ({ content: [{ type: "text", text: s }] });
1796
+ var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1797
+ var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
1577
1798
  var NO_STORE = "No instrumented Redux store found. Open the app with Buoy redux devtools active (auto-instrumentation), then retry.";
1578
1799
  function registerReduxTools(server, sync) {
1579
1800
  server.registerTool(
@@ -1581,8 +1802,8 @@ function registerReduxTools(server, sync) {
1581
1802
  {
1582
1803
  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'].)",
1583
1804
  inputSchema: {
1584
- includeValues: import_zod7.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
1585
- deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1805
+ includeValues: import_zod8.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
1806
+ deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1586
1807
  }
1587
1808
  },
1588
1809
  async (args) => {
@@ -1595,26 +1816,26 @@ function registerReduxTools(server, sync) {
1595
1816
  target
1596
1817
  );
1597
1818
  if (!res) {
1598
- return fail7(
1819
+ return fail8(
1599
1820
  "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)."
1600
1821
  );
1601
1822
  }
1602
- if (!res.available) return text6(NO_STORE);
1823
+ if (!res.available) return text7(NO_STORE);
1603
1824
  const slices = res.slices.length ? res.slices.join(", ") : "(none)";
1604
1825
  if (args.includeValues) {
1605
- return text6(`**Redux store \u2014 slices: ${slices}**
1826
+ return text7(`**Redux store \u2014 slices: ${slices}**
1606
1827
 
1607
1828
  \`\`\`json
1608
1829
  ${JSON.stringify(res.state, null, 2)}
1609
1830
  \`\`\``);
1610
1831
  }
1611
- return text6(
1832
+ return text7(
1612
1833
  `**Redux store \u2014 slices: ${slices}**
1613
1834
 
1614
1835
  _Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._`
1615
1836
  );
1616
1837
  } catch (e) {
1617
- return fail7(errMsg7(e));
1838
+ return fail8(errMsg8(e));
1618
1839
  }
1619
1840
  }
1620
1841
  );
@@ -1623,9 +1844,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
1623
1844
  {
1624
1845
  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 }.",
1625
1846
  inputSchema: {
1626
- type: import_zod7.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
1627
- payload: import_zod7.z.unknown().optional().describe("Optional action payload (any JSON)."),
1628
- deviceId: import_zod7.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1847
+ type: import_zod8.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
1848
+ payload: import_zod8.z.unknown().optional().describe("Optional action payload (any JSON)."),
1849
+ deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1629
1850
  }
1630
1851
  },
1631
1852
  async (args) => {
@@ -1634,22 +1855,22 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
1634
1855
  const action = { type: args.type };
1635
1856
  if (args.payload !== void 0) action.payload = args.payload;
1636
1857
  const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
1637
- if (!res || !res.dispatched) return fail7(NO_STORE);
1638
- return text6(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
1858
+ if (!res || !res.dispatched) return fail8(NO_STORE);
1859
+ return text7(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
1639
1860
  } catch (e) {
1640
- return fail7(errMsg7(e));
1861
+ return fail8(errMsg8(e));
1641
1862
  }
1642
1863
  }
1643
1864
  );
1644
1865
  }
1645
1866
 
1646
1867
  // src/tools/packs/storage.ts
1647
- var import_zod8 = require("zod");
1868
+ var import_zod9 = require("zod");
1648
1869
  var STORAGE_TOOL_ID = "storage";
1649
- var text7 = (s) => ({ content: [{ type: "text", text: s }] });
1650
- var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1651
- var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
1652
- function truncate(v, n = 200) {
1870
+ var text8 = (s) => ({ content: [{ type: "text", text: s }] });
1871
+ var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1872
+ var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
1873
+ function truncate2(v, n = 200) {
1653
1874
  if (v === null || v === void 0) return "(null)";
1654
1875
  const s = typeof v === "string" ? v : JSON.stringify(v);
1655
1876
  return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
@@ -1660,10 +1881,10 @@ function registerStorageTools(server, sync) {
1660
1881
  {
1661
1882
  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'.",
1662
1883
  inputSchema: {
1663
- pattern: import_zod8.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
1664
- includeValues: import_zod8.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
1665
- limit: import_zod8.z.number().optional().describe("Max keys per backend section. Default 100."),
1666
- deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1884
+ pattern: import_zod9.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
1885
+ includeValues: import_zod9.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
1886
+ limit: import_zod9.z.number().optional().describe("Max keys per backend section. Default 100."),
1887
+ deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1667
1888
  }
1668
1889
  },
1669
1890
  async (args) => {
@@ -1675,7 +1896,7 @@ function registerStorageTools(server, sync) {
1675
1896
  const sections = [];
1676
1897
  const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
1677
1898
  if (!Array.isArray(allKeys)) {
1678
- return fail8("No storage result from the device. The storage tool may be inactive.");
1899
+ return fail9("No storage result from the device. The storage tool may be inactive.");
1679
1900
  }
1680
1901
  const asyncKeys = allKeys.filter(matches).slice(0, limit);
1681
1902
  if (asyncKeys.length > 0) {
@@ -1683,7 +1904,7 @@ function registerStorageTools(server, sync) {
1683
1904
  if (args.includeValues) {
1684
1905
  const pairs = await sync.callAction(STORAGE_TOOL_ID, "async.multiGet", { keys: asyncKeys }, target);
1685
1906
  const map = new Map((pairs ?? []).map(([k, v]) => [k, v]));
1686
- lines = asyncKeys.map((k) => `- \`${k}\` = ${truncate(map.get(k) ?? null)}`);
1907
+ lines = asyncKeys.map((k) => `- \`${k}\` = ${truncate2(map.get(k) ?? null)}`);
1687
1908
  } else {
1688
1909
  lines = asyncKeys.map((k) => `- \`${k}\``);
1689
1910
  }
@@ -1695,7 +1916,7 @@ ${lines.join("\n")}`);
1695
1916
  const entries = inst.entries.filter((e) => matches(e.key)).slice(0, limit);
1696
1917
  if (entries.length === 0) continue;
1697
1918
  const lines = entries.map(
1698
- (e) => args.includeValues ? `- \`${e.key}\` (${e.valueType}) = ${truncate(e.value)}` : `- \`${e.key}\``
1919
+ (e) => args.includeValues ? `- \`${e.key}\` (${e.valueType}) = ${truncate2(e.value)}` : `- \`${e.key}\``
1699
1920
  );
1700
1921
  const flags = [inst.encrypted && "encrypted", inst.readOnly && "read-only"].filter(Boolean).join(", ");
1701
1922
  sections.push(`**MMKV \`${inst.id}\`${flags ? ` (${flags})` : ""} (${entries.length}/${inst.entries.length} keys):**
@@ -1709,19 +1930,19 @@ ${lines.join("\n")}`);
1709
1930
  if (d.requireAuthentication) return `- \`${d.key}\` \u{1F512} (biometric-protected \u2014 not read)`;
1710
1931
  if (!args.includeValues) return `- \`${d.key}\``;
1711
1932
  const v = await sync.callAction(STORAGE_TOOL_ID, "secure.get", { key: d.key }, target).catch(() => null);
1712
- return `- \`${d.key}\` = ${truncate(v)}`;
1933
+ return `- \`${d.key}\` = ${truncate2(v)}`;
1713
1934
  })
1714
1935
  );
1715
1936
  sections.push(`**SecureStore (${secureKeys.length} registered keys):**
1716
1937
  ${lines.join("\n")}`);
1717
1938
  }
1718
1939
  if (sections.length === 0) {
1719
- return text7(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
1940
+ return text8(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
1720
1941
  }
1721
1942
  const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
1722
- return text7(`${sections.join("\n\n")}${note}`);
1943
+ return text8(`${sections.join("\n\n")}${note}`);
1723
1944
  } catch (e) {
1724
- return fail8(errMsg8(e));
1945
+ return fail9(errMsg9(e));
1725
1946
  }
1726
1947
  }
1727
1948
  );
@@ -1730,10 +1951,10 @@ ${lines.join("\n")}`);
1730
1951
  {
1731
1952
  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.",
1732
1953
  inputSchema: {
1733
- action: import_zod8.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
1734
- key: import_zod8.z.string().optional().describe("The key (required for set and remove)."),
1735
- value: import_zod8.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
1736
- deviceId: import_zod8.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1954
+ action: import_zod9.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
1955
+ key: import_zod9.z.string().optional().describe("The key (required for set and remove)."),
1956
+ value: import_zod9.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
1957
+ deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1737
1958
  }
1738
1959
  },
1739
1960
  async (args) => {
@@ -1741,33 +1962,33 @@ ${lines.join("\n")}`);
1741
1962
  const target = sync.requireProDevice(args.deviceId);
1742
1963
  if (args.action === "clear") {
1743
1964
  await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
1744
- return text7("Cleared all app storage (Buoy's own devtool keys preserved).");
1965
+ return text8("Cleared all app storage (Buoy's own devtool keys preserved).");
1745
1966
  }
1746
1967
  if (!args.key) {
1747
- return fail8(`'${args.action}' needs a key.`);
1968
+ return fail9(`'${args.action}' needs a key.`);
1748
1969
  }
1749
1970
  if (args.action === "remove") {
1750
1971
  await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
1751
- return text7(`Removed storage key "${args.key}".`);
1972
+ return text8(`Removed storage key "${args.key}".`);
1752
1973
  }
1753
1974
  if (args.value === void 0) {
1754
- return fail8("'set' needs a value (a string).");
1975
+ return fail9("'set' needs a value (a string).");
1755
1976
  }
1756
1977
  await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
1757
- return text7(`Set storage key "${args.key}".`);
1978
+ return text8(`Set storage key "${args.key}".`);
1758
1979
  } catch (e) {
1759
- return fail8(errMsg8(e));
1980
+ return fail9(errMsg9(e));
1760
1981
  }
1761
1982
  }
1762
1983
  );
1763
1984
  }
1764
1985
 
1765
1986
  // src/tools/packs/routes.ts
1766
- var import_zod9 = require("zod");
1987
+ var import_zod10 = require("zod");
1767
1988
  var ROUTES_TOOL_ID = "route-events";
1768
- var text8 = (s) => ({ content: [{ type: "text", text: s }] });
1769
- var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1770
- var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
1989
+ var text9 = (s) => ({ content: [{ type: "text", text: s }] });
1990
+ var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1991
+ var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
1771
1992
  function flattenRoutes(routes, acc = []) {
1772
1993
  for (const r of routes ?? []) {
1773
1994
  if (r.path) acc.push(r.path);
@@ -1781,8 +2002,8 @@ function registerRoutesTools(server, sync) {
1781
2002
  {
1782
2003
  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'].)",
1783
2004
  inputSchema: {
1784
- includeSitemap: import_zod9.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
1785
- deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2005
+ includeSitemap: import_zod10.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
2006
+ deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1786
2007
  }
1787
2008
  },
1788
2009
  async (args) => {
@@ -1815,9 +2036,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
1815
2036
  **Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
1816
2037
  }
1817
2038
  parts.push("\n_Navigate with navigate({ path })._");
1818
- return text8(parts.join("\n"));
2039
+ return text9(parts.join("\n"));
1819
2040
  } catch (e) {
1820
- return fail9(errMsg9(e));
2041
+ return fail10(errMsg10(e));
1821
2042
  }
1822
2043
  }
1823
2044
  );
@@ -1826,28 +2047,28 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
1826
2047
  {
1827
2048
  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.",
1828
2049
  inputSchema: {
1829
- path: import_zod9.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
1830
- deviceId: import_zod9.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2050
+ path: import_zod10.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2051
+ deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1831
2052
  }
1832
2053
  },
1833
2054
  async (args) => {
1834
2055
  try {
1835
2056
  const target = sync.requireProDevice(args.deviceId);
1836
2057
  await sync.callAction(ROUTES_TOOL_ID, "navigate", { path: args.path }, target);
1837
- return text8(`Navigated to \`${args.path}\`.`);
2058
+ return text9(`Navigated to \`${args.path}\`.`);
1838
2059
  } catch (e) {
1839
- return fail9(errMsg9(e));
2060
+ return fail10(errMsg10(e));
1840
2061
  }
1841
2062
  }
1842
2063
  );
1843
2064
  }
1844
2065
 
1845
2066
  // src/tools/packs/console.ts
1846
- var import_zod10 = require("zod");
2067
+ var import_zod11 = require("zod");
1847
2068
  var CONSOLE_TOOL_ID = "console";
1848
- var text9 = (s) => ({ content: [{ type: "text", text: s }] });
1849
- var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1850
- var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
2069
+ var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2070
+ var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2071
+ var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
1851
2072
  var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
1852
2073
  var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
1853
2074
  function hhmmss(ts) {
@@ -1861,10 +2082,10 @@ function registerConsoleTools(server, sync) {
1861
2082
  {
1862
2083
  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').",
1863
2084
  inputSchema: {
1864
- level: import_zod10.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
1865
- limit: import_zod10.z.number().optional().describe("Most-recent N entries. Default 50."),
1866
- pattern: import_zod10.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
1867
- deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2085
+ level: import_zod11.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
2086
+ limit: import_zod11.z.number().optional().describe("Most-recent N entries. Default 50."),
2087
+ pattern: import_zod11.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
2088
+ deviceId: import_zod11.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1868
2089
  }
1869
2090
  },
1870
2091
  async (args) => {
@@ -1882,12 +2103,12 @@ function registerConsoleTools(server, sync) {
1882
2103
  const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
1883
2104
  const shown = sorted.slice(-limit);
1884
2105
  if (shown.length === 0) {
1885
- return text9(`No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
2106
+ return text10(`No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
1886
2107
  }
1887
2108
  const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
1888
2109
  const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
1889
2110
  const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
1890
- return text9(`${header}
2111
+ return text10(`${header}
1891
2112
 
1892
2113
  \`\`\`
1893
2114
  ${lines.join("\n")}
@@ -1895,14 +2116,14 @@ ${lines.join("\n")}
1895
2116
 
1896
2117
  _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
1897
2118
  } catch (e) {
1898
- return fail10(errMsg10(e));
2119
+ return fail11(errMsg11(e));
1899
2120
  }
1900
2121
  }
1901
2122
  );
1902
2123
  }
1903
2124
 
1904
2125
  // src/tools/packs/screenshot.ts
1905
- var import_zod11 = require("zod");
2126
+ var import_zod12 = require("zod");
1906
2127
 
1907
2128
  // src/capture/simctl.ts
1908
2129
  var import_node_child_process = require("child_process");
@@ -1982,8 +2203,8 @@ async function cropToBuffer(srcPath, full, region) {
1982
2203
  var TOOL_ID = "highlight-updates";
1983
2204
  var MARGIN_PT = 12;
1984
2205
  var CAPTURE_PADDING_PT = 6;
1985
- var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1986
- var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
2206
+ var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2207
+ var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
1987
2208
  function reasonMessage(res, query) {
1988
2209
  switch (res.reason) {
1989
2210
  case "no-renders":
@@ -2013,10 +2234,10 @@ function registerScreenshotTools(server, sync) {
2013
2234
  {
2014
2235
  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.",
2015
2236
  inputSchema: {
2016
- query: import_zod11.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2017
- nativeTag: import_zod11.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2018
- scrollIntoView: import_zod11.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2019
- deviceId: import_zod11.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2237
+ query: import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2238
+ nativeTag: import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2239
+ scrollIntoView: import_zod12.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2240
+ deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2020
2241
  }
2021
2242
  },
2022
2243
  async (args) => {
@@ -2036,11 +2257,11 @@ function registerScreenshotTools(server, sync) {
2036
2257
  target
2037
2258
  );
2038
2259
  if (!res.matched || !res.rect || !res.screen) {
2039
- return fail11(reasonMessage(res, args.query) + candidatesText(res.candidates));
2260
+ return fail12(reasonMessage(res, args.query) + candidatesText(res.candidates));
2040
2261
  }
2041
2262
  const shot = await captureIosFramebuffer();
2042
2263
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
2043
- return fail11(shot.error ?? "Screenshot capture failed.");
2264
+ return fail12(shot.error ?? "Screenshot capture failed.");
2044
2265
  }
2045
2266
  const pxScale = shot.width / res.screen.width;
2046
2267
  const r = res.rect;
@@ -2056,7 +2277,7 @@ function registerScreenshotTools(server, sync) {
2056
2277
  }
2057
2278
  );
2058
2279
  if (!crop.ok || !crop.buffer) {
2059
- return fail11(crop.error ?? "Crop failed.");
2280
+ return fail12(crop.error ?? "Crop failed.");
2060
2281
  }
2061
2282
  const label = res.componentName || "component";
2062
2283
  const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
@@ -2069,21 +2290,21 @@ function registerScreenshotTools(server, sync) {
2069
2290
  ]
2070
2291
  };
2071
2292
  } catch (e) {
2072
- return fail11(errMsg11(e));
2293
+ return fail12(errMsg12(e));
2073
2294
  }
2074
2295
  }
2075
2296
  );
2076
2297
  }
2077
2298
 
2078
2299
  // src/tools/packs/interact.ts
2079
- var import_zod12 = require("zod");
2300
+ var import_zod13 = require("zod");
2080
2301
  var TOOL_ID2 = "highlight-updates";
2081
- var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2082
- var fail12 = (message) => ({
2302
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2303
+ var fail13 = (message) => ({
2083
2304
  isError: true,
2084
2305
  content: [{ type: "text", text: message }]
2085
2306
  });
2086
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2307
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2087
2308
  function formatElement(e) {
2088
2309
  let line = e.interactive ? "\u2022 " : " ";
2089
2310
  line += e.name;
@@ -2091,6 +2312,7 @@ function formatElement(e) {
2091
2312
  if (display) line += ` "${display}"`;
2092
2313
  if (e.role) line += ` <${e.role}>`;
2093
2314
  if (e.control) line += ` {${e.control}${e.value !== void 0 ? `=${JSON.stringify(e.value)}` : ""}}`;
2315
+ if (e.longPressable) line += " {long-press}";
2094
2316
  if (e.testID) line += ` [testID=${e.testID}]`;
2095
2317
  if (e.tap) line += ` (tap: ${e.tap.x},${e.tap.y})`;
2096
2318
  if (e.nativeTag != null) line += ` #${e.nativeTag}`;
@@ -2102,8 +2324,8 @@ function registerInteractTools(server, sync) {
2102
2324
  {
2103
2325
  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.",
2104
2326
  inputSchema: {
2105
- interactiveOnly: import_zod12.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2106
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2327
+ interactiveOnly: import_zod13.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2328
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2107
2329
  }
2108
2330
  },
2109
2331
  async (args) => {
@@ -2113,37 +2335,38 @@ function registerInteractTools(server, sync) {
2113
2335
  const all = res?.elements ?? [];
2114
2336
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
2115
2337
  if (elements.length === 0) {
2116
- return text10(
2338
+ return text11(
2117
2339
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
2118
2340
  );
2119
2341
  }
2120
2342
  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 }).`;
2121
- return text10(`${header}
2343
+ return text11(`${header}
2122
2344
 
2123
2345
  ${elements.map(formatElement).join("\n")}`);
2124
2346
  } catch (e) {
2125
- return fail12(errMsg12(e));
2347
+ return fail13(errMsg13(e));
2126
2348
  }
2127
2349
  }
2128
2350
  );
2129
2351
  server.registerTool(
2130
2352
  "tap_element",
2131
2353
  {
2132
- 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.",
2354
+ 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.",
2133
2355
  inputSchema: {
2134
- nativeTag: import_zod12.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2135
- testID: import_zod12.z.string().optional().describe("Exact testID of the element."),
2136
- query: import_zod12.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2137
- value: import_zod12.z.union([import_zod12.z.number(), import_zod12.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)."),
2138
- text: import_zod12.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2139
- scrollIntoView: import_zod12.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2140
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2356
+ nativeTag: import_zod13.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2357
+ testID: import_zod13.z.string().optional().describe("Exact testID of the element."),
2358
+ query: import_zod13.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2359
+ 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)."),
2360
+ text: import_zod13.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2361
+ 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."),
2362
+ scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2363
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2141
2364
  }
2142
2365
  },
2143
2366
  async (args) => {
2144
2367
  try {
2145
2368
  if (args.nativeTag == null && !args.testID && !args.query) {
2146
- return fail12("Provide one of: nativeTag, testID, or query (from describe_screen).");
2369
+ return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
2147
2370
  }
2148
2371
  const target = sync.requireProDevice(args.deviceId);
2149
2372
  const res = await sync.callAction(
@@ -2155,6 +2378,7 @@ ${elements.map(formatElement).join("\n")}`);
2155
2378
  query: args.query,
2156
2379
  value: args.value,
2157
2380
  text: args.text,
2381
+ longPress: args.longPress,
2158
2382
  scrollIntoView: args.scrollIntoView
2159
2383
  },
2160
2384
  target
@@ -2164,27 +2388,27 @@ ${elements.map(formatElement).join("\n")}`);
2164
2388
  const label = m.text ?? m.label ?? m.name;
2165
2389
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
2166
2390
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
2167
- const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : `Tapped "${label}"${id}${m.via === "onLongPress" ? " (via onLongPress)" : ""}`;
2168
- return text10(`${verb}${scrolledNote}.`);
2391
+ 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}`;
2392
+ return text11(`${verb}${scrolledNote}.`);
2169
2393
  }
2170
2394
  let msg = res?.reason ?? "Tap failed.";
2171
2395
  if (res?.candidates?.length) {
2172
2396
  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");
2173
2397
  }
2174
- return fail12(msg);
2398
+ return fail13(msg);
2175
2399
  } catch (e) {
2176
- return fail12(errMsg12(e));
2400
+ return fail13(errMsg13(e));
2177
2401
  }
2178
2402
  }
2179
2403
  );
2180
2404
  }
2181
2405
 
2182
2406
  // src/tools/packs/jsThread.ts
2183
- var import_zod13 = require("zod");
2407
+ var import_zod14 = require("zod");
2184
2408
  var JS_TOP_TOOL_ID = "js-top";
2185
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2186
- var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2187
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2409
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2410
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2411
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2188
2412
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2189
2413
  function sparkline(values) {
2190
2414
  const max = Math.max(1, ...values);
@@ -2255,9 +2479,9 @@ function registerJsThreadTools(server, sync) {
2255
2479
  {
2256
2480
  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.",
2257
2481
  inputSchema: {
2258
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2259
- durationMs: import_zod13.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2260
- clearFirst: import_zod13.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2482
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2483
+ durationMs: import_zod14.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2484
+ clearFirst: import_zod14.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2261
2485
  }
2262
2486
  },
2263
2487
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -2270,7 +2494,7 @@ function registerJsThreadTools(server, sync) {
2270
2494
  { durationMs: duration, clearFirst: clearFirst === true },
2271
2495
  target
2272
2496
  );
2273
- if (!snap) return fail13("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2497
+ if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2274
2498
  const md = formatTopMarkdown(snap, duration);
2275
2499
  const slim = {
2276
2500
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -2289,13 +2513,13 @@ function registerJsThreadTools(server, sync) {
2289
2513
  worstFreezeMs: r.worstFreezeMs
2290
2514
  }))
2291
2515
  };
2292
- return text11(`${md}
2516
+ return text12(`${md}
2293
2517
 
2294
2518
  \`\`\`json
2295
2519
  ${JSON.stringify(slim)}
2296
2520
  \`\`\``);
2297
2521
  } catch (e) {
2298
- return fail13(errMsg13(e));
2522
+ return fail14(errMsg14(e));
2299
2523
  }
2300
2524
  }
2301
2525
  );
@@ -2304,8 +2528,8 @@ ${JSON.stringify(slim)}
2304
2528
  {
2305
2529
  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).',
2306
2530
  inputSchema: {
2307
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2308
- key: import_zod13.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2531
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2532
+ key: import_zod14.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2309
2533
  }
2310
2534
  },
2311
2535
  async ({ deviceId, key }) => {
@@ -2318,18 +2542,18 @@ ${JSON.stringify(slim)}
2318
2542
  target
2319
2543
  );
2320
2544
  if (!detail) {
2321
- return fail13(
2545
+ return fail14(
2322
2546
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2323
2547
  );
2324
2548
  }
2325
2549
  const md = formatDetailMarkdown(detail);
2326
- return text11(`${md}
2550
+ return text12(`${md}
2327
2551
 
2328
2552
  \`\`\`json
2329
2553
  ${JSON.stringify(detail)}
2330
2554
  \`\`\``);
2331
2555
  } catch (e) {
2332
- return fail13(errMsg13(e));
2556
+ return fail14(errMsg14(e));
2333
2557
  }
2334
2558
  }
2335
2559
  );
@@ -2338,27 +2562,27 @@ ${JSON.stringify(detail)}
2338
2562
  {
2339
2563
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2340
2564
  inputSchema: {
2341
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2565
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2342
2566
  }
2343
2567
  },
2344
2568
  async ({ deviceId }) => {
2345
2569
  try {
2346
2570
  const target = sync.requireProDevice(deviceId);
2347
2571
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2348
- return text11("Cleared js-top stats.");
2572
+ return text12("Cleared js-top stats.");
2349
2573
  } catch (e) {
2350
- return fail13(errMsg13(e));
2574
+ return fail14(errMsg14(e));
2351
2575
  }
2352
2576
  }
2353
2577
  );
2354
2578
  }
2355
2579
 
2356
2580
  // src/tools/packs/images.ts
2357
- var import_zod14 = require("zod");
2581
+ var import_zod15 = require("zod");
2358
2582
  var IMAGES_TOOL_ID = "images";
2359
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2360
- var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2361
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2583
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2584
+ var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2585
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
2362
2586
  function uriTail(uri) {
2363
2587
  if (!uri) return "(no source)";
2364
2588
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -2446,9 +2670,9 @@ function registerImagesTools(server, sync) {
2446
2670
  {
2447
2671
  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.",
2448
2672
  inputSchema: {
2449
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2450
- limit: import_zod14.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2451
- status: import_zod14.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2673
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2674
+ limit: import_zod15.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2675
+ status: import_zod15.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2452
2676
  }
2453
2677
  },
2454
2678
  async ({ deviceId, limit, status }) => {
@@ -2461,7 +2685,7 @@ function registerImagesTools(server, sync) {
2461
2685
  { limit: capped, status },
2462
2686
  target
2463
2687
  );
2464
- if (!list) return fail14("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2688
+ if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2465
2689
  const md = formatListMarkdown(list, capped);
2466
2690
  const slim = {
2467
2691
  stats: list.stats,
@@ -2486,13 +2710,13 @@ function registerImagesTools(server, sync) {
2486
2710
  loadCount: r.loadCount
2487
2711
  }))
2488
2712
  };
2489
- return text12(`${md}
2713
+ return text13(`${md}
2490
2714
 
2491
2715
  \`\`\`json
2492
2716
  ${JSON.stringify(slim)}
2493
2717
  \`\`\``);
2494
2718
  } catch (e) {
2495
- return fail14(errMsg14(e));
2719
+ return fail15(errMsg15(e));
2496
2720
  }
2497
2721
  }
2498
2722
  );
@@ -2501,9 +2725,9 @@ ${JSON.stringify(slim)}
2501
2725
  {
2502
2726
  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.",
2503
2727
  inputSchema: {
2504
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2505
- id: import_zod14.z.number().describe("Record id from get_images."),
2506
- action: import_zod14.z.enum([
2728
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2729
+ id: import_zod15.z.number().describe("Record id from get_images."),
2730
+ action: import_zod15.z.enum([
2507
2731
  "hard_reload",
2508
2732
  "retry",
2509
2733
  "evict_disk",
@@ -2515,7 +2739,7 @@ ${JSON.stringify(slim)}
2515
2739
  "swap_url",
2516
2740
  "restore"
2517
2741
  ]).describe("What to do."),
2518
- url: import_zod14.z.string().optional().describe("Replacement URL (swap_url only).")
2742
+ url: import_zod15.z.string().optional().describe("Replacement URL (swap_url only).")
2519
2743
  }
2520
2744
  },
2521
2745
  async ({ deviceId, id, action, url }) => {
@@ -2523,8 +2747,8 @@ ${JSON.stringify(slim)}
2523
2747
  const target = sync.requireProDevice(deviceId);
2524
2748
  if (action === "prove_savings") {
2525
2749
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2526
- if (!savings) return fail14("Device returned nothing for proveSavings.");
2527
- if (!savings.ok) return fail14(savings.message);
2750
+ if (!savings) return fail15("Device returned nothing for proveSavings.");
2751
+ if (!savings.ok) return fail15(savings.message);
2528
2752
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2529
2753
  if (savings.originalBytes != null)
2530
2754
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -2536,11 +2760,11 @@ ${JSON.stringify(slim)}
2536
2760
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
2537
2761
  if (savings.optimizedUri)
2538
2762
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
2539
- return text12(parts.join("\n"));
2763
+ return text13(parts.join("\n"));
2540
2764
  }
2541
2765
  const entry = ACTION_MAP[action];
2542
- if (!entry) return fail14(`Unknown action: ${action}`);
2543
- if (action === "swap_url" && !url) return fail14("swap_url requires the `url` param.");
2766
+ if (!entry) return fail15(`Unknown action: ${action}`);
2767
+ if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
2544
2768
  const result = await sync.callAction(
2545
2769
  IMAGES_TOOL_ID,
2546
2770
  entry.action,
@@ -2560,9 +2784,9 @@ ${JSON.stringify(slim)}
2560
2784
  );
2561
2785
  if (detail.error) lines.push(`Error: ${detail.error}`);
2562
2786
  }
2563
- return text12(lines.join("\n"));
2787
+ return text13(lines.join("\n"));
2564
2788
  } catch (e) {
2565
- return fail14(errMsg14(e));
2789
+ return fail15(errMsg15(e));
2566
2790
  }
2567
2791
  }
2568
2792
  );
@@ -2571,12 +2795,12 @@ ${JSON.stringify(slim)}
2571
2795
  {
2572
2796
  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.",
2573
2797
  inputSchema: {
2574
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2575
- network: import_zod14.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2576
- blankImages: import_zod14.z.boolean().optional().describe("Render all images without a source."),
2577
- massAction: import_zod14.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."),
2578
- clearRegistry: import_zod14.z.boolean().optional().describe("Clear the captured image log."),
2579
- clearExpoCaches: import_zod14.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
2798
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2799
+ network: import_zod15.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2800
+ blankImages: import_zod15.z.boolean().optional().describe("Render all images without a source."),
2801
+ 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."),
2802
+ clearRegistry: import_zod15.z.boolean().optional().describe("Clear the captured image log."),
2803
+ clearExpoCaches: import_zod15.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
2580
2804
  }
2581
2805
  },
2582
2806
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -2603,10 +2827,94 @@ ${JSON.stringify(slim)}
2603
2827
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
2604
2828
  done.push(r?.message ?? "expo caches cleared");
2605
2829
  }
2606
- if (done.length === 0) return fail14("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2607
- return text12(`Applied: ${done.join(" \xB7 ")}`);
2830
+ if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2831
+ return text13(`Applied: ${done.join(" \xB7 ")}`);
2608
2832
  } catch (e) {
2609
- return fail14(errMsg14(e));
2833
+ return fail15(errMsg15(e));
2834
+ }
2835
+ }
2836
+ );
2837
+ }
2838
+
2839
+ // src/tools/packs/reload.ts
2840
+ var import_zod16 = require("zod");
2841
+ var APP_TOOL_ID = "app";
2842
+ var RELOAD_ACTION = "reloadApp";
2843
+ var DEFAULT_WAIT_MS = 3e4;
2844
+ var TEARDOWN_GRACE_MS = 1200;
2845
+ var PROBE_TIMEOUT_MS = 1500;
2846
+ var PROBE_INTERVAL_MS = 400;
2847
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
2848
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2849
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
2850
+ var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
2851
+ async function ping(sync, deviceId, timeoutMs) {
2852
+ try {
2853
+ const pong = await sync.callAction(APP_TOOL_ID, "ping", {}, deviceId, { timeoutMs });
2854
+ return typeof pong?.startedAt === "number" ? pong.startedAt : null;
2855
+ } catch {
2856
+ return null;
2857
+ }
2858
+ }
2859
+ function assertReloadSupported(sync, deviceId) {
2860
+ if (deviceId === ALL_DEVICES) return;
2861
+ const entry = sync.toolActions(APP_TOOL_ID).find((t) => t.deviceId === deviceId);
2862
+ if (!entry?.actions.includes(RELOAD_ACTION)) {
2863
+ throw new Error(
2864
+ `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.`
2865
+ );
2866
+ }
2867
+ }
2868
+ async function waitForAppBack(sync, deviceId, timeoutMs, previousRealm) {
2869
+ const waitStart = Date.now();
2870
+ await delay3(TEARDOWN_GRACE_MS);
2871
+ while (Date.now() - waitStart < timeoutMs) {
2872
+ const realm = await ping(sync, deviceId, PROBE_TIMEOUT_MS);
2873
+ if (realm !== null && realm !== previousRealm) return Date.now() - waitStart;
2874
+ await delay3(PROBE_INTERVAL_MS);
2875
+ }
2876
+ return null;
2877
+ }
2878
+ function registerReloadTools(server, sync) {
2879
+ server.registerTool(
2880
+ "reload_app",
2881
+ {
2882
+ 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.",
2883
+ inputSchema: {
2884
+ strategy: import_zod16.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
2885
+ "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
2886
+ ),
2887
+ 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."),
2888
+ timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
2889
+ deviceId: import_zod16.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
2890
+ }
2891
+ },
2892
+ async (args) => {
2893
+ try {
2894
+ const target = sync.requireProDevice(args.deviceId);
2895
+ assertReloadSupported(sync, target);
2896
+ const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
2897
+ const ack = await sync.callAction(
2898
+ APP_TOOL_ID,
2899
+ RELOAD_ACTION,
2900
+ { strategy: args.strategy ?? "auto" },
2901
+ target
2902
+ );
2903
+ const via = ack?.strategy ? ` via ${ack.strategy}` : "";
2904
+ if (args.wait === false) {
2905
+ return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
2906
+ }
2907
+ const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
2908
+ if (elapsed == null) {
2909
+ return fail16(
2910
+ `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.`
2911
+ );
2912
+ }
2913
+ return text14(
2914
+ `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.`
2915
+ );
2916
+ } catch (e) {
2917
+ return fail16(errMsg16(e));
2610
2918
  }
2611
2919
  }
2612
2920
  );
@@ -2625,6 +2933,13 @@ filter (e.g. sources:["network"] to list HTTP requests). It's the universal, tok
2625
2933
  activity reader across ALL tools and is compact by default \u2014 reach for it before reading source
2626
2934
  code, and prefer it over \`get_snapshot\` for any "what happened / show me the requests" question.
2627
2935
 
2936
+ Working on ONE HTTP request rather than skimming activity? \`get_events\` deliberately emits no
2937
+ request ids, so use \`get_network_requests\` \u2014 same compact style, but it leads with the id every
2938
+ network action is keyed by, and marks which requests are pinned/saved. \`flagged:'any'\` lists the
2939
+ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-request cap and app
2940
+ restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
2941
+ pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
2942
+
2628
2943
  Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
2629
2944
  a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
2630
2945
  iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
@@ -2643,6 +2958,11 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
2643
2958
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
2644
2959
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
2645
2960
 
2961
+ Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
2962
+ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
2963
+ builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
2964
+ re-read anything you cached.
2965
+
2646
2966
  For everything else:
2647
2967
  - \`list_tool_actions\` to see what a tool can do,
2648
2968
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -2662,6 +2982,7 @@ function createMcpServer(sync) {
2662
2982
  registerGenericTools(server, sync);
2663
2983
  registerPerfMonitorTools(server, sync);
2664
2984
  registerEventsTools(server, sync);
2985
+ registerNetworkTools(server, sync);
2665
2986
  registerReactQueryTools(server, sync);
2666
2987
  registerZustandTools(server, sync);
2667
2988
  registerJotaiTools(server, sync);
@@ -2673,22 +2994,27 @@ function createMcpServer(sync) {
2673
2994
  registerInteractTools(server, sync);
2674
2995
  registerJsThreadTools(server, sync);
2675
2996
  registerImagesTools(server, sync);
2997
+ registerReloadTools(server, sync);
2676
2998
  return server;
2677
2999
  }
2678
3000
  var deviceIdField = {
2679
- deviceId: import_zod15.z.string().optional().describe(
3001
+ deviceId: import_zod17.z.string().optional().describe(
2680
3002
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
2681
3003
  )
2682
3004
  };
2683
3005
 
2684
3006
  // src/init.ts
3007
+ var import_node_child_process2 = require("child_process");
2685
3008
  var import_node_fs3 = __toESM(require("fs"));
3009
+ var import_node_os3 = __toESM(require("os"));
2686
3010
  var import_node_path3 = __toESM(require("path"));
3011
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
3012
+ var PROBE_TIMEOUT_MS2 = 3e3;
2687
3013
  function runInit(opts = {}) {
2688
3014
  const cwd = opts.cwd ?? process.cwd();
2689
3015
  const skillSrcDir = opts.skillSrcDir ?? import_node_path3.default.resolve(__dirname, "../skill");
2690
3016
  const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
2691
- const entry = opts.cliPath ? { type: "stdio", command: "node", args: [opts.cliPath] } : { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
3017
+ const entry = resolveEntry(opts, log);
2692
3018
  const done = [];
2693
3019
  upsertMcpConfig(import_node_path3.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
2694
3020
  if (import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, ".cursor"))) {
@@ -2705,12 +3031,125 @@ function runInit(opts = {}) {
2705
3031
  log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
2706
3032
  log(" 2. Open your app with Buoy devtools running on a device or simulator.");
2707
3033
  log(' 3. Say "buoy optimize" to start a guided performance pass.');
2708
- if (!opts.cliPath) {
3034
+ if (entry.command === "npx") {
2709
3035
  log("");
2710
3036
  log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
2711
3037
  log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
3038
+ } else if (!opts.cliPath) {
3039
+ log("");
3040
+ log(`This uses a pinned local install (no network on startup). To update it later,`);
3041
+ log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
2712
3042
  }
2713
3043
  }
3044
+ function resolveEntry(opts, log) {
3045
+ if (opts.cliPath) {
3046
+ return { type: "stdio", command: "node", args: [opts.cliPath] };
3047
+ }
3048
+ const mode = opts.mode ?? "auto";
3049
+ if (mode === "npx") {
3050
+ return npxEntry();
3051
+ }
3052
+ if (mode === "local") {
3053
+ return localEntry(opts, log);
3054
+ }
3055
+ const probe = probeRegistry();
3056
+ if (probe.reachable) {
3057
+ return npxEntry();
3058
+ }
3059
+ log(
3060
+ `\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.`
3061
+ );
3062
+ return localEntry(opts, log);
3063
+ }
3064
+ function npxEntry() {
3065
+ return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
3066
+ }
3067
+ function localEntry(opts, log) {
3068
+ const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path3.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
3069
+ const registry = opts.registry ?? PUBLIC_REGISTRY;
3070
+ const cliPath = installLocalCopy(installDir, registry, log);
3071
+ if (cliPath) {
3072
+ return { type: "stdio", command: "node", args: [cliPath] };
3073
+ }
3074
+ log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
3075
+ return npxEntry();
3076
+ }
3077
+ function installLocalCopy(installDir, registry, log) {
3078
+ const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
3079
+ import_node_fs3.default.mkdirSync(installDir, { recursive: true });
3080
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3081
+ log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
3082
+ const res = (0, import_node_child_process2.spawnSync)(
3083
+ npm,
3084
+ [
3085
+ "install",
3086
+ spec,
3087
+ "--prefix",
3088
+ installDir,
3089
+ "--registry",
3090
+ registry,
3091
+ `--@buoy-gg:registry=${registry}`,
3092
+ "--no-audit",
3093
+ "--no-fund",
3094
+ "--loglevel=error"
3095
+ ],
3096
+ { stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
3097
+ );
3098
+ if (res.status !== 0) {
3099
+ log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
3100
+ return null;
3101
+ }
3102
+ const cliPath = import_node_path3.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
3103
+ if (!import_node_fs3.default.existsSync(cliPath)) {
3104
+ log(`\u26A0 install finished but ${cliPath} is missing.`);
3105
+ return null;
3106
+ }
3107
+ return cliPath;
3108
+ }
3109
+ function probeRegistry() {
3110
+ const registry = effectiveRegistry();
3111
+ if (!registry) return { reachable: false, registry: null };
3112
+ if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
3113
+ return { reachable: true, registry };
3114
+ }
3115
+ return { reachable: hostReachable(registry), registry };
3116
+ }
3117
+ function effectiveRegistry() {
3118
+ const scoped = npmConfigGet("@buoy-gg:registry");
3119
+ if (scoped) return scoped;
3120
+ const global = npmConfigGet("registry");
3121
+ return global ?? PUBLIC_REGISTRY;
3122
+ }
3123
+ function npmConfigGet(key) {
3124
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3125
+ const res = (0, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 });
3126
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
3127
+ const val = res.stdout.trim();
3128
+ if (!val || val === "undefined" || val === "null") return null;
3129
+ return val;
3130
+ }
3131
+ function hostReachable(registry) {
3132
+ const done = (0, import_node_child_process2.spawnSync)(
3133
+ process.execPath,
3134
+ ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS2)],
3135
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS2 + 2e3 }
3136
+ );
3137
+ return done.status === 0;
3138
+ }
3139
+ var REACHABILITY_PROBE = `
3140
+ const url = process.argv[1];
3141
+ const ms = Number(process.argv[2]) || 3000;
3142
+ const c = new AbortController();
3143
+ const t = setTimeout(() => c.abort(), ms);
3144
+ fetch(url, { method: 'HEAD', signal: c.signal })
3145
+ .then(() => { clearTimeout(t); process.exit(0); })
3146
+ .catch(() => {
3147
+ // A HEAD may be rejected while the host is up; a bare TCP-level failure is
3148
+ // what we care about. fetch throwing a TypeError = network unreachable.
3149
+ clearTimeout(t);
3150
+ process.exit(1);
3151
+ });
3152
+ `;
2714
3153
  function upsertMcpConfig(file, key, entry, cwd, done) {
2715
3154
  let json3 = {};
2716
3155
  if (import_node_fs3.default.existsSync(file)) {