@buoy-gg/mcp 7.0.32 → 7.0.34
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/{chunk-HWYVQNMR.mjs → chunk-LS2L5OOL.mjs} +824 -629
- package/dist/cli.js +816 -621
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +816 -621
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -252,6 +252,7 @@ var SyncClient = class {
|
|
|
252
252
|
isConnected: d.isConnected !== false,
|
|
253
253
|
isPro: d.isPro === true,
|
|
254
254
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
255
|
+
collision: d.collision === true,
|
|
255
256
|
tools: caps?.tools ?? []
|
|
256
257
|
};
|
|
257
258
|
});
|
|
@@ -640,7 +641,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
640
641
|
|
|
641
642
|
// src/server.ts
|
|
642
643
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
643
|
-
var
|
|
644
|
+
var import_zod26 = require("zod");
|
|
644
645
|
|
|
645
646
|
// src/tools/generic.ts
|
|
646
647
|
var import_zod = require("zod");
|
|
@@ -651,7 +652,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
651
652
|
var import_node_path = __toESM(require("path"));
|
|
652
653
|
|
|
653
654
|
// src/version.ts
|
|
654
|
-
var VERSION = "7.0.
|
|
655
|
+
var VERSION = "7.0.34" ? "7.0.34" : "0.0.0-dev";
|
|
655
656
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
656
657
|
|
|
657
658
|
// src/update/checkForUpdate.ts
|
|
@@ -745,8 +746,12 @@ function registerGenericTools(server, sync) {
|
|
|
745
746
|
const connected = devices.filter((d) => d.isConnected);
|
|
746
747
|
const anyPro = connected.some((d) => d.isPro);
|
|
747
748
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
749
|
+
const collided = connected.filter((d) => d.collision);
|
|
748
750
|
return json({
|
|
749
751
|
devices,
|
|
752
|
+
...collided.length > 0 ? {
|
|
753
|
+
warning: `${collided.map((d) => `"${d.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`
|
|
754
|
+
} : {},
|
|
750
755
|
...anyPro ? {} : {
|
|
751
756
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
752
757
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -822,7 +827,40 @@ function errMsg(e) {
|
|
|
822
827
|
}
|
|
823
828
|
|
|
824
829
|
// src/tools/packs/perfMonitor.ts
|
|
830
|
+
var import_zod3 = require("zod");
|
|
831
|
+
|
|
832
|
+
// src/tools/packKit.ts
|
|
825
833
|
var import_zod2 = require("zod");
|
|
834
|
+
var text = (s) => ({
|
|
835
|
+
content: [{ type: "text", text: s }]
|
|
836
|
+
});
|
|
837
|
+
var fail2 = (message) => ({
|
|
838
|
+
isError: true,
|
|
839
|
+
content: [{ type: "text", text: message }]
|
|
840
|
+
});
|
|
841
|
+
var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
|
|
842
|
+
var deviceIdInput = import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");
|
|
843
|
+
function clampLimit(value, fallback) {
|
|
844
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
845
|
+
}
|
|
846
|
+
function staleHeader(stale) {
|
|
847
|
+
return stale ? `${stale}
|
|
848
|
+
|
|
849
|
+
` : "";
|
|
850
|
+
}
|
|
851
|
+
function inactiveToolMessage(options) {
|
|
852
|
+
return `No ${options.toolId} result from the device. The ${options.toolId} tool may be inactive, or the app needs a newer Buoy (${options.mcpTool} requires the ${options.action} action).`;
|
|
853
|
+
}
|
|
854
|
+
function withDevice(sync, run2) {
|
|
855
|
+
return async (args) => {
|
|
856
|
+
try {
|
|
857
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
858
|
+
return await run2(args, target);
|
|
859
|
+
} catch (e) {
|
|
860
|
+
return fail2(errMsg2(e));
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
826
864
|
|
|
827
865
|
// src/tools/packs/perfMonitorTypes.ts
|
|
828
866
|
var PERF_TOOL_ID = "perf-monitor";
|
|
@@ -1139,13 +1177,10 @@ function round(n, d) {
|
|
|
1139
1177
|
var POLL_INTERVAL_MS = 1500;
|
|
1140
1178
|
var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1141
1179
|
var json2 = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
1142
|
-
var
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
name: import_zod2.z.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1147
|
-
params: import_zod2.z.record(import_zod2.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
|
|
1148
|
-
route: import_zod2.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1180
|
+
var caseSchema = import_zod3.z.object({
|
|
1181
|
+
name: import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1182
|
+
params: import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
|
|
1183
|
+
route: import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1149
1184
|
});
|
|
1150
1185
|
async function readDeviceSettings(sync, deviceId) {
|
|
1151
1186
|
try {
|
|
@@ -1264,22 +1299,22 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1264
1299
|
{
|
|
1265
1300
|
description: "Run a perf-monitor benchmark batch and return a RANKED comparison of all cases (winner by UI FPS). Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",
|
|
1266
1301
|
inputSchema: {
|
|
1267
|
-
targetRoute:
|
|
1268
|
-
cases:
|
|
1269
|
-
bounceRoute:
|
|
1270
|
-
runsPerCase:
|
|
1271
|
-
perCaseDurationMs:
|
|
1272
|
-
settleMs:
|
|
1273
|
-
coolDownMs:
|
|
1274
|
-
navTimeoutMs:
|
|
1275
|
-
discardWarmupRuns:
|
|
1276
|
-
shuffleCases:
|
|
1277
|
-
reloadBetweenCases:
|
|
1278
|
-
captureRenders:
|
|
1302
|
+
targetRoute: import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1303
|
+
cases: import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
1304
|
+
bounceRoute: import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
|
|
1305
|
+
runsPerCase: import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
|
|
1306
|
+
perCaseDurationMs: import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),
|
|
1307
|
+
settleMs: import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),
|
|
1308
|
+
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1309
|
+
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1310
|
+
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1311
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
|
|
1312
|
+
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1313
|
+
captureRenders: import_zod3.z.boolean().optional().describe(
|
|
1279
1314
|
"Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds slight JS-thread overhead applied equally to every case. Default true."
|
|
1280
1315
|
),
|
|
1281
|
-
deviceId:
|
|
1282
|
-
maxWaitMs:
|
|
1316
|
+
deviceId: import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1317
|
+
maxWaitMs: import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
1283
1318
|
}
|
|
1284
1319
|
},
|
|
1285
1320
|
async (args) => {
|
|
@@ -1349,7 +1384,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1349
1384
|
"get_automation_status",
|
|
1350
1385
|
{
|
|
1351
1386
|
description: "Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",
|
|
1352
|
-
inputSchema: { deviceId:
|
|
1387
|
+
inputSchema: { deviceId: import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).") }
|
|
1353
1388
|
},
|
|
1354
1389
|
async ({ deviceId }) => {
|
|
1355
1390
|
try {
|
|
@@ -1370,8 +1405,8 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1370
1405
|
{
|
|
1371
1406
|
description: "Block until the in-flight perf-monitor batch finishes, then return its ranked comparison. Use this if a batch was started another way (e.g. on the device).",
|
|
1372
1407
|
inputSchema: {
|
|
1373
|
-
deviceId:
|
|
1374
|
-
maxWaitMs:
|
|
1408
|
+
deviceId: deviceIdInput,
|
|
1409
|
+
maxWaitMs: import_zod3.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")
|
|
1375
1410
|
}
|
|
1376
1411
|
},
|
|
1377
1412
|
async ({ deviceId, maxWaitMs }) => {
|
|
@@ -1444,8 +1479,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1444
1479
|
{
|
|
1445
1480
|
description: "Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",
|
|
1446
1481
|
inputSchema: {
|
|
1447
|
-
batchId:
|
|
1448
|
-
deviceId:
|
|
1482
|
+
batchId: import_zod3.z.string().describe("The batch id to report on."),
|
|
1483
|
+
deviceId: deviceIdInput
|
|
1449
1484
|
}
|
|
1450
1485
|
},
|
|
1451
1486
|
async ({ batchId, deviceId }) => {
|
|
@@ -1472,9 +1507,9 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1472
1507
|
{
|
|
1473
1508
|
description: "Compare two saved benchmark runs head-to-head: avg + p95 for FPS/CPU/memory, jank, AND render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use this to prove a memoization or re-render fix actually landed: the wasted-render delta is the number that says so.",
|
|
1474
1509
|
inputSchema: {
|
|
1475
|
-
reportIdA:
|
|
1476
|
-
reportIdB:
|
|
1477
|
-
deviceId:
|
|
1510
|
+
reportIdA: import_zod3.z.string().describe("First (baseline) report id."),
|
|
1511
|
+
reportIdB: import_zod3.z.string().describe("Second report id to compare against the baseline."),
|
|
1512
|
+
deviceId: deviceIdInput
|
|
1478
1513
|
}
|
|
1479
1514
|
},
|
|
1480
1515
|
async ({ reportIdA, reportIdB, deviceId }) => {
|
|
@@ -1502,8 +1537,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1502
1537
|
{
|
|
1503
1538
|
description: "Read the device's saved benchmark settings (per-page duration, runs, cooldown, reload, shuffle) in plain English with a time estimate. Call this before run_benchmark_batch so you can tell the user what the run will do \u2014 run_benchmark_batch inherits these unless you override them.",
|
|
1504
1539
|
inputSchema: {
|
|
1505
|
-
pages:
|
|
1506
|
-
deviceId:
|
|
1540
|
+
pages: import_zod3.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
|
|
1541
|
+
deviceId: deviceIdInput
|
|
1507
1542
|
}
|
|
1508
1543
|
},
|
|
1509
1544
|
async ({ pages, deviceId }) => {
|
|
@@ -1533,16 +1568,16 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1533
1568
|
{
|
|
1534
1569
|
description: `PERSIST benchmark settings on the device so they stick across runs. Only use when the user explicitly wants to save a change ("always", "save this"). For a one-off, pass overrides to run_benchmark_batch instead \u2014 don't change their saved profile.`,
|
|
1535
1570
|
inputSchema: {
|
|
1536
|
-
perCaseDurationMs:
|
|
1537
|
-
settleMs:
|
|
1538
|
-
coolDownMs:
|
|
1539
|
-
navTimeoutMs:
|
|
1540
|
-
runsPerCase:
|
|
1541
|
-
discardWarmupRuns:
|
|
1542
|
-
shuffleCases:
|
|
1543
|
-
reloadBetweenCases:
|
|
1544
|
-
captureRenders:
|
|
1545
|
-
deviceId:
|
|
1571
|
+
perCaseDurationMs: import_zod3.z.number().optional().describe("Recording length per page (ms)."),
|
|
1572
|
+
settleMs: import_zod3.z.number().optional().describe("Idle after navigation before recording (ms)."),
|
|
1573
|
+
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases (ms)."),
|
|
1574
|
+
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1575
|
+
runsPerCase: import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1576
|
+
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1577
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
|
|
1578
|
+
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1579
|
+
captureRenders: import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1580
|
+
deviceId: deviceIdInput
|
|
1546
1581
|
}
|
|
1547
1582
|
},
|
|
1548
1583
|
async (args) => {
|
|
@@ -1590,11 +1625,8 @@ ${JSON.stringify(saved ?? settings, null, 2)}
|
|
|
1590
1625
|
}
|
|
1591
1626
|
|
|
1592
1627
|
// src/tools/packs/events.ts
|
|
1593
|
-
var
|
|
1628
|
+
var import_zod4 = require("zod");
|
|
1594
1629
|
var EVENTS_TOOL_ID = "events";
|
|
1595
|
-
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1596
|
-
var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1597
|
-
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1598
1630
|
var SOURCE_ALIASES = {
|
|
1599
1631
|
storage: ["storage-async", "storage-mmkv"],
|
|
1600
1632
|
"react-query": ["react-query", "react-query-query", "react-query-mutation"]
|
|
@@ -1609,13 +1641,13 @@ function registerEventsTools(server, sync) {
|
|
|
1609
1641
|
{
|
|
1610
1642
|
description: "The UNIVERSAL, token-bounded activity reader for what's happening in the app across ALL Buoy tools. Check this timeline BEFORE driving the UI to reproduce a bug \u2014 the failure is usually already recorded here. The events tool aggregates every source \u2014 network requests, redux/zustand/jotai actions, react-query query/mutation updates, storage writes, route changes, renders \u2014 so filter to one with `sources`: ['network'] lists HTTP requests, ['redux'] dispatched actions, ['react-query'] query updates, ['storage'] writes, ['route'] navigation. Returns a compact one-line summary per event (source \xB7 status \xB7 title \xB7 timing) by DEFAULT \u2014 not the heavy raw payloads \u2014 so it's cheap. Re-call with includeData:true, a bigger limit, or specific sources only when you need detail. PREFER this over get_snapshot (and over reading source code) for any 'what happened' / 'show me the requests/actions' question; get_snapshot is the raw heavy current-state escape hatch.",
|
|
1611
1643
|
inputSchema: {
|
|
1612
|
-
limit:
|
|
1613
|
-
sources:
|
|
1614
|
-
status:
|
|
1615
|
-
includeData:
|
|
1616
|
-
format:
|
|
1617
|
-
preset:
|
|
1618
|
-
deviceId:
|
|
1644
|
+
limit: import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1645
|
+
sources: import_zod4.z.array(import_zod4.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
|
|
1646
|
+
status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
|
|
1647
|
+
includeData: import_zod4.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
|
|
1648
|
+
format: import_zod4.z.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
|
|
1649
|
+
preset: import_zod4.z.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
|
|
1650
|
+
deviceId: deviceIdInput
|
|
1619
1651
|
}
|
|
1620
1652
|
},
|
|
1621
1653
|
async (args) => {
|
|
@@ -1634,7 +1666,7 @@ function registerEventsTools(server, sync) {
|
|
|
1634
1666
|
target
|
|
1635
1667
|
);
|
|
1636
1668
|
if (!res || typeof res.output !== "string") {
|
|
1637
|
-
return
|
|
1669
|
+
return fail2(
|
|
1638
1670
|
"No events result from the device. The events tool may be inactive, or the app needs a newer Buoy (get_events requires the exportEvents action)."
|
|
1639
1671
|
);
|
|
1640
1672
|
}
|
|
@@ -1642,20 +1674,17 @@ function registerEventsTools(server, sync) {
|
|
|
1642
1674
|
const note = `
|
|
1643
1675
|
|
|
1644
1676
|
_Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
|
|
1645
|
-
return
|
|
1677
|
+
return text((res.output || "(no events captured)") + note);
|
|
1646
1678
|
} catch (e) {
|
|
1647
|
-
return
|
|
1679
|
+
return fail2(errMsg2(e));
|
|
1648
1680
|
}
|
|
1649
1681
|
}
|
|
1650
1682
|
);
|
|
1651
1683
|
}
|
|
1652
1684
|
|
|
1653
1685
|
// src/tools/packs/network.ts
|
|
1654
|
-
var
|
|
1686
|
+
var import_zod5 = require("zod");
|
|
1655
1687
|
var NETWORK_TOOL_ID = "network";
|
|
1656
|
-
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1657
|
-
var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1658
|
-
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1659
1688
|
var masterNote = (state) => {
|
|
1660
1689
|
if (state?.enabled !== false) return "";
|
|
1661
1690
|
return state.autoPaused ? ' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).' : " \u2014 MASTER SWITCH IS OFF, nothing is firing";
|
|
@@ -1781,12 +1810,12 @@ function registerNetworkTools(server, sync) {
|
|
|
1781
1810
|
{
|
|
1782
1811
|
description: "List captured HTTP requests WITH their request ids, plus which ones are pinned/saved. API data wrong or calls failing \u2192 read the actual request+response bodies here instead of guessing from code; capture starts at app launch, so boot-time requests (session bootstrap, first queries) are included. Use this (not get_events) when you need to ACT on a request \u2014 every network action is keyed by the id, and get_events does not emit ids. Compact by default: one line per request (id \xB7 status \xB7 method \xB7 path \xB7 timing \xB7 size). Set flagged:'pinned'|'saved'|'any' to read the developer's KEPT requests \u2014 those are snapshots that outlive Clear, the 500-request cap and app restarts, so treat a pinned request as 'a human flagged this as the problem'. Filter with status:'errors' to go straight to failures, or `pattern` to match a URL substring. includeBodies:true adds truncated request/response payloads (heavier).",
|
|
1783
1812
|
inputSchema: {
|
|
1784
|
-
limit:
|
|
1785
|
-
pattern:
|
|
1786
|
-
status:
|
|
1787
|
-
flagged:
|
|
1788
|
-
includeBodies:
|
|
1789
|
-
deviceId:
|
|
1813
|
+
limit: import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),
|
|
1814
|
+
pattern: import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
1815
|
+
status: import_zod5.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
1816
|
+
flagged: import_zod5.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."),
|
|
1817
|
+
includeBodies: import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
1818
|
+
deviceId: deviceIdInput
|
|
1790
1819
|
}
|
|
1791
1820
|
},
|
|
1792
1821
|
async (args) => {
|
|
@@ -1810,7 +1839,7 @@ function registerNetworkTools(server, sync) {
|
|
|
1810
1839
|
);
|
|
1811
1840
|
const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1812
1841
|
if (rows2.length === 0) {
|
|
1813
|
-
return
|
|
1842
|
+
return text(
|
|
1814
1843
|
`No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
|
|
1815
1844
|
|
|
1816
1845
|
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.`
|
|
@@ -1827,7 +1856,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
1827
1856
|
if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
|
|
1828
1857
|
return out;
|
|
1829
1858
|
}).join("\n");
|
|
1830
|
-
return
|
|
1859
|
+
return text(
|
|
1831
1860
|
`**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
|
|
1832
1861
|
|
|
1833
1862
|
${body2}
|
|
@@ -1838,7 +1867,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
1838
1867
|
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1839
1868
|
if (rows.length === 0) {
|
|
1840
1869
|
const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
|
|
1841
|
-
return
|
|
1870
|
+
return text(`No requests on ${target}. ${why}`);
|
|
1842
1871
|
}
|
|
1843
1872
|
const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
1844
1873
|
const body = rows.map((e, i) => {
|
|
@@ -1848,7 +1877,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
1848
1877
|
return out;
|
|
1849
1878
|
}).join("\n");
|
|
1850
1879
|
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
1851
|
-
return
|
|
1880
|
+
return text(
|
|
1852
1881
|
`**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
|
|
1853
1882
|
|
|
1854
1883
|
${body}
|
|
@@ -1856,7 +1885,7 @@ ${body}
|
|
|
1856
1885
|
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._${skippedNote(live.skipped)}`
|
|
1857
1886
|
);
|
|
1858
1887
|
} catch (e) {
|
|
1859
|
-
return
|
|
1888
|
+
return fail2(errMsg2(e));
|
|
1860
1889
|
}
|
|
1861
1890
|
}
|
|
1862
1891
|
);
|
|
@@ -1865,10 +1894,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1865
1894
|
{
|
|
1866
1895
|
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.",
|
|
1867
1896
|
inputSchema: {
|
|
1868
|
-
action:
|
|
1869
|
-
id:
|
|
1870
|
-
key:
|
|
1871
|
-
deviceId:
|
|
1897
|
+
action: import_zod5.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)."),
|
|
1898
|
+
id: import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
1899
|
+
key: import_zod5.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
|
|
1900
|
+
deviceId: deviceIdInput
|
|
1872
1901
|
}
|
|
1873
1902
|
},
|
|
1874
1903
|
async (args) => {
|
|
@@ -1876,10 +1905,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1876
1905
|
const target = sync.requireProDevice(args.deviceId);
|
|
1877
1906
|
const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
|
|
1878
1907
|
if (needsId && !args.id) {
|
|
1879
|
-
return
|
|
1908
|
+
return fail2(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
|
|
1880
1909
|
}
|
|
1881
1910
|
if (args.action === "remove" && !args.key) {
|
|
1882
|
-
return
|
|
1911
|
+
return fail2('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
|
|
1883
1912
|
}
|
|
1884
1913
|
switch (args.action) {
|
|
1885
1914
|
case "pin":
|
|
@@ -1890,10 +1919,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1890
1919
|
{ id: args.id, pinned: args.action === "pin" },
|
|
1891
1920
|
target
|
|
1892
1921
|
);
|
|
1893
|
-
if (res === null) return
|
|
1922
|
+
if (res === null) return fail2(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
|
|
1894
1923
|
const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
|
|
1895
|
-
if (capped) return
|
|
1896
|
-
return
|
|
1924
|
+
if (capped) return fail2(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
|
|
1925
|
+
return text(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
|
|
1897
1926
|
}
|
|
1898
1927
|
case "save":
|
|
1899
1928
|
case "unsave": {
|
|
@@ -1903,26 +1932,26 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1903
1932
|
{ id: args.id, saved: args.action === "save" },
|
|
1904
1933
|
target
|
|
1905
1934
|
);
|
|
1906
|
-
if (res === null) return
|
|
1935
|
+
if (res === null) return fail2(`No request "${args.id}" on ${target}. It may have been cleared or aged out \u2014 re-list with get_network_requests.`);
|
|
1907
1936
|
const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
|
|
1908
|
-
if (capped) return
|
|
1909
|
-
return
|
|
1937
|
+
if (capped) return fail2(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
|
|
1938
|
+
return text(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
|
|
1910
1939
|
}
|
|
1911
1940
|
case "remove":
|
|
1912
1941
|
await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
|
|
1913
|
-
return
|
|
1942
|
+
return text(`Removed kept record \`${args.key}\` on ${target}.`);
|
|
1914
1943
|
case "clearPinned":
|
|
1915
1944
|
await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
|
|
1916
|
-
return
|
|
1945
|
+
return text(`Unpinned all requests on ${target} (saved ones kept).`);
|
|
1917
1946
|
case "clearSaved":
|
|
1918
1947
|
await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
|
|
1919
|
-
return
|
|
1948
|
+
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
1920
1949
|
case "clearRequests":
|
|
1921
1950
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
1922
|
-
return
|
|
1951
|
+
return text(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
|
|
1923
1952
|
}
|
|
1924
1953
|
} catch (e) {
|
|
1925
|
-
return
|
|
1954
|
+
return fail2(errMsg2(e));
|
|
1926
1955
|
}
|
|
1927
1956
|
}
|
|
1928
1957
|
);
|
|
@@ -1931,48 +1960,48 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1931
1960
|
{
|
|
1932
1961
|
description: "Force matching HTTP requests to return a chosen status/body, to fail outright, or to arrive late \u2014 without touching the backend or the app's code. This is how you drive an app into a state it won't reach on its own: force a 500 and see whether the error UI appears, return [] and check the empty state, simulate offline or a slow connection. Rules run INSIDE the app and survive an app reload, so they also cover the boot path. After adding one, trigger the request (navigate/tap_element) and read the result with get_network_requests or describe_screen. ALWAYS remove your rules when done \u2014 a forgotten rule looks exactly like a real bug. Requires Buoy Pro.",
|
|
1933
1962
|
inputSchema: {
|
|
1934
|
-
action:
|
|
1963
|
+
action: import_zod5.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
1935
1964
|
"add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
|
|
1936
1965
|
),
|
|
1937
|
-
urlPattern:
|
|
1966
|
+
urlPattern: import_zod5.z.string().optional().describe(
|
|
1938
1967
|
"For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."
|
|
1939
1968
|
),
|
|
1940
|
-
fromRequestId:
|
|
1969
|
+
fromRequestId: import_zod5.z.string().optional().describe(
|
|
1941
1970
|
"For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."
|
|
1942
1971
|
),
|
|
1943
|
-
methods:
|
|
1944
|
-
kind:
|
|
1972
|
+
methods: import_zod5.z.array(import_zod5.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
|
|
1973
|
+
kind: import_zod5.z.enum(["respond", "fail", "delay"]).optional().describe(
|
|
1945
1974
|
"respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."
|
|
1946
1975
|
),
|
|
1947
|
-
status:
|
|
1976
|
+
status: import_zod5.z.number().optional().describe(
|
|
1948
1977
|
"For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."
|
|
1949
1978
|
),
|
|
1950
|
-
body:
|
|
1979
|
+
body: import_zod5.z.union([import_zod5.z.string(), import_zod5.z.record(import_zod5.z.unknown()), import_zod5.z.array(import_zod5.z.unknown())]).optional().describe(
|
|
1951
1980
|
"For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
|
|
1952
1981
|
),
|
|
1953
|
-
statusText:
|
|
1982
|
+
statusText: import_zod5.z.string().optional().describe(
|
|
1954
1983
|
"For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
|
|
1955
1984
|
),
|
|
1956
|
-
headers:
|
|
1957
|
-
failKind:
|
|
1958
|
-
delayMs:
|
|
1959
|
-
times:
|
|
1985
|
+
headers: import_zod5.z.record(import_zod5.z.string()).optional().describe("For kind 'respond': response headers to send."),
|
|
1986
|
+
failKind: import_zod5.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
|
|
1987
|
+
delayMs: import_zod5.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
|
|
1988
|
+
times: import_zod5.z.number().optional().describe(
|
|
1960
1989
|
"Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
|
|
1961
1990
|
),
|
|
1962
|
-
alternate:
|
|
1991
|
+
alternate: import_zod5.z.boolean().optional().describe(
|
|
1963
1992
|
"Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."
|
|
1964
1993
|
),
|
|
1965
|
-
name:
|
|
1966
|
-
enabled:
|
|
1994
|
+
name: import_zod5.z.string().optional().describe("Label shown in the developer's UI."),
|
|
1995
|
+
enabled: import_zod5.z.boolean().optional().describe(
|
|
1967
1996
|
"For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
|
|
1968
1997
|
),
|
|
1969
|
-
id:
|
|
1998
|
+
id: import_zod5.z.string().optional().describe(
|
|
1970
1999
|
"Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
|
|
1971
2000
|
),
|
|
1972
|
-
url:
|
|
2001
|
+
url: import_zod5.z.string().optional().describe(
|
|
1973
2002
|
"For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
|
|
1974
2003
|
),
|
|
1975
|
-
deviceId:
|
|
2004
|
+
deviceId: deviceIdInput
|
|
1976
2005
|
}
|
|
1977
2006
|
},
|
|
1978
2007
|
async (args) => {
|
|
@@ -1981,7 +2010,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1981
2010
|
switch (args.action) {
|
|
1982
2011
|
case "add": {
|
|
1983
2012
|
if (!args.urlPattern && !args.fromRequestId) {
|
|
1984
|
-
return
|
|
2013
|
+
return fail2(
|
|
1985
2014
|
"`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'."
|
|
1986
2015
|
);
|
|
1987
2016
|
}
|
|
@@ -2013,13 +2042,13 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2013
2042
|
);
|
|
2014
2043
|
const shaped = res;
|
|
2015
2044
|
if (!shaped?.ok) {
|
|
2016
|
-
return
|
|
2045
|
+
return fail2(shaped?.error ?? `Could not add the rule on ${target}.`);
|
|
2017
2046
|
}
|
|
2018
2047
|
const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
|
|
2019
2048
|
const armed = shaped.rule?.enabled === false ? `
|
|
2020
2049
|
|
|
2021
2050
|
It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
|
|
2022
|
-
return
|
|
2051
|
+
return text(
|
|
2023
2052
|
`Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
|
|
2024
2053
|
|
|
2025
2054
|
Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${shaped.rule?.id}"}) when you're done.`
|
|
@@ -2027,14 +2056,14 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2027
2056
|
}
|
|
2028
2057
|
case "remove": {
|
|
2029
2058
|
if (!args.id) {
|
|
2030
|
-
return
|
|
2059
|
+
return fail2('`remove` needs an `id`. List them with network_override({action:"list"}).');
|
|
2031
2060
|
}
|
|
2032
2061
|
await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
|
|
2033
|
-
return
|
|
2062
|
+
return text(`Removed override \`${args.id}\` on ${target}.`);
|
|
2034
2063
|
}
|
|
2035
2064
|
case "clear":
|
|
2036
2065
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2037
|
-
return
|
|
2066
|
+
return text(`Removed all override rules on ${target}. Traffic is real again.`);
|
|
2038
2067
|
case "enable":
|
|
2039
2068
|
case "disable": {
|
|
2040
2069
|
const enabled = args.action === "enable";
|
|
@@ -2045,12 +2074,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2045
2074
|
{ id: args.id, enabled },
|
|
2046
2075
|
target
|
|
2047
2076
|
);
|
|
2048
|
-
return
|
|
2077
|
+
return text(
|
|
2049
2078
|
`Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2050
2079
|
);
|
|
2051
2080
|
}
|
|
2052
2081
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2053
|
-
return
|
|
2082
|
+
return text(
|
|
2054
2083
|
enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2055
2084
|
);
|
|
2056
2085
|
}
|
|
@@ -2065,11 +2094,11 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2065
2094
|
if (args.id) {
|
|
2066
2095
|
const rule = rules.find((r) => r.id === args.id);
|
|
2067
2096
|
if (!rule) {
|
|
2068
|
-
return
|
|
2097
|
+
return fail2(
|
|
2069
2098
|
`No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
|
|
2070
2099
|
);
|
|
2071
2100
|
}
|
|
2072
|
-
return
|
|
2101
|
+
return text(
|
|
2073
2102
|
`**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
|
|
2074
2103
|
|
|
2075
2104
|
\`\`\`json
|
|
@@ -2078,7 +2107,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2078
2107
|
);
|
|
2079
2108
|
}
|
|
2080
2109
|
if (rules.length === 0) {
|
|
2081
|
-
return
|
|
2110
|
+
return text(
|
|
2082
2111
|
`No override rules on ${target}.
|
|
2083
2112
|
|
|
2084
2113
|
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
@@ -2106,7 +2135,7 @@ Add one with network_override({action:"add", urlPattern:"*", status:500}) to for
|
|
|
2106
2135
|
return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
|
|
2107
2136
|
\u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
|
|
2108
2137
|
});
|
|
2109
|
-
return
|
|
2138
|
+
return text(
|
|
2110
2139
|
`**Override rules on ${target}**${masterNote(res)}
|
|
2111
2140
|
|
|
2112
2141
|
${rows.join("\n")}
|
|
@@ -2122,11 +2151,11 @@ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).
|
|
|
2122
2151
|
target
|
|
2123
2152
|
);
|
|
2124
2153
|
if (!res) {
|
|
2125
|
-
return
|
|
2154
|
+
return fail2(
|
|
2126
2155
|
`${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
|
|
2127
2156
|
);
|
|
2128
2157
|
}
|
|
2129
|
-
return
|
|
2158
|
+
return text(
|
|
2130
2159
|
`**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
|
|
2131
2160
|
|
|
2132
2161
|
\`\`\`json
|
|
@@ -2138,24 +2167,21 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2138
2167
|
}
|
|
2139
2168
|
}
|
|
2140
2169
|
} catch (e) {
|
|
2141
|
-
return
|
|
2170
|
+
return fail2(errMsg2(e));
|
|
2142
2171
|
}
|
|
2143
2172
|
}
|
|
2144
2173
|
);
|
|
2145
2174
|
}
|
|
2146
2175
|
|
|
2147
2176
|
// src/tools/packs/reactQuery.ts
|
|
2148
|
-
var
|
|
2177
|
+
var import_zod6 = require("zod");
|
|
2149
2178
|
var RQ_TOOL_ID = "query";
|
|
2150
2179
|
var RQ_TOOL_ID_LEGACY = "react-query";
|
|
2151
|
-
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2152
|
-
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2153
|
-
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
2154
2180
|
async function callQueryAction(sync, action, params, target) {
|
|
2155
2181
|
try {
|
|
2156
2182
|
return await sync.callAction(RQ_TOOL_ID, action, params, target);
|
|
2157
2183
|
} catch (e) {
|
|
2158
|
-
const msg =
|
|
2184
|
+
const msg = errMsg2(e);
|
|
2159
2185
|
if (!/unknown tool/i.test(msg)) throw e;
|
|
2160
2186
|
return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
|
|
2161
2187
|
}
|
|
@@ -2183,10 +2209,10 @@ function registerReactQueryTools(server, sync) {
|
|
|
2183
2209
|
{
|
|
2184
2210
|
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.",
|
|
2185
2211
|
inputSchema: {
|
|
2186
|
-
includeData:
|
|
2187
|
-
staleOnly:
|
|
2188
|
-
limit:
|
|
2189
|
-
deviceId:
|
|
2212
|
+
includeData: import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
2213
|
+
staleOnly: import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),
|
|
2214
|
+
limit: import_zod6.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
|
|
2215
|
+
deviceId: deviceIdInput
|
|
2190
2216
|
}
|
|
2191
2217
|
},
|
|
2192
2218
|
async (args) => {
|
|
@@ -2199,12 +2225,12 @@ function registerReactQueryTools(server, sync) {
|
|
|
2199
2225
|
target
|
|
2200
2226
|
);
|
|
2201
2227
|
if (!res || !Array.isArray(res.queries)) {
|
|
2202
|
-
return
|
|
2228
|
+
return fail2(
|
|
2203
2229
|
"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)."
|
|
2204
2230
|
);
|
|
2205
2231
|
}
|
|
2206
2232
|
if (res.queries.length === 0) {
|
|
2207
|
-
return
|
|
2233
|
+
return text(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
|
|
2208
2234
|
}
|
|
2209
2235
|
const lines = res.queries.map((q, i) => {
|
|
2210
2236
|
const icon = STATUS_ICON[q.status] ?? "\u2022";
|
|
@@ -2217,11 +2243,11 @@ function registerReactQueryTools(server, sync) {
|
|
|
2217
2243
|
const staleCount = res.queries.filter((q) => q.isStale).length;
|
|
2218
2244
|
const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
|
|
2219
2245
|
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._";
|
|
2220
|
-
return
|
|
2246
|
+
return text(`${header}
|
|
2221
2247
|
|
|
2222
2248
|
${lines.join("\n")}${note}`);
|
|
2223
2249
|
} catch (e) {
|
|
2224
|
-
return
|
|
2250
|
+
return fail2(errMsg2(e));
|
|
2225
2251
|
}
|
|
2226
2252
|
}
|
|
2227
2253
|
);
|
|
@@ -2230,9 +2256,9 @@ ${lines.join("\n")}${note}`);
|
|
|
2230
2256
|
{
|
|
2231
2257
|
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.",
|
|
2232
2258
|
inputSchema: {
|
|
2233
|
-
action:
|
|
2234
|
-
queryHash:
|
|
2235
|
-
deviceId:
|
|
2259
|
+
action: import_zod6.z.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
|
|
2260
|
+
queryHash: import_zod6.z.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
|
|
2261
|
+
deviceId: deviceIdInput
|
|
2236
2262
|
}
|
|
2237
2263
|
},
|
|
2238
2264
|
async (args) => {
|
|
@@ -2240,174 +2266,145 @@ ${lines.join("\n")}${note}`);
|
|
|
2240
2266
|
const target = sync.requireProDevice(args.deviceId);
|
|
2241
2267
|
if (args.action === "clear") {
|
|
2242
2268
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
2243
|
-
return
|
|
2269
|
+
return text("Cleared the entire React Query cache.");
|
|
2244
2270
|
}
|
|
2245
2271
|
if (!args.queryHash) {
|
|
2246
|
-
return
|
|
2272
|
+
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
2247
2273
|
}
|
|
2248
2274
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
2249
|
-
return
|
|
2275
|
+
return text(`Ran ${args.action} on query \`${args.queryHash}\`.`);
|
|
2250
2276
|
} catch (e) {
|
|
2251
|
-
return
|
|
2277
|
+
return fail2(errMsg2(e));
|
|
2252
2278
|
}
|
|
2253
2279
|
}
|
|
2254
2280
|
);
|
|
2255
2281
|
}
|
|
2256
2282
|
|
|
2257
2283
|
// src/tools/packs/zustand.ts
|
|
2258
|
-
var
|
|
2284
|
+
var import_zod7 = require("zod");
|
|
2259
2285
|
var ZUSTAND_TOOL_ID = "zustand";
|
|
2260
|
-
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2261
|
-
var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2262
|
-
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
2263
2286
|
function registerZustandTools(server, sync) {
|
|
2264
2287
|
server.registerTool(
|
|
2265
2288
|
"get_zustand_state",
|
|
2266
2289
|
{
|
|
2267
2290
|
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'].)",
|
|
2268
2291
|
inputSchema: {
|
|
2269
|
-
includeValues:
|
|
2270
|
-
limit:
|
|
2271
|
-
deviceId:
|
|
2292
|
+
includeValues: import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
2293
|
+
limit: import_zod7.z.number().optional().describe("First N stores. Default all."),
|
|
2294
|
+
deviceId: deviceIdInput
|
|
2272
2295
|
}
|
|
2273
2296
|
},
|
|
2274
|
-
async (args) => {
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2297
|
+
withDevice(sync, async (args, target) => {
|
|
2298
|
+
const res = await sync.callAction(
|
|
2299
|
+
ZUSTAND_TOOL_ID,
|
|
2300
|
+
"listStores",
|
|
2301
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
2302
|
+
target
|
|
2303
|
+
);
|
|
2304
|
+
if (!res || !Array.isArray(res.stores)) {
|
|
2305
|
+
return fail2(
|
|
2306
|
+
inactiveToolMessage({ toolId: ZUSTAND_TOOL_ID, mcpTool: "get_zustand_state", action: "listStores" })
|
|
2282
2307
|
);
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
}
|
|
2291
|
-
const lines = res.stores.map((s, i) => {
|
|
2292
|
-
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
2293
|
-
const body = res.includedValues ? `
|
|
2308
|
+
}
|
|
2309
|
+
if (res.stores.length === 0) {
|
|
2310
|
+
return text("No Zustand stores are registered (call watchStores to register them).");
|
|
2311
|
+
}
|
|
2312
|
+
const lines = res.stores.map((s, i) => {
|
|
2313
|
+
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
2314
|
+
const body = res.includedValues ? `
|
|
2294
2315
|
${JSON.stringify(s.currentState)}` : ` \xB7 keys: ${(s.keys ?? []).join(", ") || "\u2014"}`;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2316
|
+
return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
|
|
2317
|
+
});
|
|
2318
|
+
const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
|
|
2319
|
+
return text(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
|
|
2299
2320
|
|
|
2300
2321
|
${lines.join("\n")}${note}`);
|
|
2301
|
-
|
|
2302
|
-
return fail6(errMsg6(e));
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2322
|
+
})
|
|
2305
2323
|
);
|
|
2306
2324
|
server.registerTool(
|
|
2307
2325
|
"zustand_action",
|
|
2308
2326
|
{
|
|
2309
2327
|
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.",
|
|
2310
2328
|
inputSchema: {
|
|
2311
|
-
store:
|
|
2312
|
-
state:
|
|
2313
|
-
replace:
|
|
2314
|
-
deviceId:
|
|
2329
|
+
store: import_zod7.z.string().describe("Store name to target (from get_zustand_state)."),
|
|
2330
|
+
state: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).describe("The new state object."),
|
|
2331
|
+
replace: import_zod7.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
|
|
2332
|
+
deviceId: deviceIdInput
|
|
2315
2333
|
}
|
|
2316
2334
|
},
|
|
2317
|
-
async (args) => {
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
return text5(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2327
|
-
} catch (e) {
|
|
2328
|
-
return fail6(errMsg6(e));
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2335
|
+
withDevice(sync, async (args, target) => {
|
|
2336
|
+
await sync.callAction(
|
|
2337
|
+
ZUSTAND_TOOL_ID,
|
|
2338
|
+
"setState",
|
|
2339
|
+
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
2340
|
+
target
|
|
2341
|
+
);
|
|
2342
|
+
return text(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2343
|
+
})
|
|
2331
2344
|
);
|
|
2332
2345
|
}
|
|
2333
2346
|
|
|
2334
2347
|
// src/tools/packs/jotai.ts
|
|
2335
|
-
var
|
|
2348
|
+
var import_zod8 = require("zod");
|
|
2336
2349
|
var JOTAI_TOOL_ID = "jotai";
|
|
2337
|
-
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2338
|
-
var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2339
|
-
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
2340
2350
|
function registerJotaiTools(server, sync) {
|
|
2341
2351
|
server.registerTool(
|
|
2342
2352
|
"get_jotai_state",
|
|
2343
2353
|
{
|
|
2344
2354
|
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'].)",
|
|
2345
2355
|
inputSchema: {
|
|
2346
|
-
includeValues:
|
|
2347
|
-
limit:
|
|
2348
|
-
deviceId:
|
|
2356
|
+
includeValues: import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
2357
|
+
limit: import_zod8.z.number().optional().describe("First N atoms. Default all."),
|
|
2358
|
+
deviceId: deviceIdInput
|
|
2349
2359
|
}
|
|
2350
2360
|
},
|
|
2351
|
-
async (args) => {
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2361
|
+
withDevice(sync, async (args, target) => {
|
|
2362
|
+
const res = await sync.callAction(
|
|
2363
|
+
JOTAI_TOOL_ID,
|
|
2364
|
+
"listAtoms",
|
|
2365
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
2366
|
+
target
|
|
2367
|
+
);
|
|
2368
|
+
if (!res || !Array.isArray(res.atoms)) {
|
|
2369
|
+
return fail2(
|
|
2370
|
+
inactiveToolMessage({ toolId: JOTAI_TOOL_ID, mcpTool: "get_jotai_state", action: "listAtoms" })
|
|
2359
2371
|
);
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
}
|
|
2368
|
-
const lines = res.atoms.map((a, i) => {
|
|
2369
|
-
const kind = a.writable ? "writable" : "read-only";
|
|
2370
|
-
const value = res.includedValues ? `
|
|
2372
|
+
}
|
|
2373
|
+
if (res.atoms.length === 0) {
|
|
2374
|
+
return text("No Jotai atoms are being watched (call watchAtoms to register them).");
|
|
2375
|
+
}
|
|
2376
|
+
const lines = res.atoms.map((a, i) => {
|
|
2377
|
+
const kind = a.writable ? "writable" : "read-only";
|
|
2378
|
+
const value = res.includedValues ? `
|
|
2371
2379
|
${JSON.stringify(a.currentValue)}` : "";
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2380
|
+
return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
|
|
2381
|
+
});
|
|
2382
|
+
const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
|
|
2383
|
+
return text(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
|
|
2376
2384
|
|
|
2377
2385
|
${lines.join("\n")}${note}`);
|
|
2378
|
-
|
|
2379
|
-
return fail7(errMsg7(e));
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2386
|
+
})
|
|
2382
2387
|
);
|
|
2383
2388
|
server.registerTool(
|
|
2384
2389
|
"jotai_action",
|
|
2385
2390
|
{
|
|
2386
2391
|
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.",
|
|
2387
2392
|
inputSchema: {
|
|
2388
|
-
atom:
|
|
2389
|
-
value:
|
|
2390
|
-
deviceId:
|
|
2393
|
+
atom: import_zod8.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
|
|
2394
|
+
value: import_zod8.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
|
|
2395
|
+
deviceId: deviceIdInput
|
|
2391
2396
|
}
|
|
2392
2397
|
},
|
|
2393
|
-
async (args) => {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
return text6(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2398
|
-
} catch (e) {
|
|
2399
|
-
return fail7(errMsg7(e));
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2398
|
+
withDevice(sync, async (args, target) => {
|
|
2399
|
+
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
2400
|
+
return text(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2401
|
+
})
|
|
2402
2402
|
);
|
|
2403
2403
|
}
|
|
2404
2404
|
|
|
2405
2405
|
// src/tools/packs/redux.ts
|
|
2406
|
-
var
|
|
2406
|
+
var import_zod9 = require("zod");
|
|
2407
2407
|
var REDUX_TOOL_ID = "redux";
|
|
2408
|
-
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2409
|
-
var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2410
|
-
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
2411
2408
|
function noStoreMessage(reason) {
|
|
2412
2409
|
switch (reason) {
|
|
2413
2410
|
case "no-react-redux":
|
|
@@ -2426,8 +2423,8 @@ function registerReduxTools(server, sync) {
|
|
|
2426
2423
|
{
|
|
2427
2424
|
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'].)",
|
|
2428
2425
|
inputSchema: {
|
|
2429
|
-
includeValues:
|
|
2430
|
-
deviceId:
|
|
2426
|
+
includeValues: import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
2427
|
+
deviceId: deviceIdInput
|
|
2431
2428
|
}
|
|
2432
2429
|
},
|
|
2433
2430
|
async (args) => {
|
|
@@ -2440,15 +2437,15 @@ function registerReduxTools(server, sync) {
|
|
|
2440
2437
|
target
|
|
2441
2438
|
);
|
|
2442
2439
|
if (!res) {
|
|
2443
|
-
return
|
|
2440
|
+
return fail2(
|
|
2444
2441
|
"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)."
|
|
2445
2442
|
);
|
|
2446
2443
|
}
|
|
2447
|
-
if (!res.available) return
|
|
2444
|
+
if (!res.available) return fail2(noStoreMessage(res.reason));
|
|
2448
2445
|
const slices = res.slices.length ? res.slices.join(", ") : "(none)";
|
|
2449
2446
|
const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
|
|
2450
2447
|
if (args.includeValues) {
|
|
2451
|
-
return
|
|
2448
|
+
return text(
|
|
2452
2449
|
`**Redux store \u2014 slices: ${slices}**
|
|
2453
2450
|
|
|
2454
2451
|
\`\`\`json
|
|
@@ -2456,13 +2453,13 @@ ${JSON.stringify(res.state, null, 2)}
|
|
|
2456
2453
|
\`\`\`${captureNote}`
|
|
2457
2454
|
);
|
|
2458
2455
|
}
|
|
2459
|
-
return
|
|
2456
|
+
return text(
|
|
2460
2457
|
`**Redux store \u2014 slices: ${slices}**
|
|
2461
2458
|
|
|
2462
2459
|
_Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
|
|
2463
2460
|
);
|
|
2464
2461
|
} catch (e) {
|
|
2465
|
-
return
|
|
2462
|
+
return fail2(errMsg2(e));
|
|
2466
2463
|
}
|
|
2467
2464
|
}
|
|
2468
2465
|
);
|
|
@@ -2471,9 +2468,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
2471
2468
|
{
|
|
2472
2469
|
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 }.",
|
|
2473
2470
|
inputSchema: {
|
|
2474
|
-
type:
|
|
2475
|
-
payload:
|
|
2476
|
-
deviceId:
|
|
2471
|
+
type: import_zod9.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
|
|
2472
|
+
payload: import_zod9.z.unknown().optional().describe("Optional action payload (any JSON)."),
|
|
2473
|
+
deviceId: deviceIdInput
|
|
2477
2474
|
}
|
|
2478
2475
|
},
|
|
2479
2476
|
async (args) => {
|
|
@@ -2482,21 +2479,18 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
2482
2479
|
const action = { type: args.type };
|
|
2483
2480
|
if (args.payload !== void 0) action.payload = args.payload;
|
|
2484
2481
|
const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
|
|
2485
|
-
if (!res || !res.dispatched) return
|
|
2486
|
-
return
|
|
2482
|
+
if (!res || !res.dispatched) return fail2(noStoreMessage(res?.reason));
|
|
2483
|
+
return text(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
|
|
2487
2484
|
} catch (e) {
|
|
2488
|
-
return
|
|
2485
|
+
return fail2(errMsg2(e));
|
|
2489
2486
|
}
|
|
2490
2487
|
}
|
|
2491
2488
|
);
|
|
2492
2489
|
}
|
|
2493
2490
|
|
|
2494
2491
|
// src/tools/packs/storage.ts
|
|
2495
|
-
var
|
|
2492
|
+
var import_zod10 = require("zod");
|
|
2496
2493
|
var STORAGE_TOOL_ID = "storage";
|
|
2497
|
-
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2498
|
-
var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2499
|
-
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
2500
2494
|
function truncate2(v, n = 200) {
|
|
2501
2495
|
if (v === null || v === void 0) return "(null)";
|
|
2502
2496
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -2508,10 +2502,10 @@ function registerStorageTools(server, sync) {
|
|
|
2508
2502
|
{
|
|
2509
2503
|
description: "Read the app's storage keys across ALL backends: AsyncStorage, MMKV (per instance), and Expo SecureStore. Data wrong or missing after an app restart \u2192 read the persisted values here FIRST \u2014 the bug is usually a stale, missing, or malformed stored value, and reading it beats hunting for it in code. Compact by DEFAULT \u2014 keys only \u2014 so it's token-cheap; pass includeValues:true for the values (truncated), or a `pattern` substring to narrow. Buoy's own internal keys are already filtered out.",
|
|
2510
2504
|
inputSchema: {
|
|
2511
|
-
pattern:
|
|
2512
|
-
includeValues:
|
|
2513
|
-
limit:
|
|
2514
|
-
deviceId:
|
|
2505
|
+
pattern: import_zod10.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
|
|
2506
|
+
includeValues: import_zod10.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
|
|
2507
|
+
limit: import_zod10.z.number().optional().describe("Max keys per backend section. Default 100."),
|
|
2508
|
+
deviceId: deviceIdInput
|
|
2515
2509
|
}
|
|
2516
2510
|
},
|
|
2517
2511
|
async (args) => {
|
|
@@ -2523,7 +2517,7 @@ function registerStorageTools(server, sync) {
|
|
|
2523
2517
|
const sections = [];
|
|
2524
2518
|
const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
|
|
2525
2519
|
if (!Array.isArray(allKeys)) {
|
|
2526
|
-
return
|
|
2520
|
+
return fail2("No storage result from the device. The storage tool may be inactive.");
|
|
2527
2521
|
}
|
|
2528
2522
|
const asyncKeys = allKeys.filter(matches2).slice(0, limit);
|
|
2529
2523
|
if (asyncKeys.length > 0) {
|
|
@@ -2564,12 +2558,12 @@ ${lines.join("\n")}`);
|
|
|
2564
2558
|
${lines.join("\n")}`);
|
|
2565
2559
|
}
|
|
2566
2560
|
if (sections.length === 0) {
|
|
2567
|
-
return
|
|
2561
|
+
return text(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
|
|
2568
2562
|
}
|
|
2569
2563
|
const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
|
|
2570
|
-
return
|
|
2564
|
+
return text(`${sections.join("\n\n")}${note}`);
|
|
2571
2565
|
} catch (e) {
|
|
2572
|
-
return
|
|
2566
|
+
return fail2(errMsg2(e));
|
|
2573
2567
|
}
|
|
2574
2568
|
}
|
|
2575
2569
|
);
|
|
@@ -2578,10 +2572,10 @@ ${lines.join("\n")}`);
|
|
|
2578
2572
|
{
|
|
2579
2573
|
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.",
|
|
2580
2574
|
inputSchema: {
|
|
2581
|
-
action:
|
|
2582
|
-
key:
|
|
2583
|
-
value:
|
|
2584
|
-
deviceId:
|
|
2575
|
+
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
|
|
2576
|
+
key: import_zod10.z.string().optional().describe("The key (required for set and remove)."),
|
|
2577
|
+
value: import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2578
|
+
deviceId: deviceIdInput
|
|
2585
2579
|
}
|
|
2586
2580
|
},
|
|
2587
2581
|
async (args) => {
|
|
@@ -2589,33 +2583,30 @@ ${lines.join("\n")}`);
|
|
|
2589
2583
|
const target = sync.requireProDevice(args.deviceId);
|
|
2590
2584
|
if (args.action === "clear") {
|
|
2591
2585
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2592
|
-
return
|
|
2586
|
+
return text("Cleared all app storage (Buoy's own devtool keys preserved).");
|
|
2593
2587
|
}
|
|
2594
2588
|
if (!args.key) {
|
|
2595
|
-
return
|
|
2589
|
+
return fail2(`'${args.action}' needs a key.`);
|
|
2596
2590
|
}
|
|
2597
2591
|
if (args.action === "remove") {
|
|
2598
2592
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2599
|
-
return
|
|
2593
|
+
return text(`Removed storage key "${args.key}".`);
|
|
2600
2594
|
}
|
|
2601
2595
|
if (args.value === void 0) {
|
|
2602
|
-
return
|
|
2596
|
+
return fail2("'set' needs a value (a string).");
|
|
2603
2597
|
}
|
|
2604
2598
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2605
|
-
return
|
|
2599
|
+
return text(`Set storage key "${args.key}".`);
|
|
2606
2600
|
} catch (e) {
|
|
2607
|
-
return
|
|
2601
|
+
return fail2(errMsg2(e));
|
|
2608
2602
|
}
|
|
2609
2603
|
}
|
|
2610
2604
|
);
|
|
2611
2605
|
}
|
|
2612
2606
|
|
|
2613
2607
|
// src/tools/packs/routes.ts
|
|
2614
|
-
var
|
|
2608
|
+
var import_zod11 = require("zod");
|
|
2615
2609
|
var ROUTES_TOOL_ID = "route-events";
|
|
2616
|
-
var text9 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2617
|
-
var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2618
|
-
var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
|
|
2619
2610
|
function flattenRoutes(routes, acc = []) {
|
|
2620
2611
|
for (const r of routes ?? []) {
|
|
2621
2612
|
if (r.path) acc.push(r.path);
|
|
@@ -2629,8 +2620,8 @@ function registerRoutesTools(server, sync) {
|
|
|
2629
2620
|
{
|
|
2630
2621
|
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'].)",
|
|
2631
2622
|
inputSchema: {
|
|
2632
|
-
includeSitemap:
|
|
2633
|
-
deviceId:
|
|
2623
|
+
includeSitemap: import_zod11.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
|
|
2624
|
+
deviceId: deviceIdInput
|
|
2634
2625
|
}
|
|
2635
2626
|
},
|
|
2636
2627
|
async (args) => {
|
|
@@ -2663,9 +2654,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2663
2654
|
**Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
|
|
2664
2655
|
}
|
|
2665
2656
|
parts.push("\n_Navigate with navigate({ path })._");
|
|
2666
|
-
return
|
|
2657
|
+
return text(parts.join("\n"));
|
|
2667
2658
|
} catch (e) {
|
|
2668
|
-
return
|
|
2659
|
+
return fail2(errMsg2(e));
|
|
2669
2660
|
}
|
|
2670
2661
|
}
|
|
2671
2662
|
);
|
|
@@ -2674,9 +2665,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2674
2665
|
{
|
|
2675
2666
|
description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",
|
|
2676
2667
|
inputSchema: {
|
|
2677
|
-
path:
|
|
2678
|
-
replace:
|
|
2679
|
-
deviceId:
|
|
2668
|
+
path: import_zod11.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
|
|
2669
|
+
replace: import_zod11.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
|
|
2670
|
+
deviceId: deviceIdInput
|
|
2680
2671
|
}
|
|
2681
2672
|
},
|
|
2682
2673
|
async (args) => {
|
|
@@ -2688,20 +2679,17 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2688
2679
|
{ path: args.path, replace: args.replace === true },
|
|
2689
2680
|
target
|
|
2690
2681
|
);
|
|
2691
|
-
return
|
|
2682
|
+
return text(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
|
|
2692
2683
|
} catch (e) {
|
|
2693
|
-
return
|
|
2684
|
+
return fail2(errMsg2(e));
|
|
2694
2685
|
}
|
|
2695
2686
|
}
|
|
2696
2687
|
);
|
|
2697
2688
|
}
|
|
2698
2689
|
|
|
2699
2690
|
// src/tools/packs/console.ts
|
|
2700
|
-
var
|
|
2691
|
+
var import_zod12 = require("zod");
|
|
2701
2692
|
var CONSOLE_TOOL_ID = "console";
|
|
2702
|
-
var text10 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2703
|
-
var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2704
|
-
var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
|
|
2705
2693
|
var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
|
|
2706
2694
|
var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
|
|
2707
2695
|
function hhmmss(ts) {
|
|
@@ -2715,57 +2703,275 @@ function registerConsoleTools(server, sync) {
|
|
|
2715
2703
|
{
|
|
2716
2704
|
description: "Read the app's captured console logs (console.log/warn/error/\u2026) as a compact tail \u2014 time \xB7 level \xB7 message. App crashing or erroring \u2192 read this FIRST: it captures crashes and errors including FATAL uncaught JS errors (tagged [FATAL], hooked from RN's global error handler), so a dead app's last words are here, not in the code. Token-cheap (uses each entry's pre-rendered message, not raw args). Filter by minimum level (e.g. level:'warning' for warnings+errors) or a message substring. For full args/stack of an entry, fall back to get_snapshot('console').",
|
|
2717
2705
|
inputSchema: {
|
|
2718
|
-
level:
|
|
2719
|
-
limit:
|
|
2720
|
-
pattern:
|
|
2721
|
-
deviceId:
|
|
2706
|
+
level: import_zod12.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2707
|
+
limit: import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),
|
|
2708
|
+
pattern: import_zod12.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
|
|
2709
|
+
deviceId: deviceIdInput
|
|
2710
|
+
}
|
|
2711
|
+
},
|
|
2712
|
+
withDevice(sync, async (args, target) => {
|
|
2713
|
+
const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
|
|
2714
|
+
const banner = staleHeader(stale);
|
|
2715
|
+
const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
|
|
2716
|
+
const all = Array.isArray(p.entries) ? p.entries : [];
|
|
2717
|
+
const minSev = args.level ? SEVERITY[args.level] : 0;
|
|
2718
|
+
const pattern = args.pattern?.toLowerCase();
|
|
2719
|
+
const filtered = all.filter(
|
|
2720
|
+
(e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
|
|
2721
|
+
);
|
|
2722
|
+
const sorted = [...filtered].sort((a, b) => a.id - b.id);
|
|
2723
|
+
const limit = clampLimit(args.limit, 50);
|
|
2724
|
+
const shown = sorted.slice(-limit);
|
|
2725
|
+
if (shown.length === 0) {
|
|
2726
|
+
return text(`${banner}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
|
|
2727
|
+
}
|
|
2728
|
+
const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
|
|
2729
|
+
const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
|
|
2730
|
+
const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
|
|
2731
|
+
return text(`${banner}${header}
|
|
2732
|
+
|
|
2733
|
+
\`\`\`
|
|
2734
|
+
${lines.join("\n")}
|
|
2735
|
+
\`\`\`
|
|
2736
|
+
|
|
2737
|
+
_Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
2738
|
+
})
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// src/tools/packs/sentry.ts
|
|
2743
|
+
var import_zod13 = require("zod");
|
|
2744
|
+
var SENTRY_TOOL_ID = "sentry";
|
|
2745
|
+
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2746
|
+
var fail3 = (message) => ({
|
|
2747
|
+
isError: true,
|
|
2748
|
+
content: [{ type: "text", text: message }]
|
|
2749
|
+
});
|
|
2750
|
+
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
2751
|
+
function hhmmss2(ts) {
|
|
2752
|
+
const d = new Date(ts);
|
|
2753
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2754
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
2755
|
+
}
|
|
2756
|
+
function bytes(n) {
|
|
2757
|
+
if (n < 1024) return `${n}B`;
|
|
2758
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
2759
|
+
return `${(n / (1024 * 1024)).toFixed(2)}MB`;
|
|
2760
|
+
}
|
|
2761
|
+
function units(n, format) {
|
|
2762
|
+
if (format === "bytes") return bytes(n);
|
|
2763
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2764
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
2765
|
+
return String(n);
|
|
2766
|
+
}
|
|
2767
|
+
var FREE_QUOTA = {
|
|
2768
|
+
errors: 5e3,
|
|
2769
|
+
spans: 5e6,
|
|
2770
|
+
replays: 50,
|
|
2771
|
+
logBytes: 5 * 1024 ** 3,
|
|
2772
|
+
attachmentBytes: 1 * 1024 ** 3
|
|
2773
|
+
};
|
|
2774
|
+
var CATEGORY_FORMAT = {
|
|
2775
|
+
errors: "count",
|
|
2776
|
+
spans: "count",
|
|
2777
|
+
replays: "count",
|
|
2778
|
+
logBytes: "bytes",
|
|
2779
|
+
attachmentBytes: "bytes"
|
|
2780
|
+
};
|
|
2781
|
+
function statusNote(status) {
|
|
2782
|
+
switch (status) {
|
|
2783
|
+
case "attached":
|
|
2784
|
+
return "";
|
|
2785
|
+
case "sdk-not-found":
|
|
2786
|
+
return "\n\n\u26A0\uFE0F The tool never found @sentry/react-native \u2014 nothing is being captured. The app must pass `getClient` to SentryRoot, or the SDK isn't installed.";
|
|
2787
|
+
case "no-client":
|
|
2788
|
+
return "\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";
|
|
2789
|
+
default:
|
|
2790
|
+
return "\n\n\u23F3 Still looking for the app's Sentry client.";
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function registerSentryTools(server, sync) {
|
|
2794
|
+
server.registerTool(
|
|
2795
|
+
"get_sentry_envelopes",
|
|
2796
|
+
{
|
|
2797
|
+
description: "Read what the app is actually sending to Sentry \u2014 every outgoing envelope with its item types, byte size, span count, and summary. Use this to answer 'is this error reaching Sentry', 'what does my app send at startup', or 'why is my Sentry bill high'. Debugging an error locally? These payloads are the final post-processing event, so you can read the exception, breadcrumbs and contexts here WITHOUT waiting for ingestion or spending quota. Filter by envelope item type (event / transaction / log / session / client_report / attachment / replay_event).",
|
|
2798
|
+
inputSchema: {
|
|
2799
|
+
type: import_zod13.z.string().optional().describe(
|
|
2800
|
+
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
2801
|
+
),
|
|
2802
|
+
limit: import_zod13.z.number().optional().describe("Most-recent N envelopes. Default 25."),
|
|
2803
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2722
2804
|
}
|
|
2723
2805
|
},
|
|
2724
2806
|
async (args) => {
|
|
2725
2807
|
try {
|
|
2726
2808
|
const target = sync.requireProDevice(args.deviceId);
|
|
2727
|
-
const { payload, stale } = await sync.getSnapshot(
|
|
2728
|
-
const
|
|
2809
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2810
|
+
const staleHeader2 = stale ? `${stale}
|
|
2729
2811
|
|
|
2730
2812
|
` : "";
|
|
2731
|
-
const p =
|
|
2732
|
-
const all = Array.isArray(p.
|
|
2733
|
-
const
|
|
2734
|
-
const
|
|
2735
|
-
const
|
|
2736
|
-
(e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
|
|
2737
|
-
);
|
|
2738
|
-
const sorted = [...filtered].sort((a, b) => a.id - b.id);
|
|
2739
|
-
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
|
|
2740
|
-
const shown = sorted.slice(-limit);
|
|
2813
|
+
const p = payload ?? {};
|
|
2814
|
+
const all = Array.isArray(p.envelopes) ? p.envelopes : [];
|
|
2815
|
+
const filtered = args.type ? all.filter((e) => e.items.some((i) => i.type === args.type)) : all;
|
|
2816
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
|
|
2817
|
+
const shown = filtered.slice(-limit);
|
|
2741
2818
|
if (shown.length === 0) {
|
|
2742
|
-
return
|
|
2819
|
+
return text2(
|
|
2820
|
+
`${staleHeader2}No Sentry envelopes${args.type ? ` containing a '${args.type}' item` : ""} (${all.length} captured this session).${statusNote(p.status)}`
|
|
2821
|
+
);
|
|
2743
2822
|
}
|
|
2744
|
-
const lines = shown.map((e) =>
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2823
|
+
const lines = shown.map((e) => {
|
|
2824
|
+
const types = e.items.map((i) => i.type).join("+");
|
|
2825
|
+
const spans = e.items.reduce((sum, i) => sum + (i.spanCount ?? 0), 0);
|
|
2826
|
+
const spanNote = spans > 0 ? ` \xB7 ${spans} span${spans === 1 ? "" : "s"}` : "";
|
|
2827
|
+
const head = `${e.id}. ${hhmmss2(e.timestamp)} [${types}] ${bytes(e.totalBytes)}${spanNote}`;
|
|
2828
|
+
return `${head}
|
|
2829
|
+
\u2514\u2500 ${e.items[0]?.summary ?? "envelope"}`;
|
|
2830
|
+
});
|
|
2831
|
+
const header = `**Sentry \u2014 ${all.length} envelope${all.length === 1 ? "" : "s"} sent (showing ${shown.length}${args.type ? `, type '${args.type}'` : ""}):**`;
|
|
2832
|
+
return text2(
|
|
2833
|
+
`${staleHeader2}${header}${statusNote(p.status)}
|
|
2748
2834
|
|
|
2749
|
-
\`\`\`
|
|
2750
2835
|
${lines.join("\n")}
|
|
2836
|
+
|
|
2837
|
+
_Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`
|
|
2838
|
+
);
|
|
2839
|
+
} catch (e) {
|
|
2840
|
+
return fail3(errMsg3(e));
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
);
|
|
2844
|
+
server.registerTool(
|
|
2845
|
+
"get_sentry_cost",
|
|
2846
|
+
{
|
|
2847
|
+
description: "What this session would cost in Sentry's billing units, and what that rate implies for a month against the free plan. Sentry bills on RECEIPT \u2014 data its server-side sampling later discards is still charged \u2014 and spans are billed individually, so one transaction is many units. Use this before shipping a Sentry config, or when asked why a Sentry bill is high.",
|
|
2848
|
+
inputSchema: {
|
|
2849
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2850
|
+
}
|
|
2851
|
+
},
|
|
2852
|
+
async (args) => {
|
|
2853
|
+
try {
|
|
2854
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2855
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2856
|
+
const staleHeader2 = stale ? `${stale}
|
|
2857
|
+
|
|
2858
|
+
` : "";
|
|
2859
|
+
const p = payload ?? {};
|
|
2860
|
+
const usage = p.usage;
|
|
2861
|
+
if (!usage?.session) {
|
|
2862
|
+
return text2(
|
|
2863
|
+
`${staleHeader2}No usage recorded yet \u2014 nothing has been sent to Sentry this session.${statusNote(p.status)}`
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const session = usage.session;
|
|
2867
|
+
const projected = usage.projectedMonthly;
|
|
2868
|
+
const keys = Object.keys(session);
|
|
2869
|
+
const rows = keys.map((key) => {
|
|
2870
|
+
const format = CATEGORY_FORMAT[key];
|
|
2871
|
+
const now = units(session[key], format);
|
|
2872
|
+
if (!projected) return `- ${key}: ${now} this session`;
|
|
2873
|
+
const month = projected[key];
|
|
2874
|
+
const quota = FREE_QUOTA[key];
|
|
2875
|
+
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
2876
|
+
const pctNote = pct === null ? " (not on the free plan)" : ` \u2014 ${pct}% of the free plan's ${units(quota, format)}`;
|
|
2877
|
+
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
2878
|
+
});
|
|
2879
|
+
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
2880
|
+
const contributors = usage.topContributors?.length ? `
|
|
2881
|
+
|
|
2882
|
+
**What produced it:**
|
|
2883
|
+
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
2884
|
+
const caveat = projected ? "\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._" : "\n\n_Session too short to project a month from._";
|
|
2885
|
+
return text2(
|
|
2886
|
+
`${staleHeader2}**Sentry cost \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
2887
|
+
|
|
2888
|
+
${rows.join("\n")}${contributors}${caveat}`
|
|
2889
|
+
);
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
return fail3(errMsg3(e));
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
);
|
|
2895
|
+
server.registerTool(
|
|
2896
|
+
"get_sentry_drops",
|
|
2897
|
+
{
|
|
2898
|
+
description: "Why events never reached Sentry. Answers 'I called captureException but the event isn't in Sentry' \u2014 the single most common Sentry problem \u2014 by naming the layer that discarded it: a beforeSend callback returning null, a sample-rate roll, dedupe or another event processor, React Native tracing discarding an empty transaction, or a native rate limit.",
|
|
2899
|
+
inputSchema: {
|
|
2900
|
+
limit: import_zod13.z.number().optional().describe("Most-recent N drop records. Default 30."),
|
|
2901
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2902
|
+
}
|
|
2903
|
+
},
|
|
2904
|
+
async (args) => {
|
|
2905
|
+
try {
|
|
2906
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2907
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2908
|
+
const staleHeader2 = stale ? `${stale}
|
|
2909
|
+
|
|
2910
|
+
` : "";
|
|
2911
|
+
const p = payload ?? {};
|
|
2912
|
+
const summary = p.drops?.summary ?? [];
|
|
2913
|
+
const recent = p.drops?.recent ?? [];
|
|
2914
|
+
if (summary.length === 0) {
|
|
2915
|
+
return text2(
|
|
2916
|
+
`${staleHeader2}Nothing dropped this session \u2014 every captured event reached the transport.${statusNote(p.status)}
|
|
2917
|
+
|
|
2918
|
+
_If an event is still missing from sentry.io, it left the device: check the DSN, the project, and Sentry's own inbound filters and quota._`
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
2922
|
+
const shown = recent.slice(-limit);
|
|
2923
|
+
const summaryLines = summary.map(
|
|
2924
|
+
(s) => `- ${s.reason}: ${s.units} unit${s.units === 1 ? "" : "s"} across ${s.records} record${s.records === 1 ? "" : "s"}`
|
|
2925
|
+
);
|
|
2926
|
+
const rows = shown.map(
|
|
2927
|
+
(d) => `${hhmmss2(d.timestamp)} [${d.reason}] ${d.category}${d.quantity > 1 ? ` \xD7${d.quantity}` : ""} \u2014 ${d.detail} (${d.source})`
|
|
2928
|
+
);
|
|
2929
|
+
return text2(
|
|
2930
|
+
`${staleHeader2}**Sentry drops \u2014 by reason:**
|
|
2931
|
+
|
|
2932
|
+
${summaryLines.join("\n")}
|
|
2933
|
+
|
|
2934
|
+
**Recent (${shown.length}):**
|
|
2935
|
+
|
|
2936
|
+
\`\`\`
|
|
2937
|
+
${rows.join("\n")}
|
|
2751
2938
|
\`\`\`
|
|
2752
2939
|
|
|
2753
|
-
|
|
2940
|
+
_"observed" means the tool watched it happen; "reported" means the SDK counted it afterwards. Native drops only appear when the SDK runs with debug enabled._`
|
|
2941
|
+
);
|
|
2942
|
+
} catch (e) {
|
|
2943
|
+
return fail3(errMsg3(e));
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
);
|
|
2947
|
+
server.registerTool(
|
|
2948
|
+
"sentry_action",
|
|
2949
|
+
{
|
|
2950
|
+
description: "Act on the Sentry inspector. 'clear' drops every captured envelope and drop record \u2014 use it to get a clean window before driving a flow you want to measure.",
|
|
2951
|
+
inputSchema: {
|
|
2952
|
+
action: import_zod13.z.enum(["clear"]).describe("The action to perform."),
|
|
2953
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2954
|
+
}
|
|
2955
|
+
},
|
|
2956
|
+
async (args) => {
|
|
2957
|
+
try {
|
|
2958
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2959
|
+
await sync.callAction(SENTRY_TOOL_ID, "clearEnvelopes", {}, target);
|
|
2960
|
+
return text2(
|
|
2961
|
+
"Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost."
|
|
2962
|
+
);
|
|
2754
2963
|
} catch (e) {
|
|
2755
|
-
return
|
|
2964
|
+
return fail3(errMsg3(e));
|
|
2756
2965
|
}
|
|
2757
2966
|
}
|
|
2758
2967
|
);
|
|
2759
2968
|
}
|
|
2760
2969
|
|
|
2761
2970
|
// src/tools/packs/timeMachine.ts
|
|
2762
|
-
var
|
|
2971
|
+
var import_zod14 = require("zod");
|
|
2763
2972
|
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2767
|
-
function kb(bytes) {
|
|
2768
|
-
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
2973
|
+
function kb(bytes2) {
|
|
2974
|
+
return bytes2 < 1024 ? `${bytes2} B` : `${(bytes2 / 1024).toFixed(1)} KB`;
|
|
2769
2975
|
}
|
|
2770
2976
|
function formatOutcome(outcome) {
|
|
2771
2977
|
const lines = Object.entries(outcome.results).map(([sid, r2]) => {
|
|
@@ -2790,7 +2996,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
2790
2996
|
{
|
|
2791
2997
|
description: "List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore it to re-run the flow from the same state.",
|
|
2792
2998
|
inputSchema: {
|
|
2793
|
-
deviceId:
|
|
2999
|
+
deviceId: deviceIdInput
|
|
2794
3000
|
}
|
|
2795
3001
|
},
|
|
2796
3002
|
async (args) => {
|
|
@@ -2823,9 +3029,9 @@ ${providerLines.join("\n") || "_no providers registered_"}`
|
|
|
2823
3029
|
];
|
|
2824
3030
|
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2825
3031
|
${formatOutcome(p.lastOutcome)}`);
|
|
2826
|
-
return
|
|
3032
|
+
return text(parts.join("\n\n"));
|
|
2827
3033
|
} catch (e) {
|
|
2828
|
-
return
|
|
3034
|
+
return fail2(errMsg2(e));
|
|
2829
3035
|
}
|
|
2830
3036
|
}
|
|
2831
3037
|
);
|
|
@@ -2834,14 +3040,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2834
3040
|
{
|
|
2835
3041
|
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, preview what restoring one would change (per-item added/removed/changed/won't-apply verdicts \u2014 check the blast radius BEFORE restoring), restore one (mode 'live' replaces state in place; 'reload' restores persisted storage then reloads the JS bundle \u2014 baselines always reload; pass restoreRoute:true to also return to the screen the snapshot was captured on), capture_baseline (an empty wipe-to-fresh-install restore point), delete, or rename. The restore result reports per-source applied/skipped counts \u2014 check them rather than assuming success.",
|
|
2836
3042
|
inputSchema: {
|
|
2837
|
-
action:
|
|
2838
|
-
id:
|
|
2839
|
-
name:
|
|
2840
|
-
mode:
|
|
2841
|
-
excludeKeys:
|
|
2842
|
-
restoreRoute:
|
|
2843
|
-
sourceIds:
|
|
2844
|
-
deviceId:
|
|
3043
|
+
action: import_zod14.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3044
|
+
id: import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
3045
|
+
name: import_zod14.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
3046
|
+
mode: import_zod14.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
3047
|
+
excludeKeys: import_zod14.z.array(import_zod14.z.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),
|
|
3048
|
+
restoreRoute: import_zod14.z.boolean().optional().describe("Restore only: also navigate back to the route the snapshot was captured on (shown as `route` by get_time_machine). With mode 'reload' the navigation happens automatically on the next boot. Default false \u2014 state is restored but the app stays on the current screen."),
|
|
3049
|
+
sourceIds: import_zod14.z.array(import_zod14.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
3050
|
+
deviceId: deviceIdInput
|
|
2845
3051
|
}
|
|
2846
3052
|
},
|
|
2847
3053
|
async (args) => {
|
|
@@ -2854,14 +3060,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2854
3060
|
const s = result.snapshot;
|
|
2855
3061
|
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2856
3062
|
\u26A0 ${sid}: ${w}`)).join("");
|
|
2857
|
-
return
|
|
3063
|
+
return text(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2858
3064
|
}
|
|
2859
3065
|
case "capture_baseline": {
|
|
2860
3066
|
const result = await call("captureBaseline", {});
|
|
2861
|
-
return
|
|
3067
|
+
return text(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2862
3068
|
}
|
|
2863
3069
|
case "preview": {
|
|
2864
|
-
if (!args.id) return
|
|
3070
|
+
if (!args.id) return fail2("preview requires `id` (from get_time_machine).");
|
|
2865
3071
|
const result = await call("preview", { id: args.id });
|
|
2866
3072
|
const lines = [];
|
|
2867
3073
|
let totalApplies = 0;
|
|
@@ -2880,46 +3086,46 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2880
3086
|
for (const note of src.notes) lines.push(` \xB7 ${note}`);
|
|
2881
3087
|
}
|
|
2882
3088
|
const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
|
|
2883
|
-
return
|
|
3089
|
+
return text(`${head}
|
|
2884
3090
|
${lines.join("\n")}`);
|
|
2885
3091
|
}
|
|
2886
3092
|
case "restore": {
|
|
2887
|
-
if (!args.id) return
|
|
3093
|
+
if (!args.id) return fail2("restore requires `id` (from get_time_machine).");
|
|
2888
3094
|
const outcome = await call("restore", {
|
|
2889
3095
|
id: args.id,
|
|
2890
3096
|
mode: args.mode ?? "live",
|
|
2891
3097
|
excludeKeys: args.excludeKeys,
|
|
2892
3098
|
restoreRoute: args.restoreRoute
|
|
2893
3099
|
});
|
|
2894
|
-
return
|
|
3100
|
+
return text(formatOutcome(outcome));
|
|
2895
3101
|
}
|
|
2896
3102
|
case "delete": {
|
|
2897
|
-
if (!args.id) return
|
|
3103
|
+
if (!args.id) return fail2("delete requires `id`.");
|
|
2898
3104
|
await call("delete", { id: args.id });
|
|
2899
|
-
return
|
|
3105
|
+
return text(`Deleted snapshot \`${args.id}\`.`);
|
|
2900
3106
|
}
|
|
2901
3107
|
case "rename": {
|
|
2902
|
-
if (!args.id || !args.name) return
|
|
3108
|
+
if (!args.id || !args.name) return fail2("rename requires `id` and `name`.");
|
|
2903
3109
|
await call("rename", { id: args.id, name: args.name });
|
|
2904
|
-
return
|
|
3110
|
+
return text(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2905
3111
|
}
|
|
2906
3112
|
}
|
|
2907
3113
|
} catch (e) {
|
|
2908
|
-
return
|
|
3114
|
+
return fail2(errMsg2(e));
|
|
2909
3115
|
}
|
|
2910
3116
|
}
|
|
2911
3117
|
);
|
|
2912
3118
|
}
|
|
2913
3119
|
|
|
2914
3120
|
// src/tools/packs/scenarios.ts
|
|
2915
|
-
var
|
|
3121
|
+
var import_zod15 = require("zod");
|
|
2916
3122
|
var SCENARIOS_TOOL_ID = "scenarios";
|
|
2917
|
-
var
|
|
2918
|
-
var
|
|
3123
|
+
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3124
|
+
var fail4 = (message) => ({
|
|
2919
3125
|
isError: true,
|
|
2920
3126
|
content: [{ type: "text", text: message }]
|
|
2921
3127
|
});
|
|
2922
|
-
var
|
|
3128
|
+
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
2923
3129
|
function varSpec(v) {
|
|
2924
3130
|
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
2925
3131
|
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
@@ -2981,7 +3187,7 @@ function registerScenariosTools(server, sync) {
|
|
|
2981
3187
|
{
|
|
2982
3188
|
description: "List the app's saved Scenarios \u2014 named, parameterized app states that can be applied with one call (override a response, write storage, impersonate a user, navigate). Shows what each one WILL do in plain English with a durability label per step, plus which scenario is currently active and what it changed. Use scenario_action to run one as a deterministic fixture BEFORE driving a flow, so the flow starts from a known state instead of whatever the app happened to be in. If a scenario is active, whatever you observe on the device is partly simulated \u2014 check here first when the app's data looks wrong.",
|
|
2983
3189
|
inputSchema: {
|
|
2984
|
-
deviceId:
|
|
3190
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2985
3191
|
}
|
|
2986
3192
|
},
|
|
2987
3193
|
async (args) => {
|
|
@@ -3011,9 +3217,9 @@ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing r
|
|
|
3011
3217
|
${drafts.map(summarize).join("\n")}`
|
|
3012
3218
|
);
|
|
3013
3219
|
}
|
|
3014
|
-
return
|
|
3220
|
+
return text3(parts.join("\n\n"));
|
|
3015
3221
|
} catch (e) {
|
|
3016
|
-
return
|
|
3222
|
+
return fail4(errMsg4(e));
|
|
3017
3223
|
}
|
|
3018
3224
|
}
|
|
3019
3225
|
);
|
|
@@ -3022,66 +3228,66 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3022
3228
|
{
|
|
3023
3229
|
description: `Drive Buoy Scenarios: 'preview' dry-runs one and reports exactly what it WOULD change (plus any conflict with the active scenario) without touching the app \u2014 do this before running an unfamiliar scenario; 'run' applies it and resolves only once every step has landed, which makes it a deterministic fixture to call before run_flow; 'deactivate' reverses it and reports what it could NOT restore (storage writes have no automatic inverse); 'save' writes a new scenario onto the device \u2014 it lands as a DRAFT that a human must accept on the device before it can run, so tell the user to go accept it; 'setFolder' files a saved scenario under a flow ("Checkout", "Login") or clears its folder, which is how a QA library stays navigable \u2014 check get_scenarios for the folders already in use and reuse one instead of inventing a near-duplicate; 'delete' removes a device scenario. Prefer a network override step over a query-cache poke: the override survives refetch and restart, the poke dies on the next fetch.`,
|
|
3024
3230
|
inputSchema: {
|
|
3025
|
-
action:
|
|
3026
|
-
folder:
|
|
3231
|
+
action: import_zod15.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3232
|
+
folder: import_zod15.z.string().optional().describe(
|
|
3027
3233
|
"Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."
|
|
3028
3234
|
),
|
|
3029
|
-
id:
|
|
3235
|
+
id: import_zod15.z.string().optional().describe(
|
|
3030
3236
|
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3031
3237
|
),
|
|
3032
|
-
params:
|
|
3238
|
+
params: import_zod15.z.record(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional().describe(
|
|
3033
3239
|
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3034
3240
|
),
|
|
3035
|
-
scenario:
|
|
3036
|
-
id:
|
|
3037
|
-
name:
|
|
3038
|
-
description:
|
|
3039
|
-
expectedOutcome:
|
|
3241
|
+
scenario: import_zod15.z.object({
|
|
3242
|
+
id: import_zod15.z.string().describe("Stable kebab-case id."),
|
|
3243
|
+
name: import_zod15.z.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3244
|
+
description: import_zod15.z.string().optional(),
|
|
3245
|
+
expectedOutcome: import_zod15.z.string().optional().describe(
|
|
3040
3246
|
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3041
3247
|
),
|
|
3042
|
-
author:
|
|
3043
|
-
authoredAgainst:
|
|
3044
|
-
folder:
|
|
3248
|
+
author: import_zod15.z.string().optional(),
|
|
3249
|
+
authoredAgainst: import_zod15.z.string().optional().describe("App version this was written against."),
|
|
3250
|
+
folder: import_zod15.z.string().optional().describe(
|
|
3045
3251
|
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3046
3252
|
),
|
|
3047
|
-
tags:
|
|
3048
|
-
vars:
|
|
3049
|
-
|
|
3050
|
-
key:
|
|
3051
|
-
label:
|
|
3052
|
-
type:
|
|
3053
|
-
options:
|
|
3054
|
-
default:
|
|
3253
|
+
tags: import_zod15.z.array(import_zod15.z.string()).optional(),
|
|
3254
|
+
vars: import_zod15.z.array(
|
|
3255
|
+
import_zod15.z.object({
|
|
3256
|
+
key: import_zod15.z.string(),
|
|
3257
|
+
label: import_zod15.z.string().optional(),
|
|
3258
|
+
type: import_zod15.z.enum(["string", "number", "boolean", "enum"]),
|
|
3259
|
+
options: import_zod15.z.array(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional(),
|
|
3260
|
+
default: import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()]).optional()
|
|
3055
3261
|
})
|
|
3056
3262
|
).optional().describe(
|
|
3057
3263
|
"Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."
|
|
3058
3264
|
),
|
|
3059
|
-
steps:
|
|
3060
|
-
|
|
3061
|
-
id:
|
|
3062
|
-
tool:
|
|
3265
|
+
steps: import_zod15.z.array(
|
|
3266
|
+
import_zod15.z.object({
|
|
3267
|
+
id: import_zod15.z.string().optional(),
|
|
3268
|
+
tool: import_zod15.z.string().describe(
|
|
3063
3269
|
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3064
3270
|
),
|
|
3065
|
-
action:
|
|
3271
|
+
action: import_zod15.z.string().describe(
|
|
3066
3272
|
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3067
3273
|
),
|
|
3068
|
-
params:
|
|
3069
|
-
label:
|
|
3274
|
+
params: import_zod15.z.unknown().optional(),
|
|
3275
|
+
label: import_zod15.z.string().optional()
|
|
3070
3276
|
})
|
|
3071
3277
|
).describe("Steps, in order."),
|
|
3072
|
-
undoSteps:
|
|
3073
|
-
|
|
3074
|
-
id:
|
|
3075
|
-
tool:
|
|
3076
|
-
action:
|
|
3077
|
-
params:
|
|
3078
|
-
label:
|
|
3278
|
+
undoSteps: import_zod15.z.array(
|
|
3279
|
+
import_zod15.z.object({
|
|
3280
|
+
id: import_zod15.z.string().optional(),
|
|
3281
|
+
tool: import_zod15.z.string(),
|
|
3282
|
+
action: import_zod15.z.string(),
|
|
3283
|
+
params: import_zod15.z.unknown().optional(),
|
|
3284
|
+
label: import_zod15.z.string().optional()
|
|
3079
3285
|
})
|
|
3080
3286
|
).optional().describe(
|
|
3081
3287
|
"How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored."
|
|
3082
3288
|
)
|
|
3083
3289
|
}).optional().describe("The scenario to save (action 'save')."),
|
|
3084
|
-
deviceId:
|
|
3290
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3085
3291
|
}
|
|
3086
3292
|
},
|
|
3087
3293
|
async (args) => {
|
|
@@ -3090,7 +3296,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3090
3296
|
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3091
3297
|
switch (args.action) {
|
|
3092
3298
|
case "preview": {
|
|
3093
|
-
if (!args.id) return
|
|
3299
|
+
if (!args.id) return fail4("preview requires `id` (from get_scenarios).");
|
|
3094
3300
|
const r = await call("preview", {
|
|
3095
3301
|
id: args.id,
|
|
3096
3302
|
params: args.params ?? {}
|
|
@@ -3113,16 +3319,16 @@ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
|
3113
3319
|
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3114
3320
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3115
3321
|
);
|
|
3116
|
-
return
|
|
3322
|
+
return text3(lines.join("\n\n"));
|
|
3117
3323
|
}
|
|
3118
3324
|
case "run": {
|
|
3119
|
-
if (!args.id) return
|
|
3325
|
+
if (!args.id) return fail4("run requires `id` (from get_scenarios).");
|
|
3120
3326
|
const r = await call("run", {
|
|
3121
3327
|
id: args.id,
|
|
3122
3328
|
params: args.params ?? {}
|
|
3123
3329
|
});
|
|
3124
3330
|
if (r.preflightErrors?.length) {
|
|
3125
|
-
return
|
|
3331
|
+
return fail4(
|
|
3126
3332
|
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3127
3333
|
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3128
3334
|
);
|
|
@@ -3134,14 +3340,14 @@ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
|
3134
3340
|
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3135
3341
|
}).join("\n");
|
|
3136
3342
|
if (!r.ok) {
|
|
3137
|
-
return
|
|
3343
|
+
return fail4(
|
|
3138
3344
|
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3139
3345
|
${steps}
|
|
3140
3346
|
|
|
3141
3347
|
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3142
3348
|
);
|
|
3143
3349
|
}
|
|
3144
|
-
return
|
|
3350
|
+
return text3(
|
|
3145
3351
|
[
|
|
3146
3352
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3147
3353
|
steps,
|
|
@@ -3165,49 +3371,49 @@ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
|
3165
3371
|
const errs = rep.errors.length ? `
|
|
3166
3372
|
|
|
3167
3373
|
Errors: ${rep.errors.join("; ")}` : "";
|
|
3168
|
-
return
|
|
3374
|
+
return text3(`**Deactivated.**
|
|
3169
3375
|
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3170
3376
|
}
|
|
3171
3377
|
case "save": {
|
|
3172
|
-
if (!args.scenario) return
|
|
3378
|
+
if (!args.scenario) return fail4("save requires `scenario`.");
|
|
3173
3379
|
const r = await call("save", { scenario: args.scenario });
|
|
3174
3380
|
if (!r.ok) {
|
|
3175
|
-
return
|
|
3381
|
+
return fail4(
|
|
3176
3382
|
`Save rejected:
|
|
3177
3383
|
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3178
3384
|
);
|
|
3179
3385
|
}
|
|
3180
|
-
return
|
|
3386
|
+
return text3(
|
|
3181
3387
|
`Saved \`${r.id}\` **as a draft**.
|
|
3182
3388
|
|
|
3183
3389
|
It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`
|
|
3184
3390
|
);
|
|
3185
3391
|
}
|
|
3186
3392
|
case "setFolder": {
|
|
3187
|
-
if (!args.id) return
|
|
3393
|
+
if (!args.id) return fail4("setFolder requires `id` (from get_scenarios).");
|
|
3188
3394
|
const r = await call("setFolder", {
|
|
3189
3395
|
id: args.id,
|
|
3190
3396
|
folder: args.folder
|
|
3191
3397
|
});
|
|
3192
|
-
return
|
|
3398
|
+
return text3(
|
|
3193
3399
|
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3194
3400
|
);
|
|
3195
3401
|
}
|
|
3196
3402
|
case "delete": {
|
|
3197
|
-
if (!args.id) return
|
|
3403
|
+
if (!args.id) return fail4("delete requires `id`.");
|
|
3198
3404
|
await call("delete", { id: args.id });
|
|
3199
|
-
return
|
|
3405
|
+
return text3(`Deleted \`${args.id}\` from the device library.`);
|
|
3200
3406
|
}
|
|
3201
3407
|
}
|
|
3202
3408
|
} catch (e) {
|
|
3203
|
-
return
|
|
3409
|
+
return fail4(errMsg4(e));
|
|
3204
3410
|
}
|
|
3205
3411
|
}
|
|
3206
3412
|
);
|
|
3207
3413
|
}
|
|
3208
3414
|
|
|
3209
3415
|
// src/tools/packs/screenshot.ts
|
|
3210
|
-
var
|
|
3416
|
+
var import_zod16 = require("zod");
|
|
3211
3417
|
|
|
3212
3418
|
// src/capture/simctl.ts
|
|
3213
3419
|
var import_node_child_process = require("child_process");
|
|
@@ -3321,8 +3527,8 @@ function frameToRect(frame, screen) {
|
|
|
3321
3527
|
var TOOL_ID = "highlight-updates";
|
|
3322
3528
|
var MARGIN_PT = 12;
|
|
3323
3529
|
var CAPTURE_PADDING_PT = 6;
|
|
3324
|
-
var
|
|
3325
|
-
var
|
|
3530
|
+
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3531
|
+
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
3326
3532
|
function reasonMessage(res, query) {
|
|
3327
3533
|
switch (res.reason) {
|
|
3328
3534
|
case "no-renders":
|
|
@@ -3352,10 +3558,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
3352
3558
|
{
|
|
3353
3559
|
description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. If highlight tracking hasn't seen a render yet, it falls back to a live fiber walk (same as describe_screen). iOS Simulator only, and the MCP server must run on the same Mac as the booted Simulator. Requires Buoy Pro.",
|
|
3354
3560
|
inputSchema: {
|
|
3355
|
-
query:
|
|
3356
|
-
nativeTag:
|
|
3357
|
-
scrollIntoView:
|
|
3358
|
-
deviceId:
|
|
3561
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3562
|
+
nativeTag: import_zod16.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3563
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
3564
|
+
deviceId: deviceIdInput
|
|
3359
3565
|
}
|
|
3360
3566
|
},
|
|
3361
3567
|
async (args) => {
|
|
@@ -3397,11 +3603,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3397
3603
|
}
|
|
3398
3604
|
}
|
|
3399
3605
|
if (!located.matched || !located.rect || !located.screen) {
|
|
3400
|
-
return
|
|
3606
|
+
return fail5(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
3401
3607
|
}
|
|
3402
3608
|
const shot = await captureIosFramebuffer();
|
|
3403
3609
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
3404
|
-
return
|
|
3610
|
+
return fail5(shot.error ?? "Screenshot capture failed.");
|
|
3405
3611
|
}
|
|
3406
3612
|
const pxScale = shot.width / located.screen.width;
|
|
3407
3613
|
const r = located.rect;
|
|
@@ -3417,7 +3623,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
3417
3623
|
}
|
|
3418
3624
|
);
|
|
3419
3625
|
if (!crop.ok || !crop.buffer) {
|
|
3420
|
-
return
|
|
3626
|
+
return fail5(crop.error ?? "Crop failed.");
|
|
3421
3627
|
}
|
|
3422
3628
|
const label = located.componentName || "component";
|
|
3423
3629
|
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
@@ -3430,14 +3636,14 @@ function registerScreenshotTools(server, sync) {
|
|
|
3430
3636
|
]
|
|
3431
3637
|
};
|
|
3432
3638
|
} catch (e) {
|
|
3433
|
-
return
|
|
3639
|
+
return fail5(errMsg5(e));
|
|
3434
3640
|
}
|
|
3435
3641
|
}
|
|
3436
3642
|
);
|
|
3437
3643
|
}
|
|
3438
3644
|
|
|
3439
3645
|
// src/tools/packs/camera.ts
|
|
3440
|
-
var
|
|
3646
|
+
var import_zod17 = require("zod");
|
|
3441
3647
|
|
|
3442
3648
|
// src/capture/camera.ts
|
|
3443
3649
|
var import_node_child_process2 = require("child_process");
|
|
@@ -3593,9 +3799,9 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
|
|
|
3593
3799
|
}
|
|
3594
3800
|
|
|
3595
3801
|
// src/tools/packs/camera.ts
|
|
3596
|
-
var
|
|
3597
|
-
var gate = (tool) => readEntitlement().isPro ? null :
|
|
3598
|
-
var
|
|
3802
|
+
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3803
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail6(proRequired(tool));
|
|
3804
|
+
var fail6 = (message) => ({
|
|
3599
3805
|
isError: true,
|
|
3600
3806
|
content: [{ type: "text", text: message }]
|
|
3601
3807
|
});
|
|
@@ -3605,11 +3811,11 @@ function registerCameraTools(server) {
|
|
|
3605
3811
|
{
|
|
3606
3812
|
description: "Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",
|
|
3607
3813
|
inputSchema: {
|
|
3608
|
-
kind:
|
|
3609
|
-
value:
|
|
3814
|
+
kind: import_zod17.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3815
|
+
value: import_zod17.z.string().optional().describe(
|
|
3610
3816
|
"Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."
|
|
3611
3817
|
),
|
|
3612
|
-
format:
|
|
3818
|
+
format: import_zod17.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3613
3819
|
}
|
|
3614
3820
|
},
|
|
3615
3821
|
async (args) => {
|
|
@@ -3620,12 +3826,12 @@ function registerCameraTools(server) {
|
|
|
3620
3826
|
if (args.format) cmd.push("--format", args.format);
|
|
3621
3827
|
const result = await run(cmd);
|
|
3622
3828
|
if (!result.ok) {
|
|
3623
|
-
return
|
|
3829
|
+
return fail6(
|
|
3624
3830
|
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3625
3831
|
);
|
|
3626
3832
|
}
|
|
3627
3833
|
const what = args.kind === "barcode" ? `a ${args.format ?? "qr"} code reading "${args.value}"` : args.kind === "screen" ? `this Mac's screen (${args.value ?? "behind the simulator window"})` : args.value ?? args.kind;
|
|
3628
|
-
return
|
|
3834
|
+
return text4(
|
|
3629
3835
|
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3630
3836
|
|
|
3631
3837
|
_Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`
|
|
@@ -3643,15 +3849,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3643
3849
|
if (blocked) return blocked;
|
|
3644
3850
|
const result = await run(["status"]);
|
|
3645
3851
|
if (!result.ok || !result.data) {
|
|
3646
|
-
return
|
|
3852
|
+
return fail6(result.error ?? "could not read the simulator camera status");
|
|
3647
3853
|
}
|
|
3648
3854
|
const { publishing, fps, codes } = result.data;
|
|
3649
3855
|
if (!publishing) {
|
|
3650
|
-
return
|
|
3856
|
+
return text4(
|
|
3651
3857
|
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3652
3858
|
);
|
|
3653
3859
|
}
|
|
3654
|
-
return
|
|
3860
|
+
return text4(
|
|
3655
3861
|
[
|
|
3656
3862
|
`Publishing at ${fps} fps.`,
|
|
3657
3863
|
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
@@ -3666,8 +3872,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3666
3872
|
{
|
|
3667
3873
|
description: "Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",
|
|
3668
3874
|
inputSchema: {
|
|
3669
|
-
bundleId:
|
|
3670
|
-
udid:
|
|
3875
|
+
bundleId: import_zod17.z.string().describe("The app's bundle identifier."),
|
|
3876
|
+
udid: import_zod17.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3671
3877
|
}
|
|
3672
3878
|
},
|
|
3673
3879
|
async (args) => {
|
|
@@ -3677,9 +3883,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3677
3883
|
if (args.udid) cmd.push("--udid", args.udid);
|
|
3678
3884
|
const result = await run(cmd);
|
|
3679
3885
|
if (!result.ok) {
|
|
3680
|
-
return
|
|
3886
|
+
return fail6(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3681
3887
|
}
|
|
3682
|
-
return
|
|
3888
|
+
return text4(
|
|
3683
3889
|
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3684
3890
|
);
|
|
3685
3891
|
}
|
|
@@ -3689,7 +3895,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3689
3895
|
{
|
|
3690
3896
|
description: "Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",
|
|
3691
3897
|
inputSchema: {
|
|
3692
|
-
bundleId:
|
|
3898
|
+
bundleId: import_zod17.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3693
3899
|
}
|
|
3694
3900
|
},
|
|
3695
3901
|
async (args) => {
|
|
@@ -3697,11 +3903,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3697
3903
|
if (args.bundleId) cmd.push(args.bundleId);
|
|
3698
3904
|
const result = await run(cmd);
|
|
3699
3905
|
if (!result.data?.checks) {
|
|
3700
|
-
return
|
|
3906
|
+
return fail6(result.error ?? "could not run the simulator camera diagnosis");
|
|
3701
3907
|
}
|
|
3702
3908
|
const ent = readEntitlement();
|
|
3703
3909
|
const tierNote = ent.isPro ? "" : "\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";
|
|
3704
|
-
return
|
|
3910
|
+
return text4(describeDiagnosis(result.data.checks) + tierNote);
|
|
3705
3911
|
}
|
|
3706
3912
|
);
|
|
3707
3913
|
server.registerTool(
|
|
@@ -3715,9 +3921,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3715
3921
|
if (blocked) return blocked;
|
|
3716
3922
|
const result = await run(["stop"]);
|
|
3717
3923
|
if (!result.ok) {
|
|
3718
|
-
return
|
|
3924
|
+
return fail6(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3719
3925
|
}
|
|
3720
|
-
return
|
|
3926
|
+
return text4("The simulator camera is off.");
|
|
3721
3927
|
}
|
|
3722
3928
|
);
|
|
3723
3929
|
server.registerTool(
|
|
@@ -3725,7 +3931,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3725
3931
|
{
|
|
3726
3932
|
description: "List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",
|
|
3727
3933
|
inputSchema: {
|
|
3728
|
-
udid:
|
|
3934
|
+
udid: import_zod17.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3729
3935
|
}
|
|
3730
3936
|
},
|
|
3731
3937
|
async (args) => {
|
|
@@ -3733,7 +3939,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3733
3939
|
if (blocked) return blocked;
|
|
3734
3940
|
const devicesResult = await run(["devices"]);
|
|
3735
3941
|
if (!devicesResult.ok || !devicesResult.data) {
|
|
3736
|
-
return
|
|
3942
|
+
return fail6(devicesResult.error ?? "could not list booted simulators");
|
|
3737
3943
|
}
|
|
3738
3944
|
const devices = devicesResult.data.devices ?? [];
|
|
3739
3945
|
let apps = null;
|
|
@@ -3744,7 +3950,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3744
3950
|
]);
|
|
3745
3951
|
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3746
3952
|
}
|
|
3747
|
-
return
|
|
3953
|
+
return text4(describeDevices(devices, apps));
|
|
3748
3954
|
}
|
|
3749
3955
|
);
|
|
3750
3956
|
server.registerTool(
|
|
@@ -3761,11 +3967,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3761
3967
|
run(["windows"])
|
|
3762
3968
|
]);
|
|
3763
3969
|
if (!cams.ok && !wins.ok) {
|
|
3764
|
-
return
|
|
3970
|
+
return fail6(
|
|
3765
3971
|
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3766
3972
|
);
|
|
3767
3973
|
}
|
|
3768
|
-
return
|
|
3974
|
+
return text4(
|
|
3769
3975
|
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3770
3976
|
);
|
|
3771
3977
|
}
|
|
@@ -3775,8 +3981,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3775
3981
|
{
|
|
3776
3982
|
description: "Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",
|
|
3777
3983
|
inputSchema: {
|
|
3778
|
-
action:
|
|
3779
|
-
udid:
|
|
3984
|
+
action: import_zod17.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3985
|
+
udid: import_zod17.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3780
3986
|
}
|
|
3781
3987
|
},
|
|
3782
3988
|
async (args) => {
|
|
@@ -3784,36 +3990,36 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3784
3990
|
if (blocked) return blocked;
|
|
3785
3991
|
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3786
3992
|
if (!result.ok) {
|
|
3787
|
-
return
|
|
3993
|
+
return fail6(
|
|
3788
3994
|
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3789
3995
|
);
|
|
3790
3996
|
}
|
|
3791
3997
|
const d = result.data ?? {};
|
|
3792
3998
|
if (args.action === "status") {
|
|
3793
|
-
if (!d.enabled) return
|
|
3794
|
-
return
|
|
3999
|
+
if (!d.enabled) return text4("Zero-setup mode is **off**.");
|
|
4000
|
+
return text4(
|
|
3795
4001
|
d.foreign ? `Zero-setup is on, but **another tool owns it** (\`${d.path}\`). Quit that tool before turning Buoy's on.` : "Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera."
|
|
3796
4002
|
);
|
|
3797
4003
|
}
|
|
3798
4004
|
if (args.action === "on") {
|
|
3799
|
-
return
|
|
4005
|
+
return text4(
|
|
3800
4006
|
"Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._"
|
|
3801
4007
|
);
|
|
3802
4008
|
}
|
|
3803
|
-
return
|
|
4009
|
+
return text4("Zero-setup mode is **off**.");
|
|
3804
4010
|
}
|
|
3805
4011
|
);
|
|
3806
4012
|
}
|
|
3807
4013
|
|
|
3808
4014
|
// src/tools/packs/interact.ts
|
|
3809
|
-
var
|
|
4015
|
+
var import_zod18 = require("zod");
|
|
3810
4016
|
var TOOL_ID2 = "highlight-updates";
|
|
3811
|
-
var
|
|
3812
|
-
var
|
|
4017
|
+
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4018
|
+
var fail7 = (message) => ({
|
|
3813
4019
|
isError: true,
|
|
3814
4020
|
content: [{ type: "text", text: message }]
|
|
3815
4021
|
});
|
|
3816
|
-
var
|
|
4022
|
+
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
3817
4023
|
function formatElement(e) {
|
|
3818
4024
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3819
4025
|
line += e.name;
|
|
@@ -3836,8 +4042,8 @@ function registerInteractTools(server, sync) {
|
|
|
3836
4042
|
{
|
|
3837
4043
|
description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. If you already know the sequence you want to drive, `run_flow` does the whole thing (taps, waits, assertions) in one call instead. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
|
|
3838
4044
|
inputSchema: {
|
|
3839
|
-
interactiveOnly:
|
|
3840
|
-
deviceId:
|
|
4045
|
+
interactiveOnly: import_zod18.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4046
|
+
deviceId: deviceIdInput
|
|
3841
4047
|
}
|
|
3842
4048
|
},
|
|
3843
4049
|
async (args) => {
|
|
@@ -3847,16 +4053,16 @@ function registerInteractTools(server, sync) {
|
|
|
3847
4053
|
const all = res?.elements ?? [];
|
|
3848
4054
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3849
4055
|
if (elements.length === 0) {
|
|
3850
|
-
return
|
|
4056
|
+
return text5(
|
|
3851
4057
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3852
4058
|
);
|
|
3853
4059
|
}
|
|
3854
4060
|
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 }).`;
|
|
3855
|
-
return
|
|
4061
|
+
return text5(`${header}
|
|
3856
4062
|
|
|
3857
4063
|
${elements.map(formatElement).join("\n")}`);
|
|
3858
4064
|
} catch (e) {
|
|
3859
|
-
return
|
|
4065
|
+
return fail7(errMsg6(e));
|
|
3860
4066
|
}
|
|
3861
4067
|
}
|
|
3862
4068
|
);
|
|
@@ -3865,20 +4071,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3865
4071
|
{
|
|
3866
4072
|
description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Driving MORE THAN ONE step (reproducing a bug, verifying a fix)? Use `run_flow` instead \u2014 it runs the whole sequence in one call. Requires Buoy Pro.",
|
|
3867
4073
|
inputSchema: {
|
|
3868
|
-
nativeTag:
|
|
3869
|
-
testID:
|
|
3870
|
-
query:
|
|
3871
|
-
value:
|
|
3872
|
-
text:
|
|
3873
|
-
longPress:
|
|
3874
|
-
scrollIntoView:
|
|
3875
|
-
deviceId:
|
|
4074
|
+
nativeTag: import_zod18.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4075
|
+
testID: import_zod18.z.string().optional().describe("Exact testID of the element."),
|
|
4076
|
+
query: import_zod18.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
4077
|
+
value: import_zod18.z.union([import_zod18.z.number(), import_zod18.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)."),
|
|
4078
|
+
text: import_zod18.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4079
|
+
longPress: import_zod18.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
4080
|
+
scrollIntoView: import_zod18.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
4081
|
+
deviceId: deviceIdInput
|
|
3876
4082
|
}
|
|
3877
4083
|
},
|
|
3878
4084
|
async (args) => {
|
|
3879
4085
|
try {
|
|
3880
4086
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3881
|
-
return
|
|
4087
|
+
return fail7("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3882
4088
|
}
|
|
3883
4089
|
const target = sync.requireProDevice(args.deviceId);
|
|
3884
4090
|
const res = await sync.callAction(
|
|
@@ -3903,26 +4109,23 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3903
4109
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3904
4110
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3905
4111
|
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}`;
|
|
3906
|
-
return
|
|
4112
|
+
return text5(`${verb}${scrolledNote}.${flowTip}`);
|
|
3907
4113
|
}
|
|
3908
4114
|
let msg = res?.reason ?? "Tap failed.";
|
|
3909
4115
|
if (res?.candidates?.length) {
|
|
3910
4116
|
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");
|
|
3911
4117
|
}
|
|
3912
|
-
return
|
|
4118
|
+
return fail7(msg);
|
|
3913
4119
|
} catch (e) {
|
|
3914
|
-
return
|
|
4120
|
+
return fail7(errMsg6(e));
|
|
3915
4121
|
}
|
|
3916
4122
|
}
|
|
3917
4123
|
);
|
|
3918
4124
|
}
|
|
3919
4125
|
|
|
3920
4126
|
// src/tools/packs/jsThread.ts
|
|
3921
|
-
var
|
|
4127
|
+
var import_zod19 = require("zod");
|
|
3922
4128
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3923
|
-
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3924
|
-
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3925
|
-
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3926
4129
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3927
4130
|
function sparkline(values) {
|
|
3928
4131
|
const max = Math.max(1, ...values);
|
|
@@ -3993,9 +4196,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3993
4196
|
{
|
|
3994
4197
|
description: 'Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. NOT the tool for React components re-rendering too often \u2014 this measures TASKS on the thread, not renders; for "the page re-renders constantly / typing lags / tapping one button re-renders everything" use measure_renders. Requires @buoy-gg/js-top on the device.',
|
|
3995
4198
|
inputSchema: {
|
|
3996
|
-
deviceId:
|
|
3997
|
-
durationMs:
|
|
3998
|
-
clearFirst:
|
|
4199
|
+
deviceId: deviceIdInput,
|
|
4200
|
+
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4201
|
+
clearFirst: import_zod19.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3999
4202
|
}
|
|
4000
4203
|
},
|
|
4001
4204
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -4008,7 +4211,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
4008
4211
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
4009
4212
|
target
|
|
4010
4213
|
);
|
|
4011
|
-
if (!snap) return
|
|
4214
|
+
if (!snap) return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
4012
4215
|
const md = formatTopMarkdown(snap, duration);
|
|
4013
4216
|
const slim = {
|
|
4014
4217
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -4027,13 +4230,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
4027
4230
|
worstFreezeMs: r.worstFreezeMs
|
|
4028
4231
|
}))
|
|
4029
4232
|
};
|
|
4030
|
-
return
|
|
4233
|
+
return text(`${md}
|
|
4031
4234
|
|
|
4032
4235
|
\`\`\`json
|
|
4033
4236
|
${JSON.stringify(slim)}
|
|
4034
4237
|
\`\`\``);
|
|
4035
4238
|
} catch (e) {
|
|
4036
|
-
return
|
|
4239
|
+
return fail2(errMsg2(e));
|
|
4037
4240
|
}
|
|
4038
4241
|
}
|
|
4039
4242
|
);
|
|
@@ -4042,8 +4245,8 @@ ${JSON.stringify(slim)}
|
|
|
4042
4245
|
{
|
|
4043
4246
|
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).',
|
|
4044
4247
|
inputSchema: {
|
|
4045
|
-
deviceId:
|
|
4046
|
-
key:
|
|
4248
|
+
deviceId: deviceIdInput,
|
|
4249
|
+
key: import_zod19.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
4047
4250
|
}
|
|
4048
4251
|
},
|
|
4049
4252
|
async ({ deviceId, key }) => {
|
|
@@ -4056,18 +4259,18 @@ ${JSON.stringify(slim)}
|
|
|
4056
4259
|
target
|
|
4057
4260
|
);
|
|
4058
4261
|
if (!detail) {
|
|
4059
|
-
return
|
|
4262
|
+
return fail2(
|
|
4060
4263
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
4061
4264
|
);
|
|
4062
4265
|
}
|
|
4063
4266
|
const md = formatDetailMarkdown(detail);
|
|
4064
|
-
return
|
|
4267
|
+
return text(`${md}
|
|
4065
4268
|
|
|
4066
4269
|
\`\`\`json
|
|
4067
4270
|
${JSON.stringify(detail)}
|
|
4068
4271
|
\`\`\``);
|
|
4069
4272
|
} catch (e) {
|
|
4070
|
-
return
|
|
4273
|
+
return fail2(errMsg2(e));
|
|
4071
4274
|
}
|
|
4072
4275
|
}
|
|
4073
4276
|
);
|
|
@@ -4076,27 +4279,24 @@ ${JSON.stringify(detail)}
|
|
|
4076
4279
|
{
|
|
4077
4280
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
4078
4281
|
inputSchema: {
|
|
4079
|
-
deviceId:
|
|
4282
|
+
deviceId: deviceIdInput
|
|
4080
4283
|
}
|
|
4081
4284
|
},
|
|
4082
4285
|
async ({ deviceId }) => {
|
|
4083
4286
|
try {
|
|
4084
4287
|
const target = sync.requireProDevice(deviceId);
|
|
4085
4288
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
4086
|
-
return
|
|
4289
|
+
return text("Cleared js-top stats.");
|
|
4087
4290
|
} catch (e) {
|
|
4088
|
-
return
|
|
4291
|
+
return fail2(errMsg2(e));
|
|
4089
4292
|
}
|
|
4090
4293
|
}
|
|
4091
4294
|
);
|
|
4092
4295
|
}
|
|
4093
4296
|
|
|
4094
4297
|
// src/tools/packs/images.ts
|
|
4095
|
-
var
|
|
4298
|
+
var import_zod20 = require("zod");
|
|
4096
4299
|
var IMAGES_TOOL_ID = "images";
|
|
4097
|
-
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4098
|
-
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4099
|
-
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
4100
4300
|
function uriTail(uri) {
|
|
4101
4301
|
if (!uri) return "(no source)";
|
|
4102
4302
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -4116,8 +4316,8 @@ function cacheLabel(r) {
|
|
|
4116
4316
|
if (r.progressSeen) return "network";
|
|
4117
4317
|
return "?";
|
|
4118
4318
|
}
|
|
4119
|
-
function mb(
|
|
4120
|
-
return `${(
|
|
4319
|
+
function mb(bytes2) {
|
|
4320
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`;
|
|
4121
4321
|
}
|
|
4122
4322
|
function formatListMarkdown(list, limit) {
|
|
4123
4323
|
const lines = [];
|
|
@@ -4184,9 +4384,9 @@ function registerImagesTools(server, sync) {
|
|
|
4184
4384
|
{
|
|
4185
4385
|
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.",
|
|
4186
4386
|
inputSchema: {
|
|
4187
|
-
deviceId:
|
|
4188
|
-
limit:
|
|
4189
|
-
status:
|
|
4387
|
+
deviceId: deviceIdInput,
|
|
4388
|
+
limit: import_zod20.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4389
|
+
status: import_zod20.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
4190
4390
|
}
|
|
4191
4391
|
},
|
|
4192
4392
|
async ({ deviceId, limit, status }) => {
|
|
@@ -4199,7 +4399,7 @@ function registerImagesTools(server, sync) {
|
|
|
4199
4399
|
{ limit: capped, status },
|
|
4200
4400
|
target
|
|
4201
4401
|
);
|
|
4202
|
-
if (!list) return
|
|
4402
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
4203
4403
|
const md = formatListMarkdown(list, capped);
|
|
4204
4404
|
const slim = {
|
|
4205
4405
|
stats: list.stats,
|
|
@@ -4226,13 +4426,13 @@ function registerImagesTools(server, sync) {
|
|
|
4226
4426
|
layoutShifts: r.layoutShifts
|
|
4227
4427
|
}))
|
|
4228
4428
|
};
|
|
4229
|
-
return
|
|
4429
|
+
return text(`${md}
|
|
4230
4430
|
|
|
4231
4431
|
\`\`\`json
|
|
4232
4432
|
${JSON.stringify(slim)}
|
|
4233
4433
|
\`\`\``);
|
|
4234
4434
|
} catch (e) {
|
|
4235
|
-
return
|
|
4435
|
+
return fail2(errMsg2(e));
|
|
4236
4436
|
}
|
|
4237
4437
|
}
|
|
4238
4438
|
);
|
|
@@ -4241,9 +4441,9 @@ ${JSON.stringify(slim)}
|
|
|
4241
4441
|
{
|
|
4242
4442
|
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.",
|
|
4243
4443
|
inputSchema: {
|
|
4244
|
-
deviceId:
|
|
4245
|
-
id:
|
|
4246
|
-
action:
|
|
4444
|
+
deviceId: deviceIdInput,
|
|
4445
|
+
id: import_zod20.z.number().describe("Record id from get_images."),
|
|
4446
|
+
action: import_zod20.z.enum([
|
|
4247
4447
|
"hard_reload",
|
|
4248
4448
|
"retry",
|
|
4249
4449
|
"evict_disk",
|
|
@@ -4255,7 +4455,7 @@ ${JSON.stringify(slim)}
|
|
|
4255
4455
|
"swap_url",
|
|
4256
4456
|
"restore"
|
|
4257
4457
|
]).describe("What to do."),
|
|
4258
|
-
url:
|
|
4458
|
+
url: import_zod20.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
4259
4459
|
}
|
|
4260
4460
|
},
|
|
4261
4461
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -4263,8 +4463,8 @@ ${JSON.stringify(slim)}
|
|
|
4263
4463
|
const target = sync.requireProDevice(deviceId);
|
|
4264
4464
|
if (action === "prove_savings") {
|
|
4265
4465
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
4266
|
-
if (!savings) return
|
|
4267
|
-
if (!savings.ok) return
|
|
4466
|
+
if (!savings) return fail2("Device returned nothing for proveSavings.");
|
|
4467
|
+
if (!savings.ok) return fail2(savings.message);
|
|
4268
4468
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
4269
4469
|
if (savings.originalBytes != null)
|
|
4270
4470
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -4276,11 +4476,11 @@ ${JSON.stringify(slim)}
|
|
|
4276
4476
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
4277
4477
|
if (savings.optimizedUri)
|
|
4278
4478
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
4279
|
-
return
|
|
4479
|
+
return text(parts.join("\n"));
|
|
4280
4480
|
}
|
|
4281
4481
|
const entry = ACTION_MAP[action];
|
|
4282
|
-
if (!entry) return
|
|
4283
|
-
if (action === "swap_url" && !url) return
|
|
4482
|
+
if (!entry) return fail2(`Unknown action: ${action}`);
|
|
4483
|
+
if (action === "swap_url" && !url) return fail2("swap_url requires the `url` param.");
|
|
4284
4484
|
const result = await sync.callAction(
|
|
4285
4485
|
IMAGES_TOOL_ID,
|
|
4286
4486
|
entry.action,
|
|
@@ -4300,9 +4500,9 @@ ${JSON.stringify(slim)}
|
|
|
4300
4500
|
);
|
|
4301
4501
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
4302
4502
|
}
|
|
4303
|
-
return
|
|
4503
|
+
return text(lines.join("\n"));
|
|
4304
4504
|
} catch (e) {
|
|
4305
|
-
return
|
|
4505
|
+
return fail2(errMsg2(e));
|
|
4306
4506
|
}
|
|
4307
4507
|
}
|
|
4308
4508
|
);
|
|
@@ -4311,12 +4511,12 @@ ${JSON.stringify(slim)}
|
|
|
4311
4511
|
{
|
|
4312
4512
|
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.",
|
|
4313
4513
|
inputSchema: {
|
|
4314
|
-
deviceId:
|
|
4315
|
-
network:
|
|
4316
|
-
blankImages:
|
|
4317
|
-
massAction:
|
|
4318
|
-
clearRegistry:
|
|
4319
|
-
clearExpoCaches:
|
|
4514
|
+
deviceId: deviceIdInput,
|
|
4515
|
+
network: import_zod20.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4516
|
+
blankImages: import_zod20.z.boolean().optional().describe("Render all images without a source."),
|
|
4517
|
+
massAction: import_zod20.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."),
|
|
4518
|
+
clearRegistry: import_zod20.z.boolean().optional().describe("Clear the captured image log."),
|
|
4519
|
+
clearExpoCaches: import_zod20.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4320
4520
|
}
|
|
4321
4521
|
},
|
|
4322
4522
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -4343,17 +4543,17 @@ ${JSON.stringify(slim)}
|
|
|
4343
4543
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
4344
4544
|
done.push(r?.message ?? "expo caches cleared");
|
|
4345
4545
|
}
|
|
4346
|
-
if (done.length === 0) return
|
|
4347
|
-
return
|
|
4546
|
+
if (done.length === 0) return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4547
|
+
return text(`Applied: ${done.join(" \xB7 ")}`);
|
|
4348
4548
|
} catch (e) {
|
|
4349
|
-
return
|
|
4549
|
+
return fail2(errMsg2(e));
|
|
4350
4550
|
}
|
|
4351
4551
|
}
|
|
4352
4552
|
);
|
|
4353
4553
|
}
|
|
4354
4554
|
|
|
4355
4555
|
// src/tools/packs/assets.ts
|
|
4356
|
-
var
|
|
4556
|
+
var import_zod21 = require("zod");
|
|
4357
4557
|
|
|
4358
4558
|
// src/tools/packs/assetsGraphNote.ts
|
|
4359
4559
|
function graphUnavailableNote(ss) {
|
|
@@ -4368,14 +4568,11 @@ function graphUnavailableNote(ss) {
|
|
|
4368
4568
|
|
|
4369
4569
|
// src/tools/packs/assets.ts
|
|
4370
4570
|
var ASSETS_TOOL_ID = "assets";
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
4377
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4378
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
4571
|
+
function kb2(bytes2) {
|
|
4572
|
+
if (bytes2 == null || bytes2 <= 0) return "\u2013";
|
|
4573
|
+
if (bytes2 < 1024) return `${bytes2}B`;
|
|
4574
|
+
if (bytes2 < 1024 * 1024) return `${(bytes2 / 1024).toFixed(1)}KB`;
|
|
4575
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(2)}MB`;
|
|
4379
4576
|
}
|
|
4380
4577
|
function sizeLabel(r) {
|
|
4381
4578
|
if (r.sizeBytes == null) return "\u2013";
|
|
@@ -4458,10 +4655,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4458
4655
|
{
|
|
4459
4656
|
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
4460
4657
|
inputSchema: {
|
|
4461
|
-
deviceId:
|
|
4462
|
-
limit:
|
|
4463
|
-
kind:
|
|
4464
|
-
unusedOnly:
|
|
4658
|
+
deviceId: deviceIdInput,
|
|
4659
|
+
limit: import_zod21.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4660
|
+
kind: import_zod21.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4661
|
+
unusedOnly: import_zod21.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4465
4662
|
}
|
|
4466
4663
|
},
|
|
4467
4664
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4474,7 +4671,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4474
4671
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4475
4672
|
target
|
|
4476
4673
|
);
|
|
4477
|
-
if (!list) return
|
|
4674
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4478
4675
|
if (unusedOnly) {
|
|
4479
4676
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4480
4677
|
}
|
|
@@ -4498,13 +4695,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4498
4695
|
sizeSource: r.sizeSource
|
|
4499
4696
|
}))
|
|
4500
4697
|
};
|
|
4501
|
-
return
|
|
4698
|
+
return text(`${md}
|
|
4502
4699
|
|
|
4503
4700
|
\`\`\`json
|
|
4504
4701
|
${JSON.stringify(slim)}
|
|
4505
4702
|
\`\`\``);
|
|
4506
4703
|
} catch (e) {
|
|
4507
|
-
return
|
|
4704
|
+
return fail2(errMsg2(e));
|
|
4508
4705
|
}
|
|
4509
4706
|
}
|
|
4510
4707
|
);
|
|
@@ -4513,9 +4710,9 @@ ${JSON.stringify(slim)}
|
|
|
4513
4710
|
{
|
|
4514
4711
|
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
4515
4712
|
inputSchema: {
|
|
4516
|
-
deviceId:
|
|
4517
|
-
action:
|
|
4518
|
-
id:
|
|
4713
|
+
deviceId: deviceIdInput,
|
|
4714
|
+
action: import_zod21.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4715
|
+
id: import_zod21.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4519
4716
|
}
|
|
4520
4717
|
},
|
|
4521
4718
|
async ({ deviceId, action, id }) => {
|
|
@@ -4525,19 +4722,19 @@ ${JSON.stringify(slim)}
|
|
|
4525
4722
|
case "rescan": {
|
|
4526
4723
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4527
4724
|
const ss = result?.scanStatus;
|
|
4528
|
-
if (!ss) return
|
|
4529
|
-
return
|
|
4725
|
+
if (!ss) return fail2("Device returned nothing for rescan.");
|
|
4726
|
+
return text(
|
|
4530
4727
|
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
4531
4728
|
);
|
|
4532
4729
|
}
|
|
4533
4730
|
case "measure_sizes": {
|
|
4534
4731
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4535
|
-
return
|
|
4732
|
+
return text(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4536
4733
|
}
|
|
4537
4734
|
case "detail": {
|
|
4538
|
-
if (id == null) return
|
|
4735
|
+
if (id == null) return fail2("detail requires the `id` param (from get_assets).");
|
|
4539
4736
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4540
|
-
if (!detail) return
|
|
4737
|
+
if (!detail) return fail2(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4541
4738
|
const lines = [
|
|
4542
4739
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4543
4740
|
"",
|
|
@@ -4545,8 +4742,8 @@ ${JSON.stringify(slim)}
|
|
|
4545
4742
|
];
|
|
4546
4743
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
4547
4744
|
if (detail.sizesByScale) {
|
|
4548
|
-
for (const [scale,
|
|
4549
|
-
lines.push(`- Size @${scale}x: ${kb2(
|
|
4745
|
+
for (const [scale, bytes2] of Object.entries(detail.sizesByScale)) {
|
|
4746
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes2)}`);
|
|
4550
4747
|
}
|
|
4551
4748
|
}
|
|
4552
4749
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
@@ -4555,32 +4752,32 @@ ${JSON.stringify(slim)}
|
|
|
4555
4752
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4556
4753
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4557
4754
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4558
|
-
return
|
|
4755
|
+
return text(lines.join("\n"));
|
|
4559
4756
|
}
|
|
4560
4757
|
case "save_baseline": {
|
|
4561
4758
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4562
|
-
return
|
|
4759
|
+
return text(
|
|
4563
4760
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4564
4761
|
);
|
|
4565
4762
|
}
|
|
4566
4763
|
case "clear_baseline": {
|
|
4567
4764
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4568
|
-
return
|
|
4765
|
+
return text("Baseline cleared.");
|
|
4569
4766
|
}
|
|
4570
4767
|
case "clear_records": {
|
|
4571
4768
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4572
|
-
return
|
|
4769
|
+
return text(result?.message ?? "Inventory cleared.");
|
|
4573
4770
|
}
|
|
4574
4771
|
}
|
|
4575
4772
|
} catch (e) {
|
|
4576
|
-
return
|
|
4773
|
+
return fail2(errMsg2(e));
|
|
4577
4774
|
}
|
|
4578
4775
|
}
|
|
4579
4776
|
);
|
|
4580
4777
|
}
|
|
4581
4778
|
|
|
4582
4779
|
// src/tools/packs/reload.ts
|
|
4583
|
-
var
|
|
4780
|
+
var import_zod22 = require("zod");
|
|
4584
4781
|
|
|
4585
4782
|
// src/capture/devServerReload.ts
|
|
4586
4783
|
var import_ws = require("ws");
|
|
@@ -4643,9 +4840,6 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4643
4840
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4644
4841
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4645
4842
|
var PROBE_INTERVAL_MS = 400;
|
|
4646
|
-
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4647
|
-
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4648
|
-
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4649
4843
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4650
4844
|
async function ping(sync, deviceId, timeoutMs) {
|
|
4651
4845
|
try {
|
|
@@ -4684,12 +4878,12 @@ function registerReloadTools(server, sync) {
|
|
|
4684
4878
|
{
|
|
4685
4879
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, to re-run app startup, or to RECOVER A CRASHED APP: after a fatal error the other tools stop answering, and reloading is how you get them back (it falls through to the dev server when the app is too broken to reload itself). In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost \u2014 read anything you need from get_console/get_storage BEFORE reloading. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
4686
4880
|
inputSchema: {
|
|
4687
|
-
strategy:
|
|
4881
|
+
strategy: import_zod22.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4688
4882
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4689
4883
|
),
|
|
4690
|
-
wait:
|
|
4691
|
-
timeoutMs:
|
|
4692
|
-
deviceId:
|
|
4884
|
+
wait: import_zod22.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4885
|
+
timeoutMs: import_zod22.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4886
|
+
deviceId: import_zod22.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4693
4887
|
}
|
|
4694
4888
|
},
|
|
4695
4889
|
async (args) => {
|
|
@@ -4731,39 +4925,39 @@ function registerReloadTools(server, sync) {
|
|
|
4731
4925
|
}
|
|
4732
4926
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4733
4927
|
if (args.wait === false) {
|
|
4734
|
-
return
|
|
4928
|
+
return text(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4735
4929
|
}
|
|
4736
4930
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4737
4931
|
if (elapsed == null) {
|
|
4738
|
-
return
|
|
4932
|
+
return fail2(
|
|
4739
4933
|
`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.`
|
|
4740
4934
|
);
|
|
4741
4935
|
}
|
|
4742
|
-
return
|
|
4936
|
+
return text(
|
|
4743
4937
|
`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.`
|
|
4744
4938
|
);
|
|
4745
4939
|
} catch (e) {
|
|
4746
|
-
return
|
|
4940
|
+
return fail2(errMsg2(e));
|
|
4747
4941
|
}
|
|
4748
4942
|
}
|
|
4749
4943
|
);
|
|
4750
4944
|
}
|
|
4751
4945
|
|
|
4752
4946
|
// src/tools/packs/triage.ts
|
|
4753
|
-
var
|
|
4754
|
-
var
|
|
4755
|
-
var
|
|
4947
|
+
var import_zod23 = require("zod");
|
|
4948
|
+
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4949
|
+
var fail8 = (message) => ({
|
|
4756
4950
|
isError: true,
|
|
4757
4951
|
content: [{ type: "text", text: message }]
|
|
4758
4952
|
});
|
|
4759
|
-
var
|
|
4953
|
+
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
4760
4954
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4761
4955
|
var MAX_STORAGE_KEYS = 20;
|
|
4762
4956
|
var MAX_STORAGE_VALUE = 400;
|
|
4763
4957
|
var STORAGE_VALUE_BUDGET = 1600;
|
|
4764
4958
|
var MAX_LINE = 160;
|
|
4765
4959
|
var cap = (s, n = MAX_LINE) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
4766
|
-
function
|
|
4960
|
+
function hhmmss3(ts) {
|
|
4767
4961
|
if (!ts) return "--:--:--";
|
|
4768
4962
|
const d = new Date(ts);
|
|
4769
4963
|
const p = (n) => String(n).padStart(2, "0");
|
|
@@ -4793,7 +4987,7 @@ async function section(title, run2) {
|
|
|
4793
4987
|
return body ? `${title}
|
|
4794
4988
|
${body}` : null;
|
|
4795
4989
|
} catch (e) {
|
|
4796
|
-
const msg =
|
|
4990
|
+
const msg = errMsg7(e);
|
|
4797
4991
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4798
4992
|
return `${title}
|
|
4799
4993
|
(no answer \u2014 see the note above)`;
|
|
@@ -4808,7 +5002,7 @@ function registerTriageTools(server, sync) {
|
|
|
4808
5002
|
{
|
|
4809
5003
|
description: "START HERE when debugging a running app. One call = the app's current runtime state: connected device, recent console errors (including [FATAL] crashes), recent network requests with statuses, persisted storage values, and what's on screen right now. Use it as the first step for ANY 'why is the app doing X' question \u2014 it is far cheaper than calling get_console + get_network_requests + get_storage + describe_screen separately, and it tells you which of those to drill into next. If the app has crashed it says so and shows the crash. Requires Buoy Pro.",
|
|
4810
5004
|
inputSchema: {
|
|
4811
|
-
deviceId:
|
|
5005
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4812
5006
|
}
|
|
4813
5007
|
},
|
|
4814
5008
|
async (args) => {
|
|
@@ -4832,7 +5026,7 @@ function registerTriageTools(server, sync) {
|
|
|
4832
5026
|
return `(none \u2014 ${entries.length} entries captured)`;
|
|
4833
5027
|
}
|
|
4834
5028
|
return shown.map(
|
|
4835
|
-
(e) => `${
|
|
5029
|
+
(e) => `${hhmmss3(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
|
|
4836
5030
|
(e.message ?? "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).join(" \u23CE "),
|
|
4837
5031
|
220
|
|
4838
5032
|
)}`
|
|
@@ -4915,7 +5109,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4915
5109
|
|
|
4916
5110
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4917
5111
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4918
|
-
return
|
|
5112
|
+
return text6(
|
|
4919
5113
|
`${head}${banner}
|
|
4920
5114
|
|
|
4921
5115
|
${body}
|
|
@@ -4923,22 +5117,22 @@ ${body}
|
|
|
4923
5117
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4924
5118
|
);
|
|
4925
5119
|
} catch (e) {
|
|
4926
|
-
return
|
|
5120
|
+
return fail8(errMsg7(e));
|
|
4927
5121
|
}
|
|
4928
5122
|
}
|
|
4929
5123
|
);
|
|
4930
5124
|
}
|
|
4931
5125
|
|
|
4932
5126
|
// src/tools/packs/flow.ts
|
|
4933
|
-
var
|
|
5127
|
+
var import_zod24 = require("zod");
|
|
4934
5128
|
var UI_TOOL_ID = "highlight-updates";
|
|
4935
5129
|
var APP_TOOL_ID2 = "app";
|
|
4936
|
-
var
|
|
4937
|
-
var
|
|
5130
|
+
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5131
|
+
var fail9 = (message) => ({
|
|
4938
5132
|
isError: true,
|
|
4939
5133
|
content: [{ type: "text", text: message }]
|
|
4940
5134
|
});
|
|
4941
|
-
var
|
|
5135
|
+
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
4942
5136
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4943
5137
|
var POLL_MS = 250;
|
|
4944
5138
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4955,19 +5149,19 @@ function formatElement2(e) {
|
|
|
4955
5149
|
const display = e.text ?? e.label;
|
|
4956
5150
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4957
5151
|
}
|
|
4958
|
-
var stepSchema =
|
|
4959
|
-
tap:
|
|
4960
|
-
longPress:
|
|
4961
|
-
type:
|
|
4962
|
-
into:
|
|
4963
|
-
set:
|
|
4964
|
-
reload:
|
|
4965
|
-
waitMs:
|
|
4966
|
-
waitFor:
|
|
4967
|
-
waitGone:
|
|
4968
|
-
expect:
|
|
4969
|
-
expectGone:
|
|
4970
|
-
timeoutMs:
|
|
5152
|
+
var stepSchema = import_zod24.z.object({
|
|
5153
|
+
tap: import_zod24.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5154
|
+
longPress: import_zod24.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
5155
|
+
type: import_zod24.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5156
|
+
into: import_zod24.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5157
|
+
set: import_zod24.z.union([import_zod24.z.number(), import_zod24.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
5158
|
+
reload: import_zod24.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5159
|
+
waitMs: import_zod24.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5160
|
+
waitFor: import_zod24.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5161
|
+
waitGone: import_zod24.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5162
|
+
expect: import_zod24.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
5163
|
+
expectGone: import_zod24.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
5164
|
+
timeoutMs: import_zod24.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4971
5165
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4972
5166
|
function registerFlowTools(server, sync) {
|
|
4973
5167
|
server.registerTool(
|
|
@@ -4975,10 +5169,10 @@ function registerFlowTools(server, sync) {
|
|
|
4975
5169
|
{
|
|
4976
5170
|
description: "Run a scripted sequence of UI interactions in ONE call and report what happened: per-step results, the final screen, plus the network requests and console errors the flow produced. Use this instead of alternating describe_screen/tap_element \u2014 reproducing a bug ('open cart, checkout, submit') or verifying a fix after an edit is one call, not a dozen. Steps run in order and stop at the first failure, returning the screen at that point so you can see WHERE it broke. Each step can tap, type, set a toggle/slider, reload, wait for an element, and assert. Requires Buoy Pro.",
|
|
4977
5171
|
inputSchema: {
|
|
4978
|
-
steps:
|
|
4979
|
-
include:
|
|
4980
|
-
continueOnFailure:
|
|
4981
|
-
deviceId:
|
|
5172
|
+
steps: import_zod24.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5173
|
+
include: import_zod24.z.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final screen + network/console produced by the flow; 'screen' = final screen only; 'none' = step results only."),
|
|
5174
|
+
continueOnFailure: import_zod24.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5175
|
+
deviceId: deviceIdInput
|
|
4982
5176
|
}
|
|
4983
5177
|
},
|
|
4984
5178
|
async (args) => {
|
|
@@ -5150,7 +5344,7 @@ function registerFlowTools(server, sync) {
|
|
|
5150
5344
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
5151
5345
|
}
|
|
5152
5346
|
} catch (e) {
|
|
5153
|
-
stepErrors.push(
|
|
5347
|
+
stepErrors.push(errMsg8(e));
|
|
5154
5348
|
}
|
|
5155
5349
|
if (stepErrors.length) {
|
|
5156
5350
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -5173,7 +5367,7 @@ function registerFlowTools(server, sync) {
|
|
|
5173
5367
|
${shown.map(formatElement2).join("\n")}`
|
|
5174
5368
|
);
|
|
5175
5369
|
} catch (e) {
|
|
5176
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
5370
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(e))}`);
|
|
5177
5371
|
}
|
|
5178
5372
|
}
|
|
5179
5373
|
if (include === "all") {
|
|
@@ -5202,9 +5396,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
5202
5396
|
}
|
|
5203
5397
|
}
|
|
5204
5398
|
}
|
|
5205
|
-
return
|
|
5399
|
+
return text7(parts.join("\n\n"));
|
|
5206
5400
|
} catch (e) {
|
|
5207
|
-
return
|
|
5401
|
+
return fail9(errMsg8(e));
|
|
5208
5402
|
}
|
|
5209
5403
|
}
|
|
5210
5404
|
);
|
|
@@ -5225,25 +5419,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
5225
5419
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
5226
5420
|
|
|
5227
5421
|
// src/tools/packs/renders.ts
|
|
5228
|
-
var
|
|
5422
|
+
var import_zod25 = require("zod");
|
|
5229
5423
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
5230
|
-
var
|
|
5231
|
-
var
|
|
5424
|
+
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5425
|
+
var fail10 = (message) => ({
|
|
5232
5426
|
isError: true,
|
|
5233
5427
|
content: [{ type: "text", text: message }]
|
|
5234
5428
|
});
|
|
5235
|
-
var
|
|
5429
|
+
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
5236
5430
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5237
5431
|
var DEFAULT_IDLE_MS = 3e3;
|
|
5238
5432
|
var SETTLE_MS = 400;
|
|
5239
5433
|
var DEFAULT_TOP = 12;
|
|
5240
5434
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
5241
|
-
var stepSchema2 =
|
|
5242
|
-
tap:
|
|
5243
|
-
type:
|
|
5244
|
-
into:
|
|
5245
|
-
waitMs:
|
|
5246
|
-
repeat:
|
|
5435
|
+
var stepSchema2 = import_zod25.z.object({
|
|
5436
|
+
tap: import_zod25.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5437
|
+
type: import_zod25.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5438
|
+
into: import_zod25.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5439
|
+
waitMs: import_zod25.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5440
|
+
repeat: import_zod25.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
5247
5441
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
5248
5442
|
function selector2(needle) {
|
|
5249
5443
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -5336,14 +5530,14 @@ function registerRenderTools(server, sync) {
|
|
|
5336
5530
|
{
|
|
5337
5531
|
description: "Find out WHAT re-rendered on the current screen and WHY, in one call \u2014 no test route, no scaffolding. THE tool for 'this page is slow/laggy/janky', 'typing lags', 'the whole page re-renders when I tap one button'. Give it `steps` to measure a specific interaction (tap a button, type in a field) \u2014 measuring an idle window instead tells you nothing about what taps cost, which is how re-render fixes usually go wrong. Returns per-component render COST in ms (sorted by it \u2014 a component rendering 40x for 0.4ms is noise, one rendering 4x for 38ms is the answer), the CAUSE of each (parent cascade / own state / changed props), which parent dragged it along, and which props changed. `wasted` counts renders that produced nothing new \u2014 that's the removable part. AFTER YOU EDIT, call it again with the same steps and compareToPrevious:true to prove the fix landed and didn't move the cost somewhere else. (For choosing between several implementations of a screen, use run_benchmark_batch instead \u2014 that ranks variants; this explains one screen.) Requires Buoy Pro and @buoy-gg/highlight-updates.",
|
|
5338
5532
|
inputSchema: {
|
|
5339
|
-
steps:
|
|
5533
|
+
steps: import_zod25.z.array(stepSchema2).max(20).optional().describe(
|
|
5340
5534
|
"The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."
|
|
5341
5535
|
),
|
|
5342
|
-
idleMs:
|
|
5343
|
-
label:
|
|
5344
|
-
compareToPrevious:
|
|
5345
|
-
top:
|
|
5346
|
-
deviceId:
|
|
5536
|
+
idleMs: import_zod25.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5537
|
+
label: import_zod25.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5538
|
+
compareToPrevious: import_zod25.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5539
|
+
top: import_zod25.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5540
|
+
deviceId: deviceIdInput
|
|
5347
5541
|
}
|
|
5348
5542
|
},
|
|
5349
5543
|
async (args) => {
|
|
@@ -5352,18 +5546,18 @@ function registerRenderTools(server, sync) {
|
|
|
5352
5546
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
5353
5547
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
5354
5548
|
if (!uiTool) {
|
|
5355
|
-
return
|
|
5549
|
+
return fail10(
|
|
5356
5550
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
5357
5551
|
);
|
|
5358
5552
|
}
|
|
5359
5553
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
5360
|
-
return
|
|
5554
|
+
return fail10(
|
|
5361
5555
|
"This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route."
|
|
5362
5556
|
);
|
|
5363
5557
|
}
|
|
5364
5558
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
5365
5559
|
if (began && began.ok === false) {
|
|
5366
|
-
return
|
|
5560
|
+
return fail10(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
5367
5561
|
}
|
|
5368
5562
|
await delay5(SETTLE_MS);
|
|
5369
5563
|
const startedAt = Date.now();
|
|
@@ -5408,7 +5602,7 @@ function registerRenderTools(server, sync) {
|
|
|
5408
5602
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5409
5603
|
const summary = ended?.summary;
|
|
5410
5604
|
if (!summary) {
|
|
5411
|
-
return
|
|
5605
|
+
return fail10(
|
|
5412
5606
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5413
5607
|
);
|
|
5414
5608
|
}
|
|
@@ -5433,9 +5627,9 @@ function registerRenderTools(server, sync) {
|
|
|
5433
5627
|
} else if (!args.compareToPrevious) {
|
|
5434
5628
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5435
5629
|
}
|
|
5436
|
-
return
|
|
5630
|
+
return text8(parts.join("\n"));
|
|
5437
5631
|
} catch (e) {
|
|
5438
|
-
return
|
|
5632
|
+
return fail10(errMsg9(e));
|
|
5439
5633
|
}
|
|
5440
5634
|
}
|
|
5441
5635
|
);
|
|
@@ -5719,6 +5913,7 @@ function createMcpServer(sync) {
|
|
|
5719
5913
|
registerStorageTools(server, sync);
|
|
5720
5914
|
registerRoutesTools(server, sync);
|
|
5721
5915
|
registerConsoleTools(server, sync);
|
|
5916
|
+
registerSentryTools(server, sync);
|
|
5722
5917
|
registerTimeMachineTools(server, sync);
|
|
5723
5918
|
registerScenariosTools(server, sync);
|
|
5724
5919
|
registerScreenshotTools(server, sync);
|
|
@@ -5731,7 +5926,7 @@ function createMcpServer(sync) {
|
|
|
5731
5926
|
return server;
|
|
5732
5927
|
}
|
|
5733
5928
|
var deviceIdField = {
|
|
5734
|
-
deviceId:
|
|
5929
|
+
deviceId: import_zod26.z.string().optional().describe(
|
|
5735
5930
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5736
5931
|
)
|
|
5737
5932
|
};
|