@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/index.js
CHANGED
|
@@ -287,6 +287,7 @@ var SyncClient = class {
|
|
|
287
287
|
isConnected: d.isConnected !== false,
|
|
288
288
|
isPro: d.isPro === true,
|
|
289
289
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
290
|
+
collision: d.collision === true,
|
|
290
291
|
tools: caps?.tools ?? []
|
|
291
292
|
};
|
|
292
293
|
});
|
|
@@ -675,7 +676,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
675
676
|
|
|
676
677
|
// src/server.ts
|
|
677
678
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
678
|
-
var
|
|
679
|
+
var import_zod26 = require("zod");
|
|
679
680
|
|
|
680
681
|
// src/tools/generic.ts
|
|
681
682
|
var import_zod = require("zod");
|
|
@@ -686,7 +687,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
686
687
|
var import_node_path = __toESM(require("path"));
|
|
687
688
|
|
|
688
689
|
// src/version.ts
|
|
689
|
-
var VERSION = "7.0.
|
|
690
|
+
var VERSION = "7.0.34" ? "7.0.34" : "0.0.0-dev";
|
|
690
691
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
691
692
|
|
|
692
693
|
// src/update/checkForUpdate.ts
|
|
@@ -780,8 +781,12 @@ function registerGenericTools(server, sync) {
|
|
|
780
781
|
const connected = devices.filter((d) => d.isConnected);
|
|
781
782
|
const anyPro = connected.some((d) => d.isPro);
|
|
782
783
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
784
|
+
const collided = connected.filter((d) => d.collision);
|
|
783
785
|
return json({
|
|
784
786
|
devices,
|
|
787
|
+
...collided.length > 0 ? {
|
|
788
|
+
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.`
|
|
789
|
+
} : {},
|
|
785
790
|
...anyPro ? {} : {
|
|
786
791
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
787
792
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -857,7 +862,40 @@ function errMsg(e) {
|
|
|
857
862
|
}
|
|
858
863
|
|
|
859
864
|
// src/tools/packs/perfMonitor.ts
|
|
865
|
+
var import_zod3 = require("zod");
|
|
866
|
+
|
|
867
|
+
// src/tools/packKit.ts
|
|
860
868
|
var import_zod2 = require("zod");
|
|
869
|
+
var text = (s) => ({
|
|
870
|
+
content: [{ type: "text", text: s }]
|
|
871
|
+
});
|
|
872
|
+
var fail2 = (message) => ({
|
|
873
|
+
isError: true,
|
|
874
|
+
content: [{ type: "text", text: message }]
|
|
875
|
+
});
|
|
876
|
+
var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
|
|
877
|
+
var deviceIdInput = import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");
|
|
878
|
+
function clampLimit(value, fallback) {
|
|
879
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
880
|
+
}
|
|
881
|
+
function staleHeader(stale) {
|
|
882
|
+
return stale ? `${stale}
|
|
883
|
+
|
|
884
|
+
` : "";
|
|
885
|
+
}
|
|
886
|
+
function inactiveToolMessage(options) {
|
|
887
|
+
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).`;
|
|
888
|
+
}
|
|
889
|
+
function withDevice(sync, run2) {
|
|
890
|
+
return async (args) => {
|
|
891
|
+
try {
|
|
892
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
893
|
+
return await run2(args, target);
|
|
894
|
+
} catch (e) {
|
|
895
|
+
return fail2(errMsg2(e));
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
861
899
|
|
|
862
900
|
// src/tools/packs/perfMonitorTypes.ts
|
|
863
901
|
var PERF_TOOL_ID = "perf-monitor";
|
|
@@ -1174,13 +1212,10 @@ function round(n, d) {
|
|
|
1174
1212
|
var POLL_INTERVAL_MS = 1500;
|
|
1175
1213
|
var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1176
1214
|
var json2 = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
1177
|
-
var
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
name: import_zod2.z.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1182
|
-
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" }.'),
|
|
1183
|
-
route: import_zod2.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1215
|
+
var caseSchema = import_zod3.z.object({
|
|
1216
|
+
name: import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1217
|
+
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" }.'),
|
|
1218
|
+
route: import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1184
1219
|
});
|
|
1185
1220
|
async function readDeviceSettings(sync, deviceId) {
|
|
1186
1221
|
try {
|
|
@@ -1299,22 +1334,22 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1299
1334
|
{
|
|
1300
1335
|
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.",
|
|
1301
1336
|
inputSchema: {
|
|
1302
|
-
targetRoute:
|
|
1303
|
-
cases:
|
|
1304
|
-
bounceRoute:
|
|
1305
|
-
runsPerCase:
|
|
1306
|
-
perCaseDurationMs:
|
|
1307
|
-
settleMs:
|
|
1308
|
-
coolDownMs:
|
|
1309
|
-
navTimeoutMs:
|
|
1310
|
-
discardWarmupRuns:
|
|
1311
|
-
shuffleCases:
|
|
1312
|
-
reloadBetweenCases:
|
|
1313
|
-
captureRenders:
|
|
1337
|
+
targetRoute: import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1338
|
+
cases: import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
1339
|
+
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 "/").'),
|
|
1340
|
+
runsPerCase: import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
|
|
1341
|
+
perCaseDurationMs: import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),
|
|
1342
|
+
settleMs: import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),
|
|
1343
|
+
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1344
|
+
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1345
|
+
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1346
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
|
|
1347
|
+
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1348
|
+
captureRenders: import_zod3.z.boolean().optional().describe(
|
|
1314
1349
|
"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."
|
|
1315
1350
|
),
|
|
1316
|
-
deviceId:
|
|
1317
|
-
maxWaitMs:
|
|
1351
|
+
deviceId: import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1352
|
+
maxWaitMs: import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
1318
1353
|
}
|
|
1319
1354
|
},
|
|
1320
1355
|
async (args) => {
|
|
@@ -1384,7 +1419,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1384
1419
|
"get_automation_status",
|
|
1385
1420
|
{
|
|
1386
1421
|
description: "Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",
|
|
1387
|
-
inputSchema: { deviceId:
|
|
1422
|
+
inputSchema: { deviceId: import_zod3.z.string().optional().describe("Target device id (auto-selected if one is connected).") }
|
|
1388
1423
|
},
|
|
1389
1424
|
async ({ deviceId }) => {
|
|
1390
1425
|
try {
|
|
@@ -1405,8 +1440,8 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1405
1440
|
{
|
|
1406
1441
|
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).",
|
|
1407
1442
|
inputSchema: {
|
|
1408
|
-
deviceId:
|
|
1409
|
-
maxWaitMs:
|
|
1443
|
+
deviceId: deviceIdInput,
|
|
1444
|
+
maxWaitMs: import_zod3.z.number().optional().describe("Max time to wait. Default 600000 (10 min).")
|
|
1410
1445
|
}
|
|
1411
1446
|
},
|
|
1412
1447
|
async ({ deviceId, maxWaitMs }) => {
|
|
@@ -1479,8 +1514,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1479
1514
|
{
|
|
1480
1515
|
description: "Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",
|
|
1481
1516
|
inputSchema: {
|
|
1482
|
-
batchId:
|
|
1483
|
-
deviceId:
|
|
1517
|
+
batchId: import_zod3.z.string().describe("The batch id to report on."),
|
|
1518
|
+
deviceId: deviceIdInput
|
|
1484
1519
|
}
|
|
1485
1520
|
},
|
|
1486
1521
|
async ({ batchId, deviceId }) => {
|
|
@@ -1507,9 +1542,9 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1507
1542
|
{
|
|
1508
1543
|
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.",
|
|
1509
1544
|
inputSchema: {
|
|
1510
|
-
reportIdA:
|
|
1511
|
-
reportIdB:
|
|
1512
|
-
deviceId:
|
|
1545
|
+
reportIdA: import_zod3.z.string().describe("First (baseline) report id."),
|
|
1546
|
+
reportIdB: import_zod3.z.string().describe("Second report id to compare against the baseline."),
|
|
1547
|
+
deviceId: deviceIdInput
|
|
1513
1548
|
}
|
|
1514
1549
|
},
|
|
1515
1550
|
async ({ reportIdA, reportIdB, deviceId }) => {
|
|
@@ -1537,8 +1572,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1537
1572
|
{
|
|
1538
1573
|
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.",
|
|
1539
1574
|
inputSchema: {
|
|
1540
|
-
pages:
|
|
1541
|
-
deviceId:
|
|
1575
|
+
pages: import_zod3.z.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
|
|
1576
|
+
deviceId: deviceIdInput
|
|
1542
1577
|
}
|
|
1543
1578
|
},
|
|
1544
1579
|
async ({ pages, deviceId }) => {
|
|
@@ -1568,16 +1603,16 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1568
1603
|
{
|
|
1569
1604
|
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.`,
|
|
1570
1605
|
inputSchema: {
|
|
1571
|
-
perCaseDurationMs:
|
|
1572
|
-
settleMs:
|
|
1573
|
-
coolDownMs:
|
|
1574
|
-
navTimeoutMs:
|
|
1575
|
-
runsPerCase:
|
|
1576
|
-
discardWarmupRuns:
|
|
1577
|
-
shuffleCases:
|
|
1578
|
-
reloadBetweenCases:
|
|
1579
|
-
captureRenders:
|
|
1580
|
-
deviceId:
|
|
1606
|
+
perCaseDurationMs: import_zod3.z.number().optional().describe("Recording length per page (ms)."),
|
|
1607
|
+
settleMs: import_zod3.z.number().optional().describe("Idle after navigation before recording (ms)."),
|
|
1608
|
+
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases (ms)."),
|
|
1609
|
+
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1610
|
+
runsPerCase: import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1611
|
+
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1612
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
|
|
1613
|
+
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1614
|
+
captureRenders: import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1615
|
+
deviceId: deviceIdInput
|
|
1581
1616
|
}
|
|
1582
1617
|
},
|
|
1583
1618
|
async (args) => {
|
|
@@ -1625,11 +1660,8 @@ ${JSON.stringify(saved ?? settings, null, 2)}
|
|
|
1625
1660
|
}
|
|
1626
1661
|
|
|
1627
1662
|
// src/tools/packs/events.ts
|
|
1628
|
-
var
|
|
1663
|
+
var import_zod4 = require("zod");
|
|
1629
1664
|
var EVENTS_TOOL_ID = "events";
|
|
1630
|
-
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1631
|
-
var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1632
|
-
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1633
1665
|
var SOURCE_ALIASES = {
|
|
1634
1666
|
storage: ["storage-async", "storage-mmkv"],
|
|
1635
1667
|
"react-query": ["react-query", "react-query-query", "react-query-mutation"]
|
|
@@ -1644,13 +1676,13 @@ function registerEventsTools(server, sync) {
|
|
|
1644
1676
|
{
|
|
1645
1677
|
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.",
|
|
1646
1678
|
inputSchema: {
|
|
1647
|
-
limit:
|
|
1648
|
-
sources:
|
|
1649
|
-
status:
|
|
1650
|
-
includeData:
|
|
1651
|
-
format:
|
|
1652
|
-
preset:
|
|
1653
|
-
deviceId:
|
|
1679
|
+
limit: import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1680
|
+
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.'),
|
|
1681
|
+
status: import_zod4.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
|
|
1682
|
+
includeData: import_zod4.z.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
|
|
1683
|
+
format: import_zod4.z.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
|
|
1684
|
+
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)."),
|
|
1685
|
+
deviceId: deviceIdInput
|
|
1654
1686
|
}
|
|
1655
1687
|
},
|
|
1656
1688
|
async (args) => {
|
|
@@ -1669,7 +1701,7 @@ function registerEventsTools(server, sync) {
|
|
|
1669
1701
|
target
|
|
1670
1702
|
);
|
|
1671
1703
|
if (!res || typeof res.output !== "string") {
|
|
1672
|
-
return
|
|
1704
|
+
return fail2(
|
|
1673
1705
|
"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)."
|
|
1674
1706
|
);
|
|
1675
1707
|
}
|
|
@@ -1677,20 +1709,17 @@ function registerEventsTools(server, sync) {
|
|
|
1677
1709
|
const note = `
|
|
1678
1710
|
|
|
1679
1711
|
_Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
|
|
1680
|
-
return
|
|
1712
|
+
return text((res.output || "(no events captured)") + note);
|
|
1681
1713
|
} catch (e) {
|
|
1682
|
-
return
|
|
1714
|
+
return fail2(errMsg2(e));
|
|
1683
1715
|
}
|
|
1684
1716
|
}
|
|
1685
1717
|
);
|
|
1686
1718
|
}
|
|
1687
1719
|
|
|
1688
1720
|
// src/tools/packs/network.ts
|
|
1689
|
-
var
|
|
1721
|
+
var import_zod5 = require("zod");
|
|
1690
1722
|
var NETWORK_TOOL_ID = "network";
|
|
1691
|
-
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1692
|
-
var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1693
|
-
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1694
1723
|
var masterNote = (state) => {
|
|
1695
1724
|
if (state?.enabled !== false) return "";
|
|
1696
1725
|
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";
|
|
@@ -1816,12 +1845,12 @@ function registerNetworkTools(server, sync) {
|
|
|
1816
1845
|
{
|
|
1817
1846
|
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).",
|
|
1818
1847
|
inputSchema: {
|
|
1819
|
-
limit:
|
|
1820
|
-
pattern:
|
|
1821
|
-
status:
|
|
1822
|
-
flagged:
|
|
1823
|
-
includeBodies:
|
|
1824
|
-
deviceId:
|
|
1848
|
+
limit: import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),
|
|
1849
|
+
pattern: import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
1850
|
+
status: import_zod5.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
1851
|
+
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."),
|
|
1852
|
+
includeBodies: import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
1853
|
+
deviceId: deviceIdInput
|
|
1825
1854
|
}
|
|
1826
1855
|
},
|
|
1827
1856
|
async (args) => {
|
|
@@ -1845,7 +1874,7 @@ function registerNetworkTools(server, sync) {
|
|
|
1845
1874
|
);
|
|
1846
1875
|
const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1847
1876
|
if (rows2.length === 0) {
|
|
1848
|
-
return
|
|
1877
|
+
return text(
|
|
1849
1878
|
`No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
|
|
1850
1879
|
|
|
1851
1880
|
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.`
|
|
@@ -1862,7 +1891,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
1862
1891
|
if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
|
|
1863
1892
|
return out;
|
|
1864
1893
|
}).join("\n");
|
|
1865
|
-
return
|
|
1894
|
+
return text(
|
|
1866
1895
|
`**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
|
|
1867
1896
|
|
|
1868
1897
|
${body2}
|
|
@@ -1873,7 +1902,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
1873
1902
|
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1874
1903
|
if (rows.length === 0) {
|
|
1875
1904
|
const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
|
|
1876
|
-
return
|
|
1905
|
+
return text(`No requests on ${target}. ${why}`);
|
|
1877
1906
|
}
|
|
1878
1907
|
const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
1879
1908
|
const body = rows.map((e, i) => {
|
|
@@ -1883,7 +1912,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
1883
1912
|
return out;
|
|
1884
1913
|
}).join("\n");
|
|
1885
1914
|
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
1886
|
-
return
|
|
1915
|
+
return text(
|
|
1887
1916
|
`**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
|
|
1888
1917
|
|
|
1889
1918
|
${body}
|
|
@@ -1891,7 +1920,7 @@ ${body}
|
|
|
1891
1920
|
_\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)}`
|
|
1892
1921
|
);
|
|
1893
1922
|
} catch (e) {
|
|
1894
|
-
return
|
|
1923
|
+
return fail2(errMsg2(e));
|
|
1895
1924
|
}
|
|
1896
1925
|
}
|
|
1897
1926
|
);
|
|
@@ -1900,10 +1929,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1900
1929
|
{
|
|
1901
1930
|
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.",
|
|
1902
1931
|
inputSchema: {
|
|
1903
|
-
action:
|
|
1904
|
-
id:
|
|
1905
|
-
key:
|
|
1906
|
-
deviceId:
|
|
1932
|
+
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)."),
|
|
1933
|
+
id: import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
1934
|
+
key: import_zod5.z.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
|
|
1935
|
+
deviceId: deviceIdInput
|
|
1907
1936
|
}
|
|
1908
1937
|
},
|
|
1909
1938
|
async (args) => {
|
|
@@ -1911,10 +1940,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1911
1940
|
const target = sync.requireProDevice(args.deviceId);
|
|
1912
1941
|
const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
|
|
1913
1942
|
if (needsId && !args.id) {
|
|
1914
|
-
return
|
|
1943
|
+
return fail2(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
|
|
1915
1944
|
}
|
|
1916
1945
|
if (args.action === "remove" && !args.key) {
|
|
1917
|
-
return
|
|
1946
|
+
return fail2('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
|
|
1918
1947
|
}
|
|
1919
1948
|
switch (args.action) {
|
|
1920
1949
|
case "pin":
|
|
@@ -1925,10 +1954,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1925
1954
|
{ id: args.id, pinned: args.action === "pin" },
|
|
1926
1955
|
target
|
|
1927
1956
|
);
|
|
1928
|
-
if (res === null) return
|
|
1957
|
+
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.`);
|
|
1929
1958
|
const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
|
|
1930
|
-
if (capped) return
|
|
1931
|
-
return
|
|
1959
|
+
if (capped) return fail2(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
|
|
1960
|
+
return text(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
|
|
1932
1961
|
}
|
|
1933
1962
|
case "save":
|
|
1934
1963
|
case "unsave": {
|
|
@@ -1938,26 +1967,26 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1938
1967
|
{ id: args.id, saved: args.action === "save" },
|
|
1939
1968
|
target
|
|
1940
1969
|
);
|
|
1941
|
-
if (res === null) return
|
|
1970
|
+
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.`);
|
|
1942
1971
|
const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
|
|
1943
|
-
if (capped) return
|
|
1944
|
-
return
|
|
1972
|
+
if (capped) return fail2(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
|
|
1973
|
+
return text(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
|
|
1945
1974
|
}
|
|
1946
1975
|
case "remove":
|
|
1947
1976
|
await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
|
|
1948
|
-
return
|
|
1977
|
+
return text(`Removed kept record \`${args.key}\` on ${target}.`);
|
|
1949
1978
|
case "clearPinned":
|
|
1950
1979
|
await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
|
|
1951
|
-
return
|
|
1980
|
+
return text(`Unpinned all requests on ${target} (saved ones kept).`);
|
|
1952
1981
|
case "clearSaved":
|
|
1953
1982
|
await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
|
|
1954
|
-
return
|
|
1983
|
+
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
1955
1984
|
case "clearRequests":
|
|
1956
1985
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
1957
|
-
return
|
|
1986
|
+
return text(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
|
|
1958
1987
|
}
|
|
1959
1988
|
} catch (e) {
|
|
1960
|
-
return
|
|
1989
|
+
return fail2(errMsg2(e));
|
|
1961
1990
|
}
|
|
1962
1991
|
}
|
|
1963
1992
|
);
|
|
@@ -1966,48 +1995,48 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1966
1995
|
{
|
|
1967
1996
|
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.",
|
|
1968
1997
|
inputSchema: {
|
|
1969
|
-
action:
|
|
1998
|
+
action: import_zod5.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
1970
1999
|
"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."
|
|
1971
2000
|
),
|
|
1972
|
-
urlPattern:
|
|
2001
|
+
urlPattern: import_zod5.z.string().optional().describe(
|
|
1973
2002
|
"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."
|
|
1974
2003
|
),
|
|
1975
|
-
fromRequestId:
|
|
2004
|
+
fromRequestId: import_zod5.z.string().optional().describe(
|
|
1976
2005
|
"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."
|
|
1977
2006
|
),
|
|
1978
|
-
methods:
|
|
1979
|
-
kind:
|
|
2007
|
+
methods: import_zod5.z.array(import_zod5.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
|
|
2008
|
+
kind: import_zod5.z.enum(["respond", "fail", "delay"]).optional().describe(
|
|
1980
2009
|
"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."
|
|
1981
2010
|
),
|
|
1982
|
-
status:
|
|
2011
|
+
status: import_zod5.z.number().optional().describe(
|
|
1983
2012
|
"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."
|
|
1984
2013
|
),
|
|
1985
|
-
body:
|
|
2014
|
+
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(
|
|
1986
2015
|
"For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
|
|
1987
2016
|
),
|
|
1988
|
-
statusText:
|
|
2017
|
+
statusText: import_zod5.z.string().optional().describe(
|
|
1989
2018
|
"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."
|
|
1990
2019
|
),
|
|
1991
|
-
headers:
|
|
1992
|
-
failKind:
|
|
1993
|
-
delayMs:
|
|
1994
|
-
times:
|
|
2020
|
+
headers: import_zod5.z.record(import_zod5.z.string()).optional().describe("For kind 'respond': response headers to send."),
|
|
2021
|
+
failKind: import_zod5.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
|
|
2022
|
+
delayMs: import_zod5.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
|
|
2023
|
+
times: import_zod5.z.number().optional().describe(
|
|
1995
2024
|
"Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
|
|
1996
2025
|
),
|
|
1997
|
-
alternate:
|
|
2026
|
+
alternate: import_zod5.z.boolean().optional().describe(
|
|
1998
2027
|
"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."
|
|
1999
2028
|
),
|
|
2000
|
-
name:
|
|
2001
|
-
enabled:
|
|
2029
|
+
name: import_zod5.z.string().optional().describe("Label shown in the developer's UI."),
|
|
2030
|
+
enabled: import_zod5.z.boolean().optional().describe(
|
|
2002
2031
|
"For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
|
|
2003
2032
|
),
|
|
2004
|
-
id:
|
|
2033
|
+
id: import_zod5.z.string().optional().describe(
|
|
2005
2034
|
"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."
|
|
2006
2035
|
),
|
|
2007
|
-
url:
|
|
2036
|
+
url: import_zod5.z.string().optional().describe(
|
|
2008
2037
|
"For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
|
|
2009
2038
|
),
|
|
2010
|
-
deviceId:
|
|
2039
|
+
deviceId: deviceIdInput
|
|
2011
2040
|
}
|
|
2012
2041
|
},
|
|
2013
2042
|
async (args) => {
|
|
@@ -2016,7 +2045,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2016
2045
|
switch (args.action) {
|
|
2017
2046
|
case "add": {
|
|
2018
2047
|
if (!args.urlPattern && !args.fromRequestId) {
|
|
2019
|
-
return
|
|
2048
|
+
return fail2(
|
|
2020
2049
|
"`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*'."
|
|
2021
2050
|
);
|
|
2022
2051
|
}
|
|
@@ -2048,13 +2077,13 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2048
2077
|
);
|
|
2049
2078
|
const shaped = res;
|
|
2050
2079
|
if (!shaped?.ok) {
|
|
2051
|
-
return
|
|
2080
|
+
return fail2(shaped?.error ?? `Could not add the rule on ${target}.`);
|
|
2052
2081
|
}
|
|
2053
2082
|
const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
|
|
2054
2083
|
const armed = shaped.rule?.enabled === false ? `
|
|
2055
2084
|
|
|
2056
2085
|
It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
|
|
2057
|
-
return
|
|
2086
|
+
return text(
|
|
2058
2087
|
`Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
|
|
2059
2088
|
|
|
2060
2089
|
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.`
|
|
@@ -2062,14 +2091,14 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2062
2091
|
}
|
|
2063
2092
|
case "remove": {
|
|
2064
2093
|
if (!args.id) {
|
|
2065
|
-
return
|
|
2094
|
+
return fail2('`remove` needs an `id`. List them with network_override({action:"list"}).');
|
|
2066
2095
|
}
|
|
2067
2096
|
await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
|
|
2068
|
-
return
|
|
2097
|
+
return text(`Removed override \`${args.id}\` on ${target}.`);
|
|
2069
2098
|
}
|
|
2070
2099
|
case "clear":
|
|
2071
2100
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2072
|
-
return
|
|
2101
|
+
return text(`Removed all override rules on ${target}. Traffic is real again.`);
|
|
2073
2102
|
case "enable":
|
|
2074
2103
|
case "disable": {
|
|
2075
2104
|
const enabled = args.action === "enable";
|
|
@@ -2080,12 +2109,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2080
2109
|
{ id: args.id, enabled },
|
|
2081
2110
|
target
|
|
2082
2111
|
);
|
|
2083
|
-
return
|
|
2112
|
+
return text(
|
|
2084
2113
|
`Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2085
2114
|
);
|
|
2086
2115
|
}
|
|
2087
2116
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2088
|
-
return
|
|
2117
|
+
return text(
|
|
2089
2118
|
enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2090
2119
|
);
|
|
2091
2120
|
}
|
|
@@ -2100,11 +2129,11 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2100
2129
|
if (args.id) {
|
|
2101
2130
|
const rule = rules.find((r) => r.id === args.id);
|
|
2102
2131
|
if (!rule) {
|
|
2103
|
-
return
|
|
2132
|
+
return fail2(
|
|
2104
2133
|
`No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
|
|
2105
2134
|
);
|
|
2106
2135
|
}
|
|
2107
|
-
return
|
|
2136
|
+
return text(
|
|
2108
2137
|
`**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
|
|
2109
2138
|
|
|
2110
2139
|
\`\`\`json
|
|
@@ -2113,7 +2142,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2113
2142
|
);
|
|
2114
2143
|
}
|
|
2115
2144
|
if (rules.length === 0) {
|
|
2116
|
-
return
|
|
2145
|
+
return text(
|
|
2117
2146
|
`No override rules on ${target}.
|
|
2118
2147
|
|
|
2119
2148
|
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
@@ -2141,7 +2170,7 @@ Add one with network_override({action:"add", urlPattern:"*", status:500}) to for
|
|
|
2141
2170
|
return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
|
|
2142
2171
|
\u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
|
|
2143
2172
|
});
|
|
2144
|
-
return
|
|
2173
|
+
return text(
|
|
2145
2174
|
`**Override rules on ${target}**${masterNote(res)}
|
|
2146
2175
|
|
|
2147
2176
|
${rows.join("\n")}
|
|
@@ -2157,11 +2186,11 @@ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).
|
|
|
2157
2186
|
target
|
|
2158
2187
|
);
|
|
2159
2188
|
if (!res) {
|
|
2160
|
-
return
|
|
2189
|
+
return fail2(
|
|
2161
2190
|
`${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
|
|
2162
2191
|
);
|
|
2163
2192
|
}
|
|
2164
|
-
return
|
|
2193
|
+
return text(
|
|
2165
2194
|
`**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
|
|
2166
2195
|
|
|
2167
2196
|
\`\`\`json
|
|
@@ -2173,24 +2202,21 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2173
2202
|
}
|
|
2174
2203
|
}
|
|
2175
2204
|
} catch (e) {
|
|
2176
|
-
return
|
|
2205
|
+
return fail2(errMsg2(e));
|
|
2177
2206
|
}
|
|
2178
2207
|
}
|
|
2179
2208
|
);
|
|
2180
2209
|
}
|
|
2181
2210
|
|
|
2182
2211
|
// src/tools/packs/reactQuery.ts
|
|
2183
|
-
var
|
|
2212
|
+
var import_zod6 = require("zod");
|
|
2184
2213
|
var RQ_TOOL_ID = "query";
|
|
2185
2214
|
var RQ_TOOL_ID_LEGACY = "react-query";
|
|
2186
|
-
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2187
|
-
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2188
|
-
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
2189
2215
|
async function callQueryAction(sync, action, params, target) {
|
|
2190
2216
|
try {
|
|
2191
2217
|
return await sync.callAction(RQ_TOOL_ID, action, params, target);
|
|
2192
2218
|
} catch (e) {
|
|
2193
|
-
const msg =
|
|
2219
|
+
const msg = errMsg2(e);
|
|
2194
2220
|
if (!/unknown tool/i.test(msg)) throw e;
|
|
2195
2221
|
return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
|
|
2196
2222
|
}
|
|
@@ -2218,10 +2244,10 @@ function registerReactQueryTools(server, sync) {
|
|
|
2218
2244
|
{
|
|
2219
2245
|
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.",
|
|
2220
2246
|
inputSchema: {
|
|
2221
|
-
includeData:
|
|
2222
|
-
staleOnly:
|
|
2223
|
-
limit:
|
|
2224
|
-
deviceId:
|
|
2247
|
+
includeData: import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
2248
|
+
staleOnly: import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),
|
|
2249
|
+
limit: import_zod6.z.number().optional().describe("Most-recently-updated N queries. Default 25."),
|
|
2250
|
+
deviceId: deviceIdInput
|
|
2225
2251
|
}
|
|
2226
2252
|
},
|
|
2227
2253
|
async (args) => {
|
|
@@ -2234,12 +2260,12 @@ function registerReactQueryTools(server, sync) {
|
|
|
2234
2260
|
target
|
|
2235
2261
|
);
|
|
2236
2262
|
if (!res || !Array.isArray(res.queries)) {
|
|
2237
|
-
return
|
|
2263
|
+
return fail2(
|
|
2238
2264
|
"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)."
|
|
2239
2265
|
);
|
|
2240
2266
|
}
|
|
2241
2267
|
if (res.queries.length === 0) {
|
|
2242
|
-
return
|
|
2268
|
+
return text(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
|
|
2243
2269
|
}
|
|
2244
2270
|
const lines = res.queries.map((q, i) => {
|
|
2245
2271
|
const icon = STATUS_ICON[q.status] ?? "\u2022";
|
|
@@ -2252,11 +2278,11 @@ function registerReactQueryTools(server, sync) {
|
|
|
2252
2278
|
const staleCount = res.queries.filter((q) => q.isStale).length;
|
|
2253
2279
|
const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
|
|
2254
2280
|
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._";
|
|
2255
|
-
return
|
|
2281
|
+
return text(`${header}
|
|
2256
2282
|
|
|
2257
2283
|
${lines.join("\n")}${note}`);
|
|
2258
2284
|
} catch (e) {
|
|
2259
|
-
return
|
|
2285
|
+
return fail2(errMsg2(e));
|
|
2260
2286
|
}
|
|
2261
2287
|
}
|
|
2262
2288
|
);
|
|
@@ -2265,9 +2291,9 @@ ${lines.join("\n")}${note}`);
|
|
|
2265
2291
|
{
|
|
2266
2292
|
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.",
|
|
2267
2293
|
inputSchema: {
|
|
2268
|
-
action:
|
|
2269
|
-
queryHash:
|
|
2270
|
-
deviceId:
|
|
2294
|
+
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."),
|
|
2295
|
+
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."),
|
|
2296
|
+
deviceId: deviceIdInput
|
|
2271
2297
|
}
|
|
2272
2298
|
},
|
|
2273
2299
|
async (args) => {
|
|
@@ -2275,174 +2301,145 @@ ${lines.join("\n")}${note}`);
|
|
|
2275
2301
|
const target = sync.requireProDevice(args.deviceId);
|
|
2276
2302
|
if (args.action === "clear") {
|
|
2277
2303
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
2278
|
-
return
|
|
2304
|
+
return text("Cleared the entire React Query cache.");
|
|
2279
2305
|
}
|
|
2280
2306
|
if (!args.queryHash) {
|
|
2281
|
-
return
|
|
2307
|
+
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
2282
2308
|
}
|
|
2283
2309
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
2284
|
-
return
|
|
2310
|
+
return text(`Ran ${args.action} on query \`${args.queryHash}\`.`);
|
|
2285
2311
|
} catch (e) {
|
|
2286
|
-
return
|
|
2312
|
+
return fail2(errMsg2(e));
|
|
2287
2313
|
}
|
|
2288
2314
|
}
|
|
2289
2315
|
);
|
|
2290
2316
|
}
|
|
2291
2317
|
|
|
2292
2318
|
// src/tools/packs/zustand.ts
|
|
2293
|
-
var
|
|
2319
|
+
var import_zod7 = require("zod");
|
|
2294
2320
|
var ZUSTAND_TOOL_ID = "zustand";
|
|
2295
|
-
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2296
|
-
var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2297
|
-
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
2298
2321
|
function registerZustandTools(server, sync) {
|
|
2299
2322
|
server.registerTool(
|
|
2300
2323
|
"get_zustand_state",
|
|
2301
2324
|
{
|
|
2302
2325
|
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'].)",
|
|
2303
2326
|
inputSchema: {
|
|
2304
|
-
includeValues:
|
|
2305
|
-
limit:
|
|
2306
|
-
deviceId:
|
|
2327
|
+
includeValues: import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
2328
|
+
limit: import_zod7.z.number().optional().describe("First N stores. Default all."),
|
|
2329
|
+
deviceId: deviceIdInput
|
|
2307
2330
|
}
|
|
2308
2331
|
},
|
|
2309
|
-
async (args) => {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2332
|
+
withDevice(sync, async (args, target) => {
|
|
2333
|
+
const res = await sync.callAction(
|
|
2334
|
+
ZUSTAND_TOOL_ID,
|
|
2335
|
+
"listStores",
|
|
2336
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
2337
|
+
target
|
|
2338
|
+
);
|
|
2339
|
+
if (!res || !Array.isArray(res.stores)) {
|
|
2340
|
+
return fail2(
|
|
2341
|
+
inactiveToolMessage({ toolId: ZUSTAND_TOOL_ID, mcpTool: "get_zustand_state", action: "listStores" })
|
|
2317
2342
|
);
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2326
|
-
const lines = res.stores.map((s, i) => {
|
|
2327
|
-
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
2328
|
-
const body = res.includedValues ? `
|
|
2343
|
+
}
|
|
2344
|
+
if (res.stores.length === 0) {
|
|
2345
|
+
return text("No Zustand stores are registered (call watchStores to register them).");
|
|
2346
|
+
}
|
|
2347
|
+
const lines = res.stores.map((s, i) => {
|
|
2348
|
+
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
2349
|
+
const body = res.includedValues ? `
|
|
2329
2350
|
${JSON.stringify(s.currentState)}` : ` \xB7 keys: ${(s.keys ?? []).join(", ") || "\u2014"}`;
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2351
|
+
return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
|
|
2352
|
+
});
|
|
2353
|
+
const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
|
|
2354
|
+
return text(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
|
|
2334
2355
|
|
|
2335
2356
|
${lines.join("\n")}${note}`);
|
|
2336
|
-
|
|
2337
|
-
return fail6(errMsg6(e));
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2357
|
+
})
|
|
2340
2358
|
);
|
|
2341
2359
|
server.registerTool(
|
|
2342
2360
|
"zustand_action",
|
|
2343
2361
|
{
|
|
2344
2362
|
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.",
|
|
2345
2363
|
inputSchema: {
|
|
2346
|
-
store:
|
|
2347
|
-
state:
|
|
2348
|
-
replace:
|
|
2349
|
-
deviceId:
|
|
2364
|
+
store: import_zod7.z.string().describe("Store name to target (from get_zustand_state)."),
|
|
2365
|
+
state: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).describe("The new state object."),
|
|
2366
|
+
replace: import_zod7.z.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
|
|
2367
|
+
deviceId: deviceIdInput
|
|
2350
2368
|
}
|
|
2351
2369
|
},
|
|
2352
|
-
async (args) => {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
return text5(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2362
|
-
} catch (e) {
|
|
2363
|
-
return fail6(errMsg6(e));
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2370
|
+
withDevice(sync, async (args, target) => {
|
|
2371
|
+
await sync.callAction(
|
|
2372
|
+
ZUSTAND_TOOL_ID,
|
|
2373
|
+
"setState",
|
|
2374
|
+
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
2375
|
+
target
|
|
2376
|
+
);
|
|
2377
|
+
return text(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2378
|
+
})
|
|
2366
2379
|
);
|
|
2367
2380
|
}
|
|
2368
2381
|
|
|
2369
2382
|
// src/tools/packs/jotai.ts
|
|
2370
|
-
var
|
|
2383
|
+
var import_zod8 = require("zod");
|
|
2371
2384
|
var JOTAI_TOOL_ID = "jotai";
|
|
2372
|
-
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2373
|
-
var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2374
|
-
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
2375
2385
|
function registerJotaiTools(server, sync) {
|
|
2376
2386
|
server.registerTool(
|
|
2377
2387
|
"get_jotai_state",
|
|
2378
2388
|
{
|
|
2379
2389
|
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'].)",
|
|
2380
2390
|
inputSchema: {
|
|
2381
|
-
includeValues:
|
|
2382
|
-
limit:
|
|
2383
|
-
deviceId:
|
|
2391
|
+
includeValues: import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
2392
|
+
limit: import_zod8.z.number().optional().describe("First N atoms. Default all."),
|
|
2393
|
+
deviceId: deviceIdInput
|
|
2384
2394
|
}
|
|
2385
2395
|
},
|
|
2386
|
-
async (args) => {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2396
|
+
withDevice(sync, async (args, target) => {
|
|
2397
|
+
const res = await sync.callAction(
|
|
2398
|
+
JOTAI_TOOL_ID,
|
|
2399
|
+
"listAtoms",
|
|
2400
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
2401
|
+
target
|
|
2402
|
+
);
|
|
2403
|
+
if (!res || !Array.isArray(res.atoms)) {
|
|
2404
|
+
return fail2(
|
|
2405
|
+
inactiveToolMessage({ toolId: JOTAI_TOOL_ID, mcpTool: "get_jotai_state", action: "listAtoms" })
|
|
2394
2406
|
);
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
}
|
|
2403
|
-
const lines = res.atoms.map((a, i) => {
|
|
2404
|
-
const kind = a.writable ? "writable" : "read-only";
|
|
2405
|
-
const value = res.includedValues ? `
|
|
2407
|
+
}
|
|
2408
|
+
if (res.atoms.length === 0) {
|
|
2409
|
+
return text("No Jotai atoms are being watched (call watchAtoms to register them).");
|
|
2410
|
+
}
|
|
2411
|
+
const lines = res.atoms.map((a, i) => {
|
|
2412
|
+
const kind = a.writable ? "writable" : "read-only";
|
|
2413
|
+
const value = res.includedValues ? `
|
|
2406
2414
|
${JSON.stringify(a.currentValue)}` : "";
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2415
|
+
return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
|
|
2416
|
+
});
|
|
2417
|
+
const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
|
|
2418
|
+
return text(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
|
|
2411
2419
|
|
|
2412
2420
|
${lines.join("\n")}${note}`);
|
|
2413
|
-
|
|
2414
|
-
return fail7(errMsg7(e));
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2421
|
+
})
|
|
2417
2422
|
);
|
|
2418
2423
|
server.registerTool(
|
|
2419
2424
|
"jotai_action",
|
|
2420
2425
|
{
|
|
2421
2426
|
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.",
|
|
2422
2427
|
inputSchema: {
|
|
2423
|
-
atom:
|
|
2424
|
-
value:
|
|
2425
|
-
deviceId:
|
|
2428
|
+
atom: import_zod8.z.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
|
|
2429
|
+
value: import_zod8.z.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
|
|
2430
|
+
deviceId: deviceIdInput
|
|
2426
2431
|
}
|
|
2427
2432
|
},
|
|
2428
|
-
async (args) => {
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
return text6(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2433
|
-
} catch (e) {
|
|
2434
|
-
return fail7(errMsg7(e));
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2433
|
+
withDevice(sync, async (args, target) => {
|
|
2434
|
+
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
2435
|
+
return text(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2436
|
+
})
|
|
2437
2437
|
);
|
|
2438
2438
|
}
|
|
2439
2439
|
|
|
2440
2440
|
// src/tools/packs/redux.ts
|
|
2441
|
-
var
|
|
2441
|
+
var import_zod9 = require("zod");
|
|
2442
2442
|
var REDUX_TOOL_ID = "redux";
|
|
2443
|
-
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2444
|
-
var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2445
|
-
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
2446
2443
|
function noStoreMessage(reason) {
|
|
2447
2444
|
switch (reason) {
|
|
2448
2445
|
case "no-react-redux":
|
|
@@ -2461,8 +2458,8 @@ function registerReduxTools(server, sync) {
|
|
|
2461
2458
|
{
|
|
2462
2459
|
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'].)",
|
|
2463
2460
|
inputSchema: {
|
|
2464
|
-
includeValues:
|
|
2465
|
-
deviceId:
|
|
2461
|
+
includeValues: import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
2462
|
+
deviceId: deviceIdInput
|
|
2466
2463
|
}
|
|
2467
2464
|
},
|
|
2468
2465
|
async (args) => {
|
|
@@ -2475,15 +2472,15 @@ function registerReduxTools(server, sync) {
|
|
|
2475
2472
|
target
|
|
2476
2473
|
);
|
|
2477
2474
|
if (!res) {
|
|
2478
|
-
return
|
|
2475
|
+
return fail2(
|
|
2479
2476
|
"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)."
|
|
2480
2477
|
);
|
|
2481
2478
|
}
|
|
2482
|
-
if (!res.available) return
|
|
2479
|
+
if (!res.available) return fail2(noStoreMessage(res.reason));
|
|
2483
2480
|
const slices = res.slices.length ? res.slices.join(", ") : "(none)";
|
|
2484
2481
|
const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
|
|
2485
2482
|
if (args.includeValues) {
|
|
2486
|
-
return
|
|
2483
|
+
return text(
|
|
2487
2484
|
`**Redux store \u2014 slices: ${slices}**
|
|
2488
2485
|
|
|
2489
2486
|
\`\`\`json
|
|
@@ -2491,13 +2488,13 @@ ${JSON.stringify(res.state, null, 2)}
|
|
|
2491
2488
|
\`\`\`${captureNote}`
|
|
2492
2489
|
);
|
|
2493
2490
|
}
|
|
2494
|
-
return
|
|
2491
|
+
return text(
|
|
2495
2492
|
`**Redux store \u2014 slices: ${slices}**
|
|
2496
2493
|
|
|
2497
2494
|
_Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
|
|
2498
2495
|
);
|
|
2499
2496
|
} catch (e) {
|
|
2500
|
-
return
|
|
2497
|
+
return fail2(errMsg2(e));
|
|
2501
2498
|
}
|
|
2502
2499
|
}
|
|
2503
2500
|
);
|
|
@@ -2506,9 +2503,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
2506
2503
|
{
|
|
2507
2504
|
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 }.",
|
|
2508
2505
|
inputSchema: {
|
|
2509
|
-
type:
|
|
2510
|
-
payload:
|
|
2511
|
-
deviceId:
|
|
2506
|
+
type: import_zod9.z.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
|
|
2507
|
+
payload: import_zod9.z.unknown().optional().describe("Optional action payload (any JSON)."),
|
|
2508
|
+
deviceId: deviceIdInput
|
|
2512
2509
|
}
|
|
2513
2510
|
},
|
|
2514
2511
|
async (args) => {
|
|
@@ -2517,21 +2514,18 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
2517
2514
|
const action = { type: args.type };
|
|
2518
2515
|
if (args.payload !== void 0) action.payload = args.payload;
|
|
2519
2516
|
const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
|
|
2520
|
-
if (!res || !res.dispatched) return
|
|
2521
|
-
return
|
|
2517
|
+
if (!res || !res.dispatched) return fail2(noStoreMessage(res?.reason));
|
|
2518
|
+
return text(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
|
|
2522
2519
|
} catch (e) {
|
|
2523
|
-
return
|
|
2520
|
+
return fail2(errMsg2(e));
|
|
2524
2521
|
}
|
|
2525
2522
|
}
|
|
2526
2523
|
);
|
|
2527
2524
|
}
|
|
2528
2525
|
|
|
2529
2526
|
// src/tools/packs/storage.ts
|
|
2530
|
-
var
|
|
2527
|
+
var import_zod10 = require("zod");
|
|
2531
2528
|
var STORAGE_TOOL_ID = "storage";
|
|
2532
|
-
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2533
|
-
var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2534
|
-
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
2535
2529
|
function truncate2(v, n = 200) {
|
|
2536
2530
|
if (v === null || v === void 0) return "(null)";
|
|
2537
2531
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -2543,10 +2537,10 @@ function registerStorageTools(server, sync) {
|
|
|
2543
2537
|
{
|
|
2544
2538
|
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.",
|
|
2545
2539
|
inputSchema: {
|
|
2546
|
-
pattern:
|
|
2547
|
-
includeValues:
|
|
2548
|
-
limit:
|
|
2549
|
-
deviceId:
|
|
2540
|
+
pattern: import_zod10.z.string().optional().describe("Only keys containing this substring (case-insensitive)."),
|
|
2541
|
+
includeValues: import_zod10.z.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
|
|
2542
|
+
limit: import_zod10.z.number().optional().describe("Max keys per backend section. Default 100."),
|
|
2543
|
+
deviceId: deviceIdInput
|
|
2550
2544
|
}
|
|
2551
2545
|
},
|
|
2552
2546
|
async (args) => {
|
|
@@ -2558,7 +2552,7 @@ function registerStorageTools(server, sync) {
|
|
|
2558
2552
|
const sections = [];
|
|
2559
2553
|
const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
|
|
2560
2554
|
if (!Array.isArray(allKeys)) {
|
|
2561
|
-
return
|
|
2555
|
+
return fail2("No storage result from the device. The storage tool may be inactive.");
|
|
2562
2556
|
}
|
|
2563
2557
|
const asyncKeys = allKeys.filter(matches2).slice(0, limit);
|
|
2564
2558
|
if (asyncKeys.length > 0) {
|
|
@@ -2599,12 +2593,12 @@ ${lines.join("\n")}`);
|
|
|
2599
2593
|
${lines.join("\n")}`);
|
|
2600
2594
|
}
|
|
2601
2595
|
if (sections.length === 0) {
|
|
2602
|
-
return
|
|
2596
|
+
return text(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
|
|
2603
2597
|
}
|
|
2604
2598
|
const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
|
|
2605
|
-
return
|
|
2599
|
+
return text(`${sections.join("\n\n")}${note}`);
|
|
2606
2600
|
} catch (e) {
|
|
2607
|
-
return
|
|
2601
|
+
return fail2(errMsg2(e));
|
|
2608
2602
|
}
|
|
2609
2603
|
}
|
|
2610
2604
|
);
|
|
@@ -2613,10 +2607,10 @@ ${lines.join("\n")}`);
|
|
|
2613
2607
|
{
|
|
2614
2608
|
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.",
|
|
2615
2609
|
inputSchema: {
|
|
2616
|
-
action:
|
|
2617
|
-
key:
|
|
2618
|
-
value:
|
|
2619
|
-
deviceId:
|
|
2610
|
+
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
|
|
2611
|
+
key: import_zod10.z.string().optional().describe("The key (required for set and remove)."),
|
|
2612
|
+
value: import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2613
|
+
deviceId: deviceIdInput
|
|
2620
2614
|
}
|
|
2621
2615
|
},
|
|
2622
2616
|
async (args) => {
|
|
@@ -2624,33 +2618,30 @@ ${lines.join("\n")}`);
|
|
|
2624
2618
|
const target = sync.requireProDevice(args.deviceId);
|
|
2625
2619
|
if (args.action === "clear") {
|
|
2626
2620
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2627
|
-
return
|
|
2621
|
+
return text("Cleared all app storage (Buoy's own devtool keys preserved).");
|
|
2628
2622
|
}
|
|
2629
2623
|
if (!args.key) {
|
|
2630
|
-
return
|
|
2624
|
+
return fail2(`'${args.action}' needs a key.`);
|
|
2631
2625
|
}
|
|
2632
2626
|
if (args.action === "remove") {
|
|
2633
2627
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2634
|
-
return
|
|
2628
|
+
return text(`Removed storage key "${args.key}".`);
|
|
2635
2629
|
}
|
|
2636
2630
|
if (args.value === void 0) {
|
|
2637
|
-
return
|
|
2631
|
+
return fail2("'set' needs a value (a string).");
|
|
2638
2632
|
}
|
|
2639
2633
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2640
|
-
return
|
|
2634
|
+
return text(`Set storage key "${args.key}".`);
|
|
2641
2635
|
} catch (e) {
|
|
2642
|
-
return
|
|
2636
|
+
return fail2(errMsg2(e));
|
|
2643
2637
|
}
|
|
2644
2638
|
}
|
|
2645
2639
|
);
|
|
2646
2640
|
}
|
|
2647
2641
|
|
|
2648
2642
|
// src/tools/packs/routes.ts
|
|
2649
|
-
var
|
|
2643
|
+
var import_zod11 = require("zod");
|
|
2650
2644
|
var ROUTES_TOOL_ID = "route-events";
|
|
2651
|
-
var text9 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2652
|
-
var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2653
|
-
var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
|
|
2654
2645
|
function flattenRoutes(routes, acc = []) {
|
|
2655
2646
|
for (const r of routes ?? []) {
|
|
2656
2647
|
if (r.path) acc.push(r.path);
|
|
@@ -2664,8 +2655,8 @@ function registerRoutesTools(server, sync) {
|
|
|
2664
2655
|
{
|
|
2665
2656
|
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'].)",
|
|
2666
2657
|
inputSchema: {
|
|
2667
|
-
includeSitemap:
|
|
2668
|
-
deviceId:
|
|
2658
|
+
includeSitemap: import_zod11.z.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
|
|
2659
|
+
deviceId: deviceIdInput
|
|
2669
2660
|
}
|
|
2670
2661
|
},
|
|
2671
2662
|
async (args) => {
|
|
@@ -2698,9 +2689,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2698
2689
|
**Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
|
|
2699
2690
|
}
|
|
2700
2691
|
parts.push("\n_Navigate with navigate({ path })._");
|
|
2701
|
-
return
|
|
2692
|
+
return text(parts.join("\n"));
|
|
2702
2693
|
} catch (e) {
|
|
2703
|
-
return
|
|
2694
|
+
return fail2(errMsg2(e));
|
|
2704
2695
|
}
|
|
2705
2696
|
}
|
|
2706
2697
|
);
|
|
@@ -2709,9 +2700,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2709
2700
|
{
|
|
2710
2701
|
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 '/').",
|
|
2711
2702
|
inputSchema: {
|
|
2712
|
-
path:
|
|
2713
|
-
replace:
|
|
2714
|
-
deviceId:
|
|
2703
|
+
path: import_zod11.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
|
|
2704
|
+
replace: import_zod11.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
|
|
2705
|
+
deviceId: deviceIdInput
|
|
2715
2706
|
}
|
|
2716
2707
|
},
|
|
2717
2708
|
async (args) => {
|
|
@@ -2723,20 +2714,17 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2723
2714
|
{ path: args.path, replace: args.replace === true },
|
|
2724
2715
|
target
|
|
2725
2716
|
);
|
|
2726
|
-
return
|
|
2717
|
+
return text(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
|
|
2727
2718
|
} catch (e) {
|
|
2728
|
-
return
|
|
2719
|
+
return fail2(errMsg2(e));
|
|
2729
2720
|
}
|
|
2730
2721
|
}
|
|
2731
2722
|
);
|
|
2732
2723
|
}
|
|
2733
2724
|
|
|
2734
2725
|
// src/tools/packs/console.ts
|
|
2735
|
-
var
|
|
2726
|
+
var import_zod12 = require("zod");
|
|
2736
2727
|
var CONSOLE_TOOL_ID = "console";
|
|
2737
|
-
var text10 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2738
|
-
var fail11 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2739
|
-
var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
|
|
2740
2728
|
var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
|
|
2741
2729
|
var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
|
|
2742
2730
|
function hhmmss(ts) {
|
|
@@ -2750,57 +2738,275 @@ function registerConsoleTools(server, sync) {
|
|
|
2750
2738
|
{
|
|
2751
2739
|
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').",
|
|
2752
2740
|
inputSchema: {
|
|
2753
|
-
level:
|
|
2754
|
-
limit:
|
|
2755
|
-
pattern:
|
|
2756
|
-
deviceId:
|
|
2741
|
+
level: import_zod12.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2742
|
+
limit: import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),
|
|
2743
|
+
pattern: import_zod12.z.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
|
|
2744
|
+
deviceId: deviceIdInput
|
|
2745
|
+
}
|
|
2746
|
+
},
|
|
2747
|
+
withDevice(sync, async (args, target) => {
|
|
2748
|
+
const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
|
|
2749
|
+
const banner = staleHeader(stale);
|
|
2750
|
+
const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
|
|
2751
|
+
const all = Array.isArray(p.entries) ? p.entries : [];
|
|
2752
|
+
const minSev = args.level ? SEVERITY[args.level] : 0;
|
|
2753
|
+
const pattern = args.pattern?.toLowerCase();
|
|
2754
|
+
const filtered = all.filter(
|
|
2755
|
+
(e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
|
|
2756
|
+
);
|
|
2757
|
+
const sorted = [...filtered].sort((a, b) => a.id - b.id);
|
|
2758
|
+
const limit = clampLimit(args.limit, 50);
|
|
2759
|
+
const shown = sorted.slice(-limit);
|
|
2760
|
+
if (shown.length === 0) {
|
|
2761
|
+
return text(`${banner}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
|
|
2762
|
+
}
|
|
2763
|
+
const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
|
|
2764
|
+
const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
|
|
2765
|
+
const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
|
|
2766
|
+
return text(`${banner}${header}
|
|
2767
|
+
|
|
2768
|
+
\`\`\`
|
|
2769
|
+
${lines.join("\n")}
|
|
2770
|
+
\`\`\`
|
|
2771
|
+
|
|
2772
|
+
_Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
2773
|
+
})
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// src/tools/packs/sentry.ts
|
|
2778
|
+
var import_zod13 = require("zod");
|
|
2779
|
+
var SENTRY_TOOL_ID = "sentry";
|
|
2780
|
+
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2781
|
+
var fail3 = (message) => ({
|
|
2782
|
+
isError: true,
|
|
2783
|
+
content: [{ type: "text", text: message }]
|
|
2784
|
+
});
|
|
2785
|
+
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
2786
|
+
function hhmmss2(ts) {
|
|
2787
|
+
const d = new Date(ts);
|
|
2788
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2789
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
2790
|
+
}
|
|
2791
|
+
function bytes(n) {
|
|
2792
|
+
if (n < 1024) return `${n}B`;
|
|
2793
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
2794
|
+
return `${(n / (1024 * 1024)).toFixed(2)}MB`;
|
|
2795
|
+
}
|
|
2796
|
+
function units(n, format) {
|
|
2797
|
+
if (format === "bytes") return bytes(n);
|
|
2798
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2799
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
2800
|
+
return String(n);
|
|
2801
|
+
}
|
|
2802
|
+
var FREE_QUOTA = {
|
|
2803
|
+
errors: 5e3,
|
|
2804
|
+
spans: 5e6,
|
|
2805
|
+
replays: 50,
|
|
2806
|
+
logBytes: 5 * 1024 ** 3,
|
|
2807
|
+
attachmentBytes: 1 * 1024 ** 3
|
|
2808
|
+
};
|
|
2809
|
+
var CATEGORY_FORMAT = {
|
|
2810
|
+
errors: "count",
|
|
2811
|
+
spans: "count",
|
|
2812
|
+
replays: "count",
|
|
2813
|
+
logBytes: "bytes",
|
|
2814
|
+
attachmentBytes: "bytes"
|
|
2815
|
+
};
|
|
2816
|
+
function statusNote(status) {
|
|
2817
|
+
switch (status) {
|
|
2818
|
+
case "attached":
|
|
2819
|
+
return "";
|
|
2820
|
+
case "sdk-not-found":
|
|
2821
|
+
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.";
|
|
2822
|
+
case "no-client":
|
|
2823
|
+
return "\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";
|
|
2824
|
+
default:
|
|
2825
|
+
return "\n\n\u23F3 Still looking for the app's Sentry client.";
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
function registerSentryTools(server, sync) {
|
|
2829
|
+
server.registerTool(
|
|
2830
|
+
"get_sentry_envelopes",
|
|
2831
|
+
{
|
|
2832
|
+
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).",
|
|
2833
|
+
inputSchema: {
|
|
2834
|
+
type: import_zod13.z.string().optional().describe(
|
|
2835
|
+
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
2836
|
+
),
|
|
2837
|
+
limit: import_zod13.z.number().optional().describe("Most-recent N envelopes. Default 25."),
|
|
2838
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2757
2839
|
}
|
|
2758
2840
|
},
|
|
2759
2841
|
async (args) => {
|
|
2760
2842
|
try {
|
|
2761
2843
|
const target = sync.requireProDevice(args.deviceId);
|
|
2762
|
-
const { payload, stale } = await sync.getSnapshot(
|
|
2763
|
-
const
|
|
2844
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2845
|
+
const staleHeader2 = stale ? `${stale}
|
|
2764
2846
|
|
|
2765
2847
|
` : "";
|
|
2766
|
-
const p =
|
|
2767
|
-
const all = Array.isArray(p.
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const
|
|
2771
|
-
(e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
|
|
2772
|
-
);
|
|
2773
|
-
const sorted = [...filtered].sort((a, b) => a.id - b.id);
|
|
2774
|
-
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 50;
|
|
2775
|
-
const shown = sorted.slice(-limit);
|
|
2848
|
+
const p = payload ?? {};
|
|
2849
|
+
const all = Array.isArray(p.envelopes) ? p.envelopes : [];
|
|
2850
|
+
const filtered = args.type ? all.filter((e) => e.items.some((i) => i.type === args.type)) : all;
|
|
2851
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
|
|
2852
|
+
const shown = filtered.slice(-limit);
|
|
2776
2853
|
if (shown.length === 0) {
|
|
2777
|
-
return
|
|
2854
|
+
return text2(
|
|
2855
|
+
`${staleHeader2}No Sentry envelopes${args.type ? ` containing a '${args.type}' item` : ""} (${all.length} captured this session).${statusNote(p.status)}`
|
|
2856
|
+
);
|
|
2778
2857
|
}
|
|
2779
|
-
const lines = shown.map((e) =>
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2858
|
+
const lines = shown.map((e) => {
|
|
2859
|
+
const types = e.items.map((i) => i.type).join("+");
|
|
2860
|
+
const spans = e.items.reduce((sum, i) => sum + (i.spanCount ?? 0), 0);
|
|
2861
|
+
const spanNote = spans > 0 ? ` \xB7 ${spans} span${spans === 1 ? "" : "s"}` : "";
|
|
2862
|
+
const head = `${e.id}. ${hhmmss2(e.timestamp)} [${types}] ${bytes(e.totalBytes)}${spanNote}`;
|
|
2863
|
+
return `${head}
|
|
2864
|
+
\u2514\u2500 ${e.items[0]?.summary ?? "envelope"}`;
|
|
2865
|
+
});
|
|
2866
|
+
const header = `**Sentry \u2014 ${all.length} envelope${all.length === 1 ? "" : "s"} sent (showing ${shown.length}${args.type ? `, type '${args.type}'` : ""}):**`;
|
|
2867
|
+
return text2(
|
|
2868
|
+
`${staleHeader2}${header}${statusNote(p.status)}
|
|
2783
2869
|
|
|
2784
|
-
\`\`\`
|
|
2785
2870
|
${lines.join("\n")}
|
|
2871
|
+
|
|
2872
|
+
_Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`
|
|
2873
|
+
);
|
|
2874
|
+
} catch (e) {
|
|
2875
|
+
return fail3(errMsg3(e));
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
);
|
|
2879
|
+
server.registerTool(
|
|
2880
|
+
"get_sentry_cost",
|
|
2881
|
+
{
|
|
2882
|
+
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.",
|
|
2883
|
+
inputSchema: {
|
|
2884
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2885
|
+
}
|
|
2886
|
+
},
|
|
2887
|
+
async (args) => {
|
|
2888
|
+
try {
|
|
2889
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2890
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2891
|
+
const staleHeader2 = stale ? `${stale}
|
|
2892
|
+
|
|
2893
|
+
` : "";
|
|
2894
|
+
const p = payload ?? {};
|
|
2895
|
+
const usage = p.usage;
|
|
2896
|
+
if (!usage?.session) {
|
|
2897
|
+
return text2(
|
|
2898
|
+
`${staleHeader2}No usage recorded yet \u2014 nothing has been sent to Sentry this session.${statusNote(p.status)}`
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
const session = usage.session;
|
|
2902
|
+
const projected = usage.projectedMonthly;
|
|
2903
|
+
const keys = Object.keys(session);
|
|
2904
|
+
const rows = keys.map((key) => {
|
|
2905
|
+
const format = CATEGORY_FORMAT[key];
|
|
2906
|
+
const now = units(session[key], format);
|
|
2907
|
+
if (!projected) return `- ${key}: ${now} this session`;
|
|
2908
|
+
const month = projected[key];
|
|
2909
|
+
const quota = FREE_QUOTA[key];
|
|
2910
|
+
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
2911
|
+
const pctNote = pct === null ? " (not on the free plan)" : ` \u2014 ${pct}% of the free plan's ${units(quota, format)}`;
|
|
2912
|
+
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
2913
|
+
});
|
|
2914
|
+
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
2915
|
+
const contributors = usage.topContributors?.length ? `
|
|
2916
|
+
|
|
2917
|
+
**What produced it:**
|
|
2918
|
+
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
2919
|
+
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._";
|
|
2920
|
+
return text2(
|
|
2921
|
+
`${staleHeader2}**Sentry cost \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
2922
|
+
|
|
2923
|
+
${rows.join("\n")}${contributors}${caveat}`
|
|
2924
|
+
);
|
|
2925
|
+
} catch (e) {
|
|
2926
|
+
return fail3(errMsg3(e));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
);
|
|
2930
|
+
server.registerTool(
|
|
2931
|
+
"get_sentry_drops",
|
|
2932
|
+
{
|
|
2933
|
+
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.",
|
|
2934
|
+
inputSchema: {
|
|
2935
|
+
limit: import_zod13.z.number().optional().describe("Most-recent N drop records. Default 30."),
|
|
2936
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2937
|
+
}
|
|
2938
|
+
},
|
|
2939
|
+
async (args) => {
|
|
2940
|
+
try {
|
|
2941
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2942
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
2943
|
+
const staleHeader2 = stale ? `${stale}
|
|
2944
|
+
|
|
2945
|
+
` : "";
|
|
2946
|
+
const p = payload ?? {};
|
|
2947
|
+
const summary = p.drops?.summary ?? [];
|
|
2948
|
+
const recent = p.drops?.recent ?? [];
|
|
2949
|
+
if (summary.length === 0) {
|
|
2950
|
+
return text2(
|
|
2951
|
+
`${staleHeader2}Nothing dropped this session \u2014 every captured event reached the transport.${statusNote(p.status)}
|
|
2952
|
+
|
|
2953
|
+
_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._`
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
2957
|
+
const shown = recent.slice(-limit);
|
|
2958
|
+
const summaryLines = summary.map(
|
|
2959
|
+
(s) => `- ${s.reason}: ${s.units} unit${s.units === 1 ? "" : "s"} across ${s.records} record${s.records === 1 ? "" : "s"}`
|
|
2960
|
+
);
|
|
2961
|
+
const rows = shown.map(
|
|
2962
|
+
(d) => `${hhmmss2(d.timestamp)} [${d.reason}] ${d.category}${d.quantity > 1 ? ` \xD7${d.quantity}` : ""} \u2014 ${d.detail} (${d.source})`
|
|
2963
|
+
);
|
|
2964
|
+
return text2(
|
|
2965
|
+
`${staleHeader2}**Sentry drops \u2014 by reason:**
|
|
2966
|
+
|
|
2967
|
+
${summaryLines.join("\n")}
|
|
2968
|
+
|
|
2969
|
+
**Recent (${shown.length}):**
|
|
2970
|
+
|
|
2971
|
+
\`\`\`
|
|
2972
|
+
${rows.join("\n")}
|
|
2786
2973
|
\`\`\`
|
|
2787
2974
|
|
|
2788
|
-
|
|
2975
|
+
_"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._`
|
|
2976
|
+
);
|
|
2977
|
+
} catch (e) {
|
|
2978
|
+
return fail3(errMsg3(e));
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
);
|
|
2982
|
+
server.registerTool(
|
|
2983
|
+
"sentry_action",
|
|
2984
|
+
{
|
|
2985
|
+
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.",
|
|
2986
|
+
inputSchema: {
|
|
2987
|
+
action: import_zod13.z.enum(["clear"]).describe("The action to perform."),
|
|
2988
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2989
|
+
}
|
|
2990
|
+
},
|
|
2991
|
+
async (args) => {
|
|
2992
|
+
try {
|
|
2993
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2994
|
+
await sync.callAction(SENTRY_TOOL_ID, "clearEnvelopes", {}, target);
|
|
2995
|
+
return text2(
|
|
2996
|
+
"Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost."
|
|
2997
|
+
);
|
|
2789
2998
|
} catch (e) {
|
|
2790
|
-
return
|
|
2999
|
+
return fail3(errMsg3(e));
|
|
2791
3000
|
}
|
|
2792
3001
|
}
|
|
2793
3002
|
);
|
|
2794
3003
|
}
|
|
2795
3004
|
|
|
2796
3005
|
// src/tools/packs/timeMachine.ts
|
|
2797
|
-
var
|
|
3006
|
+
var import_zod14 = require("zod");
|
|
2798
3007
|
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2802
|
-
function kb(bytes) {
|
|
2803
|
-
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
3008
|
+
function kb(bytes2) {
|
|
3009
|
+
return bytes2 < 1024 ? `${bytes2} B` : `${(bytes2 / 1024).toFixed(1)} KB`;
|
|
2804
3010
|
}
|
|
2805
3011
|
function formatOutcome(outcome) {
|
|
2806
3012
|
const lines = Object.entries(outcome.results).map(([sid, r2]) => {
|
|
@@ -2825,7 +3031,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
2825
3031
|
{
|
|
2826
3032
|
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.",
|
|
2827
3033
|
inputSchema: {
|
|
2828
|
-
deviceId:
|
|
3034
|
+
deviceId: deviceIdInput
|
|
2829
3035
|
}
|
|
2830
3036
|
},
|
|
2831
3037
|
async (args) => {
|
|
@@ -2858,9 +3064,9 @@ ${providerLines.join("\n") || "_no providers registered_"}`
|
|
|
2858
3064
|
];
|
|
2859
3065
|
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2860
3066
|
${formatOutcome(p.lastOutcome)}`);
|
|
2861
|
-
return
|
|
3067
|
+
return text(parts.join("\n\n"));
|
|
2862
3068
|
} catch (e) {
|
|
2863
|
-
return
|
|
3069
|
+
return fail2(errMsg2(e));
|
|
2864
3070
|
}
|
|
2865
3071
|
}
|
|
2866
3072
|
);
|
|
@@ -2869,14 +3075,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2869
3075
|
{
|
|
2870
3076
|
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.",
|
|
2871
3077
|
inputSchema: {
|
|
2872
|
-
action:
|
|
2873
|
-
id:
|
|
2874
|
-
name:
|
|
2875
|
-
mode:
|
|
2876
|
-
excludeKeys:
|
|
2877
|
-
restoreRoute:
|
|
2878
|
-
sourceIds:
|
|
2879
|
-
deviceId:
|
|
3078
|
+
action: import_zod14.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3079
|
+
id: import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
3080
|
+
name: import_zod14.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
3081
|
+
mode: import_zod14.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
3082
|
+
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").'),
|
|
3083
|
+
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."),
|
|
3084
|
+
sourceIds: import_zod14.z.array(import_zod14.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
3085
|
+
deviceId: deviceIdInput
|
|
2880
3086
|
}
|
|
2881
3087
|
},
|
|
2882
3088
|
async (args) => {
|
|
@@ -2889,14 +3095,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2889
3095
|
const s = result.snapshot;
|
|
2890
3096
|
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2891
3097
|
\u26A0 ${sid}: ${w}`)).join("");
|
|
2892
|
-
return
|
|
3098
|
+
return text(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2893
3099
|
}
|
|
2894
3100
|
case "capture_baseline": {
|
|
2895
3101
|
const result = await call("captureBaseline", {});
|
|
2896
|
-
return
|
|
3102
|
+
return text(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2897
3103
|
}
|
|
2898
3104
|
case "preview": {
|
|
2899
|
-
if (!args.id) return
|
|
3105
|
+
if (!args.id) return fail2("preview requires `id` (from get_time_machine).");
|
|
2900
3106
|
const result = await call("preview", { id: args.id });
|
|
2901
3107
|
const lines = [];
|
|
2902
3108
|
let totalApplies = 0;
|
|
@@ -2915,46 +3121,46 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2915
3121
|
for (const note of src.notes) lines.push(` \xB7 ${note}`);
|
|
2916
3122
|
}
|
|
2917
3123
|
const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
|
|
2918
|
-
return
|
|
3124
|
+
return text(`${head}
|
|
2919
3125
|
${lines.join("\n")}`);
|
|
2920
3126
|
}
|
|
2921
3127
|
case "restore": {
|
|
2922
|
-
if (!args.id) return
|
|
3128
|
+
if (!args.id) return fail2("restore requires `id` (from get_time_machine).");
|
|
2923
3129
|
const outcome = await call("restore", {
|
|
2924
3130
|
id: args.id,
|
|
2925
3131
|
mode: args.mode ?? "live",
|
|
2926
3132
|
excludeKeys: args.excludeKeys,
|
|
2927
3133
|
restoreRoute: args.restoreRoute
|
|
2928
3134
|
});
|
|
2929
|
-
return
|
|
3135
|
+
return text(formatOutcome(outcome));
|
|
2930
3136
|
}
|
|
2931
3137
|
case "delete": {
|
|
2932
|
-
if (!args.id) return
|
|
3138
|
+
if (!args.id) return fail2("delete requires `id`.");
|
|
2933
3139
|
await call("delete", { id: args.id });
|
|
2934
|
-
return
|
|
3140
|
+
return text(`Deleted snapshot \`${args.id}\`.`);
|
|
2935
3141
|
}
|
|
2936
3142
|
case "rename": {
|
|
2937
|
-
if (!args.id || !args.name) return
|
|
3143
|
+
if (!args.id || !args.name) return fail2("rename requires `id` and `name`.");
|
|
2938
3144
|
await call("rename", { id: args.id, name: args.name });
|
|
2939
|
-
return
|
|
3145
|
+
return text(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2940
3146
|
}
|
|
2941
3147
|
}
|
|
2942
3148
|
} catch (e) {
|
|
2943
|
-
return
|
|
3149
|
+
return fail2(errMsg2(e));
|
|
2944
3150
|
}
|
|
2945
3151
|
}
|
|
2946
3152
|
);
|
|
2947
3153
|
}
|
|
2948
3154
|
|
|
2949
3155
|
// src/tools/packs/scenarios.ts
|
|
2950
|
-
var
|
|
3156
|
+
var import_zod15 = require("zod");
|
|
2951
3157
|
var SCENARIOS_TOOL_ID = "scenarios";
|
|
2952
|
-
var
|
|
2953
|
-
var
|
|
3158
|
+
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3159
|
+
var fail4 = (message) => ({
|
|
2954
3160
|
isError: true,
|
|
2955
3161
|
content: [{ type: "text", text: message }]
|
|
2956
3162
|
});
|
|
2957
|
-
var
|
|
3163
|
+
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
2958
3164
|
function varSpec(v) {
|
|
2959
3165
|
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
2960
3166
|
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
@@ -3016,7 +3222,7 @@ function registerScenariosTools(server, sync) {
|
|
|
3016
3222
|
{
|
|
3017
3223
|
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.",
|
|
3018
3224
|
inputSchema: {
|
|
3019
|
-
deviceId:
|
|
3225
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3020
3226
|
}
|
|
3021
3227
|
},
|
|
3022
3228
|
async (args) => {
|
|
@@ -3046,9 +3252,9 @@ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing r
|
|
|
3046
3252
|
${drafts.map(summarize).join("\n")}`
|
|
3047
3253
|
);
|
|
3048
3254
|
}
|
|
3049
|
-
return
|
|
3255
|
+
return text3(parts.join("\n\n"));
|
|
3050
3256
|
} catch (e) {
|
|
3051
|
-
return
|
|
3257
|
+
return fail4(errMsg4(e));
|
|
3052
3258
|
}
|
|
3053
3259
|
}
|
|
3054
3260
|
);
|
|
@@ -3057,66 +3263,66 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3057
3263
|
{
|
|
3058
3264
|
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.`,
|
|
3059
3265
|
inputSchema: {
|
|
3060
|
-
action:
|
|
3061
|
-
folder:
|
|
3266
|
+
action: import_zod15.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3267
|
+
folder: import_zod15.z.string().optional().describe(
|
|
3062
3268
|
"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."
|
|
3063
3269
|
),
|
|
3064
|
-
id:
|
|
3270
|
+
id: import_zod15.z.string().optional().describe(
|
|
3065
3271
|
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3066
3272
|
),
|
|
3067
|
-
params:
|
|
3273
|
+
params: import_zod15.z.record(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional().describe(
|
|
3068
3274
|
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3069
3275
|
),
|
|
3070
|
-
scenario:
|
|
3071
|
-
id:
|
|
3072
|
-
name:
|
|
3073
|
-
description:
|
|
3074
|
-
expectedOutcome:
|
|
3276
|
+
scenario: import_zod15.z.object({
|
|
3277
|
+
id: import_zod15.z.string().describe("Stable kebab-case id."),
|
|
3278
|
+
name: import_zod15.z.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3279
|
+
description: import_zod15.z.string().optional(),
|
|
3280
|
+
expectedOutcome: import_zod15.z.string().optional().describe(
|
|
3075
3281
|
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3076
3282
|
),
|
|
3077
|
-
author:
|
|
3078
|
-
authoredAgainst:
|
|
3079
|
-
folder:
|
|
3283
|
+
author: import_zod15.z.string().optional(),
|
|
3284
|
+
authoredAgainst: import_zod15.z.string().optional().describe("App version this was written against."),
|
|
3285
|
+
folder: import_zod15.z.string().optional().describe(
|
|
3080
3286
|
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3081
3287
|
),
|
|
3082
|
-
tags:
|
|
3083
|
-
vars:
|
|
3084
|
-
|
|
3085
|
-
key:
|
|
3086
|
-
label:
|
|
3087
|
-
type:
|
|
3088
|
-
options:
|
|
3089
|
-
default:
|
|
3288
|
+
tags: import_zod15.z.array(import_zod15.z.string()).optional(),
|
|
3289
|
+
vars: import_zod15.z.array(
|
|
3290
|
+
import_zod15.z.object({
|
|
3291
|
+
key: import_zod15.z.string(),
|
|
3292
|
+
label: import_zod15.z.string().optional(),
|
|
3293
|
+
type: import_zod15.z.enum(["string", "number", "boolean", "enum"]),
|
|
3294
|
+
options: import_zod15.z.array(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()])).optional(),
|
|
3295
|
+
default: import_zod15.z.union([import_zod15.z.string(), import_zod15.z.number(), import_zod15.z.boolean()]).optional()
|
|
3090
3296
|
})
|
|
3091
3297
|
).optional().describe(
|
|
3092
3298
|
"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."
|
|
3093
3299
|
),
|
|
3094
|
-
steps:
|
|
3095
|
-
|
|
3096
|
-
id:
|
|
3097
|
-
tool:
|
|
3300
|
+
steps: import_zod15.z.array(
|
|
3301
|
+
import_zod15.z.object({
|
|
3302
|
+
id: import_zod15.z.string().optional(),
|
|
3303
|
+
tool: import_zod15.z.string().describe(
|
|
3098
3304
|
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3099
3305
|
),
|
|
3100
|
-
action:
|
|
3306
|
+
action: import_zod15.z.string().describe(
|
|
3101
3307
|
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3102
3308
|
),
|
|
3103
|
-
params:
|
|
3104
|
-
label:
|
|
3309
|
+
params: import_zod15.z.unknown().optional(),
|
|
3310
|
+
label: import_zod15.z.string().optional()
|
|
3105
3311
|
})
|
|
3106
3312
|
).describe("Steps, in order."),
|
|
3107
|
-
undoSteps:
|
|
3108
|
-
|
|
3109
|
-
id:
|
|
3110
|
-
tool:
|
|
3111
|
-
action:
|
|
3112
|
-
params:
|
|
3113
|
-
label:
|
|
3313
|
+
undoSteps: import_zod15.z.array(
|
|
3314
|
+
import_zod15.z.object({
|
|
3315
|
+
id: import_zod15.z.string().optional(),
|
|
3316
|
+
tool: import_zod15.z.string(),
|
|
3317
|
+
action: import_zod15.z.string(),
|
|
3318
|
+
params: import_zod15.z.unknown().optional(),
|
|
3319
|
+
label: import_zod15.z.string().optional()
|
|
3114
3320
|
})
|
|
3115
3321
|
).optional().describe(
|
|
3116
3322
|
"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."
|
|
3117
3323
|
)
|
|
3118
3324
|
}).optional().describe("The scenario to save (action 'save')."),
|
|
3119
|
-
deviceId:
|
|
3325
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3120
3326
|
}
|
|
3121
3327
|
},
|
|
3122
3328
|
async (args) => {
|
|
@@ -3125,7 +3331,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3125
3331
|
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3126
3332
|
switch (args.action) {
|
|
3127
3333
|
case "preview": {
|
|
3128
|
-
if (!args.id) return
|
|
3334
|
+
if (!args.id) return fail4("preview requires `id` (from get_scenarios).");
|
|
3129
3335
|
const r = await call("preview", {
|
|
3130
3336
|
id: args.id,
|
|
3131
3337
|
params: args.params ?? {}
|
|
@@ -3148,16 +3354,16 @@ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
|
3148
3354
|
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3149
3355
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3150
3356
|
);
|
|
3151
|
-
return
|
|
3357
|
+
return text3(lines.join("\n\n"));
|
|
3152
3358
|
}
|
|
3153
3359
|
case "run": {
|
|
3154
|
-
if (!args.id) return
|
|
3360
|
+
if (!args.id) return fail4("run requires `id` (from get_scenarios).");
|
|
3155
3361
|
const r = await call("run", {
|
|
3156
3362
|
id: args.id,
|
|
3157
3363
|
params: args.params ?? {}
|
|
3158
3364
|
});
|
|
3159
3365
|
if (r.preflightErrors?.length) {
|
|
3160
|
-
return
|
|
3366
|
+
return fail4(
|
|
3161
3367
|
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3162
3368
|
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3163
3369
|
);
|
|
@@ -3169,14 +3375,14 @@ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
|
3169
3375
|
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3170
3376
|
}).join("\n");
|
|
3171
3377
|
if (!r.ok) {
|
|
3172
|
-
return
|
|
3378
|
+
return fail4(
|
|
3173
3379
|
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3174
3380
|
${steps}
|
|
3175
3381
|
|
|
3176
3382
|
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3177
3383
|
);
|
|
3178
3384
|
}
|
|
3179
|
-
return
|
|
3385
|
+
return text3(
|
|
3180
3386
|
[
|
|
3181
3387
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3182
3388
|
steps,
|
|
@@ -3200,49 +3406,49 @@ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
|
3200
3406
|
const errs = rep.errors.length ? `
|
|
3201
3407
|
|
|
3202
3408
|
Errors: ${rep.errors.join("; ")}` : "";
|
|
3203
|
-
return
|
|
3409
|
+
return text3(`**Deactivated.**
|
|
3204
3410
|
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3205
3411
|
}
|
|
3206
3412
|
case "save": {
|
|
3207
|
-
if (!args.scenario) return
|
|
3413
|
+
if (!args.scenario) return fail4("save requires `scenario`.");
|
|
3208
3414
|
const r = await call("save", { scenario: args.scenario });
|
|
3209
3415
|
if (!r.ok) {
|
|
3210
|
-
return
|
|
3416
|
+
return fail4(
|
|
3211
3417
|
`Save rejected:
|
|
3212
3418
|
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3213
3419
|
);
|
|
3214
3420
|
}
|
|
3215
|
-
return
|
|
3421
|
+
return text3(
|
|
3216
3422
|
`Saved \`${r.id}\` **as a draft**.
|
|
3217
3423
|
|
|
3218
3424
|
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.`
|
|
3219
3425
|
);
|
|
3220
3426
|
}
|
|
3221
3427
|
case "setFolder": {
|
|
3222
|
-
if (!args.id) return
|
|
3428
|
+
if (!args.id) return fail4("setFolder requires `id` (from get_scenarios).");
|
|
3223
3429
|
const r = await call("setFolder", {
|
|
3224
3430
|
id: args.id,
|
|
3225
3431
|
folder: args.folder
|
|
3226
3432
|
});
|
|
3227
|
-
return
|
|
3433
|
+
return text3(
|
|
3228
3434
|
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3229
3435
|
);
|
|
3230
3436
|
}
|
|
3231
3437
|
case "delete": {
|
|
3232
|
-
if (!args.id) return
|
|
3438
|
+
if (!args.id) return fail4("delete requires `id`.");
|
|
3233
3439
|
await call("delete", { id: args.id });
|
|
3234
|
-
return
|
|
3440
|
+
return text3(`Deleted \`${args.id}\` from the device library.`);
|
|
3235
3441
|
}
|
|
3236
3442
|
}
|
|
3237
3443
|
} catch (e) {
|
|
3238
|
-
return
|
|
3444
|
+
return fail4(errMsg4(e));
|
|
3239
3445
|
}
|
|
3240
3446
|
}
|
|
3241
3447
|
);
|
|
3242
3448
|
}
|
|
3243
3449
|
|
|
3244
3450
|
// src/tools/packs/screenshot.ts
|
|
3245
|
-
var
|
|
3451
|
+
var import_zod16 = require("zod");
|
|
3246
3452
|
|
|
3247
3453
|
// src/capture/simctl.ts
|
|
3248
3454
|
var import_node_child_process = require("child_process");
|
|
@@ -3356,8 +3562,8 @@ function frameToRect(frame, screen) {
|
|
|
3356
3562
|
var TOOL_ID = "highlight-updates";
|
|
3357
3563
|
var MARGIN_PT = 12;
|
|
3358
3564
|
var CAPTURE_PADDING_PT = 6;
|
|
3359
|
-
var
|
|
3360
|
-
var
|
|
3565
|
+
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3566
|
+
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
3361
3567
|
function reasonMessage(res, query) {
|
|
3362
3568
|
switch (res.reason) {
|
|
3363
3569
|
case "no-renders":
|
|
@@ -3387,10 +3593,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
3387
3593
|
{
|
|
3388
3594
|
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.",
|
|
3389
3595
|
inputSchema: {
|
|
3390
|
-
query:
|
|
3391
|
-
nativeTag:
|
|
3392
|
-
scrollIntoView:
|
|
3393
|
-
deviceId:
|
|
3596
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3597
|
+
nativeTag: import_zod16.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3598
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
3599
|
+
deviceId: deviceIdInput
|
|
3394
3600
|
}
|
|
3395
3601
|
},
|
|
3396
3602
|
async (args) => {
|
|
@@ -3432,11 +3638,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3432
3638
|
}
|
|
3433
3639
|
}
|
|
3434
3640
|
if (!located.matched || !located.rect || !located.screen) {
|
|
3435
|
-
return
|
|
3641
|
+
return fail5(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
3436
3642
|
}
|
|
3437
3643
|
const shot = await captureIosFramebuffer();
|
|
3438
3644
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
3439
|
-
return
|
|
3645
|
+
return fail5(shot.error ?? "Screenshot capture failed.");
|
|
3440
3646
|
}
|
|
3441
3647
|
const pxScale = shot.width / located.screen.width;
|
|
3442
3648
|
const r = located.rect;
|
|
@@ -3452,7 +3658,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
3452
3658
|
}
|
|
3453
3659
|
);
|
|
3454
3660
|
if (!crop.ok || !crop.buffer) {
|
|
3455
|
-
return
|
|
3661
|
+
return fail5(crop.error ?? "Crop failed.");
|
|
3456
3662
|
}
|
|
3457
3663
|
const label = located.componentName || "component";
|
|
3458
3664
|
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
@@ -3465,14 +3671,14 @@ function registerScreenshotTools(server, sync) {
|
|
|
3465
3671
|
]
|
|
3466
3672
|
};
|
|
3467
3673
|
} catch (e) {
|
|
3468
|
-
return
|
|
3674
|
+
return fail5(errMsg5(e));
|
|
3469
3675
|
}
|
|
3470
3676
|
}
|
|
3471
3677
|
);
|
|
3472
3678
|
}
|
|
3473
3679
|
|
|
3474
3680
|
// src/tools/packs/camera.ts
|
|
3475
|
-
var
|
|
3681
|
+
var import_zod17 = require("zod");
|
|
3476
3682
|
|
|
3477
3683
|
// src/capture/camera.ts
|
|
3478
3684
|
var import_node_child_process2 = require("child_process");
|
|
@@ -3628,9 +3834,9 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
|
|
|
3628
3834
|
}
|
|
3629
3835
|
|
|
3630
3836
|
// src/tools/packs/camera.ts
|
|
3631
|
-
var
|
|
3632
|
-
var gate = (tool) => readEntitlement().isPro ? null :
|
|
3633
|
-
var
|
|
3837
|
+
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3838
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail6(proRequired(tool));
|
|
3839
|
+
var fail6 = (message) => ({
|
|
3634
3840
|
isError: true,
|
|
3635
3841
|
content: [{ type: "text", text: message }]
|
|
3636
3842
|
});
|
|
@@ -3640,11 +3846,11 @@ function registerCameraTools(server) {
|
|
|
3640
3846
|
{
|
|
3641
3847
|
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.",
|
|
3642
3848
|
inputSchema: {
|
|
3643
|
-
kind:
|
|
3644
|
-
value:
|
|
3849
|
+
kind: import_zod17.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3850
|
+
value: import_zod17.z.string().optional().describe(
|
|
3645
3851
|
"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`."
|
|
3646
3852
|
),
|
|
3647
|
-
format:
|
|
3853
|
+
format: import_zod17.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3648
3854
|
}
|
|
3649
3855
|
},
|
|
3650
3856
|
async (args) => {
|
|
@@ -3655,12 +3861,12 @@ function registerCameraTools(server) {
|
|
|
3655
3861
|
if (args.format) cmd.push("--format", args.format);
|
|
3656
3862
|
const result = await run(cmd);
|
|
3657
3863
|
if (!result.ok) {
|
|
3658
|
-
return
|
|
3864
|
+
return fail6(
|
|
3659
3865
|
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3660
3866
|
);
|
|
3661
3867
|
}
|
|
3662
3868
|
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;
|
|
3663
|
-
return
|
|
3869
|
+
return text4(
|
|
3664
3870
|
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3665
3871
|
|
|
3666
3872
|
_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._`
|
|
@@ -3678,15 +3884,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3678
3884
|
if (blocked) return blocked;
|
|
3679
3885
|
const result = await run(["status"]);
|
|
3680
3886
|
if (!result.ok || !result.data) {
|
|
3681
|
-
return
|
|
3887
|
+
return fail6(result.error ?? "could not read the simulator camera status");
|
|
3682
3888
|
}
|
|
3683
3889
|
const { publishing, fps, codes } = result.data;
|
|
3684
3890
|
if (!publishing) {
|
|
3685
|
-
return
|
|
3891
|
+
return text4(
|
|
3686
3892
|
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3687
3893
|
);
|
|
3688
3894
|
}
|
|
3689
|
-
return
|
|
3895
|
+
return text4(
|
|
3690
3896
|
[
|
|
3691
3897
|
`Publishing at ${fps} fps.`,
|
|
3692
3898
|
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
@@ -3701,8 +3907,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3701
3907
|
{
|
|
3702
3908
|
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.",
|
|
3703
3909
|
inputSchema: {
|
|
3704
|
-
bundleId:
|
|
3705
|
-
udid:
|
|
3910
|
+
bundleId: import_zod17.z.string().describe("The app's bundle identifier."),
|
|
3911
|
+
udid: import_zod17.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3706
3912
|
}
|
|
3707
3913
|
},
|
|
3708
3914
|
async (args) => {
|
|
@@ -3712,9 +3918,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3712
3918
|
if (args.udid) cmd.push("--udid", args.udid);
|
|
3713
3919
|
const result = await run(cmd);
|
|
3714
3920
|
if (!result.ok) {
|
|
3715
|
-
return
|
|
3921
|
+
return fail6(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3716
3922
|
}
|
|
3717
|
-
return
|
|
3923
|
+
return text4(
|
|
3718
3924
|
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3719
3925
|
);
|
|
3720
3926
|
}
|
|
@@ -3724,7 +3930,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3724
3930
|
{
|
|
3725
3931
|
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.",
|
|
3726
3932
|
inputSchema: {
|
|
3727
|
-
bundleId:
|
|
3933
|
+
bundleId: import_zod17.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3728
3934
|
}
|
|
3729
3935
|
},
|
|
3730
3936
|
async (args) => {
|
|
@@ -3732,11 +3938,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3732
3938
|
if (args.bundleId) cmd.push(args.bundleId);
|
|
3733
3939
|
const result = await run(cmd);
|
|
3734
3940
|
if (!result.data?.checks) {
|
|
3735
|
-
return
|
|
3941
|
+
return fail6(result.error ?? "could not run the simulator camera diagnosis");
|
|
3736
3942
|
}
|
|
3737
3943
|
const ent = readEntitlement();
|
|
3738
3944
|
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._";
|
|
3739
|
-
return
|
|
3945
|
+
return text4(describeDiagnosis(result.data.checks) + tierNote);
|
|
3740
3946
|
}
|
|
3741
3947
|
);
|
|
3742
3948
|
server.registerTool(
|
|
@@ -3750,9 +3956,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3750
3956
|
if (blocked) return blocked;
|
|
3751
3957
|
const result = await run(["stop"]);
|
|
3752
3958
|
if (!result.ok) {
|
|
3753
|
-
return
|
|
3959
|
+
return fail6(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3754
3960
|
}
|
|
3755
|
-
return
|
|
3961
|
+
return text4("The simulator camera is off.");
|
|
3756
3962
|
}
|
|
3757
3963
|
);
|
|
3758
3964
|
server.registerTool(
|
|
@@ -3760,7 +3966,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3760
3966
|
{
|
|
3761
3967
|
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.",
|
|
3762
3968
|
inputSchema: {
|
|
3763
|
-
udid:
|
|
3969
|
+
udid: import_zod17.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3764
3970
|
}
|
|
3765
3971
|
},
|
|
3766
3972
|
async (args) => {
|
|
@@ -3768,7 +3974,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3768
3974
|
if (blocked) return blocked;
|
|
3769
3975
|
const devicesResult = await run(["devices"]);
|
|
3770
3976
|
if (!devicesResult.ok || !devicesResult.data) {
|
|
3771
|
-
return
|
|
3977
|
+
return fail6(devicesResult.error ?? "could not list booted simulators");
|
|
3772
3978
|
}
|
|
3773
3979
|
const devices = devicesResult.data.devices ?? [];
|
|
3774
3980
|
let apps = null;
|
|
@@ -3779,7 +3985,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3779
3985
|
]);
|
|
3780
3986
|
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3781
3987
|
}
|
|
3782
|
-
return
|
|
3988
|
+
return text4(describeDevices(devices, apps));
|
|
3783
3989
|
}
|
|
3784
3990
|
);
|
|
3785
3991
|
server.registerTool(
|
|
@@ -3796,11 +4002,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3796
4002
|
run(["windows"])
|
|
3797
4003
|
]);
|
|
3798
4004
|
if (!cams.ok && !wins.ok) {
|
|
3799
|
-
return
|
|
4005
|
+
return fail6(
|
|
3800
4006
|
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3801
4007
|
);
|
|
3802
4008
|
}
|
|
3803
|
-
return
|
|
4009
|
+
return text4(
|
|
3804
4010
|
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3805
4011
|
);
|
|
3806
4012
|
}
|
|
@@ -3810,8 +4016,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3810
4016
|
{
|
|
3811
4017
|
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.",
|
|
3812
4018
|
inputSchema: {
|
|
3813
|
-
action:
|
|
3814
|
-
udid:
|
|
4019
|
+
action: import_zod17.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
4020
|
+
udid: import_zod17.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3815
4021
|
}
|
|
3816
4022
|
},
|
|
3817
4023
|
async (args) => {
|
|
@@ -3819,36 +4025,36 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3819
4025
|
if (blocked) return blocked;
|
|
3820
4026
|
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3821
4027
|
if (!result.ok) {
|
|
3822
|
-
return
|
|
4028
|
+
return fail6(
|
|
3823
4029
|
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3824
4030
|
);
|
|
3825
4031
|
}
|
|
3826
4032
|
const d = result.data ?? {};
|
|
3827
4033
|
if (args.action === "status") {
|
|
3828
|
-
if (!d.enabled) return
|
|
3829
|
-
return
|
|
4034
|
+
if (!d.enabled) return text4("Zero-setup mode is **off**.");
|
|
4035
|
+
return text4(
|
|
3830
4036
|
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."
|
|
3831
4037
|
);
|
|
3832
4038
|
}
|
|
3833
4039
|
if (args.action === "on") {
|
|
3834
|
-
return
|
|
4040
|
+
return text4(
|
|
3835
4041
|
"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._"
|
|
3836
4042
|
);
|
|
3837
4043
|
}
|
|
3838
|
-
return
|
|
4044
|
+
return text4("Zero-setup mode is **off**.");
|
|
3839
4045
|
}
|
|
3840
4046
|
);
|
|
3841
4047
|
}
|
|
3842
4048
|
|
|
3843
4049
|
// src/tools/packs/interact.ts
|
|
3844
|
-
var
|
|
4050
|
+
var import_zod18 = require("zod");
|
|
3845
4051
|
var TOOL_ID2 = "highlight-updates";
|
|
3846
|
-
var
|
|
3847
|
-
var
|
|
4052
|
+
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4053
|
+
var fail7 = (message) => ({
|
|
3848
4054
|
isError: true,
|
|
3849
4055
|
content: [{ type: "text", text: message }]
|
|
3850
4056
|
});
|
|
3851
|
-
var
|
|
4057
|
+
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
3852
4058
|
function formatElement(e) {
|
|
3853
4059
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3854
4060
|
line += e.name;
|
|
@@ -3871,8 +4077,8 @@ function registerInteractTools(server, sync) {
|
|
|
3871
4077
|
{
|
|
3872
4078
|
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.",
|
|
3873
4079
|
inputSchema: {
|
|
3874
|
-
interactiveOnly:
|
|
3875
|
-
deviceId:
|
|
4080
|
+
interactiveOnly: import_zod18.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4081
|
+
deviceId: deviceIdInput
|
|
3876
4082
|
}
|
|
3877
4083
|
},
|
|
3878
4084
|
async (args) => {
|
|
@@ -3882,16 +4088,16 @@ function registerInteractTools(server, sync) {
|
|
|
3882
4088
|
const all = res?.elements ?? [];
|
|
3883
4089
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3884
4090
|
if (elements.length === 0) {
|
|
3885
|
-
return
|
|
4091
|
+
return text5(
|
|
3886
4092
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3887
4093
|
);
|
|
3888
4094
|
}
|
|
3889
4095
|
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 }).`;
|
|
3890
|
-
return
|
|
4096
|
+
return text5(`${header}
|
|
3891
4097
|
|
|
3892
4098
|
${elements.map(formatElement).join("\n")}`);
|
|
3893
4099
|
} catch (e) {
|
|
3894
|
-
return
|
|
4100
|
+
return fail7(errMsg6(e));
|
|
3895
4101
|
}
|
|
3896
4102
|
}
|
|
3897
4103
|
);
|
|
@@ -3900,20 +4106,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3900
4106
|
{
|
|
3901
4107
|
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.",
|
|
3902
4108
|
inputSchema: {
|
|
3903
|
-
nativeTag:
|
|
3904
|
-
testID:
|
|
3905
|
-
query:
|
|
3906
|
-
value:
|
|
3907
|
-
text:
|
|
3908
|
-
longPress:
|
|
3909
|
-
scrollIntoView:
|
|
3910
|
-
deviceId:
|
|
4109
|
+
nativeTag: import_zod18.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4110
|
+
testID: import_zod18.z.string().optional().describe("Exact testID of the element."),
|
|
4111
|
+
query: import_zod18.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
4112
|
+
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)."),
|
|
4113
|
+
text: import_zod18.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4114
|
+
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."),
|
|
4115
|
+
scrollIntoView: import_zod18.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
4116
|
+
deviceId: deviceIdInput
|
|
3911
4117
|
}
|
|
3912
4118
|
},
|
|
3913
4119
|
async (args) => {
|
|
3914
4120
|
try {
|
|
3915
4121
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3916
|
-
return
|
|
4122
|
+
return fail7("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3917
4123
|
}
|
|
3918
4124
|
const target = sync.requireProDevice(args.deviceId);
|
|
3919
4125
|
const res = await sync.callAction(
|
|
@@ -3938,26 +4144,23 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3938
4144
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3939
4145
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3940
4146
|
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}`;
|
|
3941
|
-
return
|
|
4147
|
+
return text5(`${verb}${scrolledNote}.${flowTip}`);
|
|
3942
4148
|
}
|
|
3943
4149
|
let msg = res?.reason ?? "Tap failed.";
|
|
3944
4150
|
if (res?.candidates?.length) {
|
|
3945
4151
|
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");
|
|
3946
4152
|
}
|
|
3947
|
-
return
|
|
4153
|
+
return fail7(msg);
|
|
3948
4154
|
} catch (e) {
|
|
3949
|
-
return
|
|
4155
|
+
return fail7(errMsg6(e));
|
|
3950
4156
|
}
|
|
3951
4157
|
}
|
|
3952
4158
|
);
|
|
3953
4159
|
}
|
|
3954
4160
|
|
|
3955
4161
|
// src/tools/packs/jsThread.ts
|
|
3956
|
-
var
|
|
4162
|
+
var import_zod19 = require("zod");
|
|
3957
4163
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3958
|
-
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3959
|
-
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3960
|
-
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3961
4164
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3962
4165
|
function sparkline(values) {
|
|
3963
4166
|
const max = Math.max(1, ...values);
|
|
@@ -4028,9 +4231,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
4028
4231
|
{
|
|
4029
4232
|
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.',
|
|
4030
4233
|
inputSchema: {
|
|
4031
|
-
deviceId:
|
|
4032
|
-
durationMs:
|
|
4033
|
-
clearFirst:
|
|
4234
|
+
deviceId: deviceIdInput,
|
|
4235
|
+
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4236
|
+
clearFirst: import_zod19.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
4034
4237
|
}
|
|
4035
4238
|
},
|
|
4036
4239
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -4043,7 +4246,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
4043
4246
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
4044
4247
|
target
|
|
4045
4248
|
);
|
|
4046
|
-
if (!snap) return
|
|
4249
|
+
if (!snap) return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
4047
4250
|
const md = formatTopMarkdown(snap, duration);
|
|
4048
4251
|
const slim = {
|
|
4049
4252
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -4062,13 +4265,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
4062
4265
|
worstFreezeMs: r.worstFreezeMs
|
|
4063
4266
|
}))
|
|
4064
4267
|
};
|
|
4065
|
-
return
|
|
4268
|
+
return text(`${md}
|
|
4066
4269
|
|
|
4067
4270
|
\`\`\`json
|
|
4068
4271
|
${JSON.stringify(slim)}
|
|
4069
4272
|
\`\`\``);
|
|
4070
4273
|
} catch (e) {
|
|
4071
|
-
return
|
|
4274
|
+
return fail2(errMsg2(e));
|
|
4072
4275
|
}
|
|
4073
4276
|
}
|
|
4074
4277
|
);
|
|
@@ -4077,8 +4280,8 @@ ${JSON.stringify(slim)}
|
|
|
4077
4280
|
{
|
|
4078
4281
|
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).',
|
|
4079
4282
|
inputSchema: {
|
|
4080
|
-
deviceId:
|
|
4081
|
-
key:
|
|
4283
|
+
deviceId: deviceIdInput,
|
|
4284
|
+
key: import_zod19.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
4082
4285
|
}
|
|
4083
4286
|
},
|
|
4084
4287
|
async ({ deviceId, key }) => {
|
|
@@ -4091,18 +4294,18 @@ ${JSON.stringify(slim)}
|
|
|
4091
4294
|
target
|
|
4092
4295
|
);
|
|
4093
4296
|
if (!detail) {
|
|
4094
|
-
return
|
|
4297
|
+
return fail2(
|
|
4095
4298
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
4096
4299
|
);
|
|
4097
4300
|
}
|
|
4098
4301
|
const md = formatDetailMarkdown(detail);
|
|
4099
|
-
return
|
|
4302
|
+
return text(`${md}
|
|
4100
4303
|
|
|
4101
4304
|
\`\`\`json
|
|
4102
4305
|
${JSON.stringify(detail)}
|
|
4103
4306
|
\`\`\``);
|
|
4104
4307
|
} catch (e) {
|
|
4105
|
-
return
|
|
4308
|
+
return fail2(errMsg2(e));
|
|
4106
4309
|
}
|
|
4107
4310
|
}
|
|
4108
4311
|
);
|
|
@@ -4111,27 +4314,24 @@ ${JSON.stringify(detail)}
|
|
|
4111
4314
|
{
|
|
4112
4315
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
4113
4316
|
inputSchema: {
|
|
4114
|
-
deviceId:
|
|
4317
|
+
deviceId: deviceIdInput
|
|
4115
4318
|
}
|
|
4116
4319
|
},
|
|
4117
4320
|
async ({ deviceId }) => {
|
|
4118
4321
|
try {
|
|
4119
4322
|
const target = sync.requireProDevice(deviceId);
|
|
4120
4323
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
4121
|
-
return
|
|
4324
|
+
return text("Cleared js-top stats.");
|
|
4122
4325
|
} catch (e) {
|
|
4123
|
-
return
|
|
4326
|
+
return fail2(errMsg2(e));
|
|
4124
4327
|
}
|
|
4125
4328
|
}
|
|
4126
4329
|
);
|
|
4127
4330
|
}
|
|
4128
4331
|
|
|
4129
4332
|
// src/tools/packs/images.ts
|
|
4130
|
-
var
|
|
4333
|
+
var import_zod20 = require("zod");
|
|
4131
4334
|
var IMAGES_TOOL_ID = "images";
|
|
4132
|
-
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4133
|
-
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4134
|
-
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
4135
4335
|
function uriTail(uri) {
|
|
4136
4336
|
if (!uri) return "(no source)";
|
|
4137
4337
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -4151,8 +4351,8 @@ function cacheLabel(r) {
|
|
|
4151
4351
|
if (r.progressSeen) return "network";
|
|
4152
4352
|
return "?";
|
|
4153
4353
|
}
|
|
4154
|
-
function mb(
|
|
4155
|
-
return `${(
|
|
4354
|
+
function mb(bytes2) {
|
|
4355
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`;
|
|
4156
4356
|
}
|
|
4157
4357
|
function formatListMarkdown(list, limit) {
|
|
4158
4358
|
const lines = [];
|
|
@@ -4219,9 +4419,9 @@ function registerImagesTools(server, sync) {
|
|
|
4219
4419
|
{
|
|
4220
4420
|
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.",
|
|
4221
4421
|
inputSchema: {
|
|
4222
|
-
deviceId:
|
|
4223
|
-
limit:
|
|
4224
|
-
status:
|
|
4422
|
+
deviceId: deviceIdInput,
|
|
4423
|
+
limit: import_zod20.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4424
|
+
status: import_zod20.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
4225
4425
|
}
|
|
4226
4426
|
},
|
|
4227
4427
|
async ({ deviceId, limit, status }) => {
|
|
@@ -4234,7 +4434,7 @@ function registerImagesTools(server, sync) {
|
|
|
4234
4434
|
{ limit: capped, status },
|
|
4235
4435
|
target
|
|
4236
4436
|
);
|
|
4237
|
-
if (!list) return
|
|
4437
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
4238
4438
|
const md = formatListMarkdown(list, capped);
|
|
4239
4439
|
const slim = {
|
|
4240
4440
|
stats: list.stats,
|
|
@@ -4261,13 +4461,13 @@ function registerImagesTools(server, sync) {
|
|
|
4261
4461
|
layoutShifts: r.layoutShifts
|
|
4262
4462
|
}))
|
|
4263
4463
|
};
|
|
4264
|
-
return
|
|
4464
|
+
return text(`${md}
|
|
4265
4465
|
|
|
4266
4466
|
\`\`\`json
|
|
4267
4467
|
${JSON.stringify(slim)}
|
|
4268
4468
|
\`\`\``);
|
|
4269
4469
|
} catch (e) {
|
|
4270
|
-
return
|
|
4470
|
+
return fail2(errMsg2(e));
|
|
4271
4471
|
}
|
|
4272
4472
|
}
|
|
4273
4473
|
);
|
|
@@ -4276,9 +4476,9 @@ ${JSON.stringify(slim)}
|
|
|
4276
4476
|
{
|
|
4277
4477
|
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.",
|
|
4278
4478
|
inputSchema: {
|
|
4279
|
-
deviceId:
|
|
4280
|
-
id:
|
|
4281
|
-
action:
|
|
4479
|
+
deviceId: deviceIdInput,
|
|
4480
|
+
id: import_zod20.z.number().describe("Record id from get_images."),
|
|
4481
|
+
action: import_zod20.z.enum([
|
|
4282
4482
|
"hard_reload",
|
|
4283
4483
|
"retry",
|
|
4284
4484
|
"evict_disk",
|
|
@@ -4290,7 +4490,7 @@ ${JSON.stringify(slim)}
|
|
|
4290
4490
|
"swap_url",
|
|
4291
4491
|
"restore"
|
|
4292
4492
|
]).describe("What to do."),
|
|
4293
|
-
url:
|
|
4493
|
+
url: import_zod20.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
4294
4494
|
}
|
|
4295
4495
|
},
|
|
4296
4496
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -4298,8 +4498,8 @@ ${JSON.stringify(slim)}
|
|
|
4298
4498
|
const target = sync.requireProDevice(deviceId);
|
|
4299
4499
|
if (action === "prove_savings") {
|
|
4300
4500
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
4301
|
-
if (!savings) return
|
|
4302
|
-
if (!savings.ok) return
|
|
4501
|
+
if (!savings) return fail2("Device returned nothing for proveSavings.");
|
|
4502
|
+
if (!savings.ok) return fail2(savings.message);
|
|
4303
4503
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
4304
4504
|
if (savings.originalBytes != null)
|
|
4305
4505
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -4311,11 +4511,11 @@ ${JSON.stringify(slim)}
|
|
|
4311
4511
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
4312
4512
|
if (savings.optimizedUri)
|
|
4313
4513
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
4314
|
-
return
|
|
4514
|
+
return text(parts.join("\n"));
|
|
4315
4515
|
}
|
|
4316
4516
|
const entry = ACTION_MAP[action];
|
|
4317
|
-
if (!entry) return
|
|
4318
|
-
if (action === "swap_url" && !url) return
|
|
4517
|
+
if (!entry) return fail2(`Unknown action: ${action}`);
|
|
4518
|
+
if (action === "swap_url" && !url) return fail2("swap_url requires the `url` param.");
|
|
4319
4519
|
const result = await sync.callAction(
|
|
4320
4520
|
IMAGES_TOOL_ID,
|
|
4321
4521
|
entry.action,
|
|
@@ -4335,9 +4535,9 @@ ${JSON.stringify(slim)}
|
|
|
4335
4535
|
);
|
|
4336
4536
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
4337
4537
|
}
|
|
4338
|
-
return
|
|
4538
|
+
return text(lines.join("\n"));
|
|
4339
4539
|
} catch (e) {
|
|
4340
|
-
return
|
|
4540
|
+
return fail2(errMsg2(e));
|
|
4341
4541
|
}
|
|
4342
4542
|
}
|
|
4343
4543
|
);
|
|
@@ -4346,12 +4546,12 @@ ${JSON.stringify(slim)}
|
|
|
4346
4546
|
{
|
|
4347
4547
|
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.",
|
|
4348
4548
|
inputSchema: {
|
|
4349
|
-
deviceId:
|
|
4350
|
-
network:
|
|
4351
|
-
blankImages:
|
|
4352
|
-
massAction:
|
|
4353
|
-
clearRegistry:
|
|
4354
|
-
clearExpoCaches:
|
|
4549
|
+
deviceId: deviceIdInput,
|
|
4550
|
+
network: import_zod20.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4551
|
+
blankImages: import_zod20.z.boolean().optional().describe("Render all images without a source."),
|
|
4552
|
+
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."),
|
|
4553
|
+
clearRegistry: import_zod20.z.boolean().optional().describe("Clear the captured image log."),
|
|
4554
|
+
clearExpoCaches: import_zod20.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4355
4555
|
}
|
|
4356
4556
|
},
|
|
4357
4557
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -4378,17 +4578,17 @@ ${JSON.stringify(slim)}
|
|
|
4378
4578
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
4379
4579
|
done.push(r?.message ?? "expo caches cleared");
|
|
4380
4580
|
}
|
|
4381
|
-
if (done.length === 0) return
|
|
4382
|
-
return
|
|
4581
|
+
if (done.length === 0) return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4582
|
+
return text(`Applied: ${done.join(" \xB7 ")}`);
|
|
4383
4583
|
} catch (e) {
|
|
4384
|
-
return
|
|
4584
|
+
return fail2(errMsg2(e));
|
|
4385
4585
|
}
|
|
4386
4586
|
}
|
|
4387
4587
|
);
|
|
4388
4588
|
}
|
|
4389
4589
|
|
|
4390
4590
|
// src/tools/packs/assets.ts
|
|
4391
|
-
var
|
|
4591
|
+
var import_zod21 = require("zod");
|
|
4392
4592
|
|
|
4393
4593
|
// src/tools/packs/assetsGraphNote.ts
|
|
4394
4594
|
function graphUnavailableNote(ss) {
|
|
@@ -4403,14 +4603,11 @@ function graphUnavailableNote(ss) {
|
|
|
4403
4603
|
|
|
4404
4604
|
// src/tools/packs/assets.ts
|
|
4405
4605
|
var ASSETS_TOOL_ID = "assets";
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
4412
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4413
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
4606
|
+
function kb2(bytes2) {
|
|
4607
|
+
if (bytes2 == null || bytes2 <= 0) return "\u2013";
|
|
4608
|
+
if (bytes2 < 1024) return `${bytes2}B`;
|
|
4609
|
+
if (bytes2 < 1024 * 1024) return `${(bytes2 / 1024).toFixed(1)}KB`;
|
|
4610
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(2)}MB`;
|
|
4414
4611
|
}
|
|
4415
4612
|
function sizeLabel(r) {
|
|
4416
4613
|
if (r.sizeBytes == null) return "\u2013";
|
|
@@ -4493,10 +4690,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4493
4690
|
{
|
|
4494
4691
|
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.",
|
|
4495
4692
|
inputSchema: {
|
|
4496
|
-
deviceId:
|
|
4497
|
-
limit:
|
|
4498
|
-
kind:
|
|
4499
|
-
unusedOnly:
|
|
4693
|
+
deviceId: deviceIdInput,
|
|
4694
|
+
limit: import_zod21.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4695
|
+
kind: import_zod21.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4696
|
+
unusedOnly: import_zod21.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4500
4697
|
}
|
|
4501
4698
|
},
|
|
4502
4699
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4509,7 +4706,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4509
4706
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4510
4707
|
target
|
|
4511
4708
|
);
|
|
4512
|
-
if (!list) return
|
|
4709
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4513
4710
|
if (unusedOnly) {
|
|
4514
4711
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4515
4712
|
}
|
|
@@ -4533,13 +4730,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4533
4730
|
sizeSource: r.sizeSource
|
|
4534
4731
|
}))
|
|
4535
4732
|
};
|
|
4536
|
-
return
|
|
4733
|
+
return text(`${md}
|
|
4537
4734
|
|
|
4538
4735
|
\`\`\`json
|
|
4539
4736
|
${JSON.stringify(slim)}
|
|
4540
4737
|
\`\`\``);
|
|
4541
4738
|
} catch (e) {
|
|
4542
|
-
return
|
|
4739
|
+
return fail2(errMsg2(e));
|
|
4543
4740
|
}
|
|
4544
4741
|
}
|
|
4545
4742
|
);
|
|
@@ -4548,9 +4745,9 @@ ${JSON.stringify(slim)}
|
|
|
4548
4745
|
{
|
|
4549
4746
|
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).",
|
|
4550
4747
|
inputSchema: {
|
|
4551
|
-
deviceId:
|
|
4552
|
-
action:
|
|
4553
|
-
id:
|
|
4748
|
+
deviceId: deviceIdInput,
|
|
4749
|
+
action: import_zod21.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4750
|
+
id: import_zod21.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4554
4751
|
}
|
|
4555
4752
|
},
|
|
4556
4753
|
async ({ deviceId, action, id }) => {
|
|
@@ -4560,19 +4757,19 @@ ${JSON.stringify(slim)}
|
|
|
4560
4757
|
case "rescan": {
|
|
4561
4758
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4562
4759
|
const ss = result?.scanStatus;
|
|
4563
|
-
if (!ss) return
|
|
4564
|
-
return
|
|
4760
|
+
if (!ss) return fail2("Device returned nothing for rescan.");
|
|
4761
|
+
return text(
|
|
4565
4762
|
`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" : "")
|
|
4566
4763
|
);
|
|
4567
4764
|
}
|
|
4568
4765
|
case "measure_sizes": {
|
|
4569
4766
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4570
|
-
return
|
|
4767
|
+
return text(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4571
4768
|
}
|
|
4572
4769
|
case "detail": {
|
|
4573
|
-
if (id == null) return
|
|
4770
|
+
if (id == null) return fail2("detail requires the `id` param (from get_assets).");
|
|
4574
4771
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4575
|
-
if (!detail) return
|
|
4772
|
+
if (!detail) return fail2(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4576
4773
|
const lines = [
|
|
4577
4774
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4578
4775
|
"",
|
|
@@ -4580,8 +4777,8 @@ ${JSON.stringify(slim)}
|
|
|
4580
4777
|
];
|
|
4581
4778
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
4582
4779
|
if (detail.sizesByScale) {
|
|
4583
|
-
for (const [scale,
|
|
4584
|
-
lines.push(`- Size @${scale}x: ${kb2(
|
|
4780
|
+
for (const [scale, bytes2] of Object.entries(detail.sizesByScale)) {
|
|
4781
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes2)}`);
|
|
4585
4782
|
}
|
|
4586
4783
|
}
|
|
4587
4784
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
@@ -4590,32 +4787,32 @@ ${JSON.stringify(slim)}
|
|
|
4590
4787
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4591
4788
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4592
4789
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4593
|
-
return
|
|
4790
|
+
return text(lines.join("\n"));
|
|
4594
4791
|
}
|
|
4595
4792
|
case "save_baseline": {
|
|
4596
4793
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4597
|
-
return
|
|
4794
|
+
return text(
|
|
4598
4795
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4599
4796
|
);
|
|
4600
4797
|
}
|
|
4601
4798
|
case "clear_baseline": {
|
|
4602
4799
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4603
|
-
return
|
|
4800
|
+
return text("Baseline cleared.");
|
|
4604
4801
|
}
|
|
4605
4802
|
case "clear_records": {
|
|
4606
4803
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4607
|
-
return
|
|
4804
|
+
return text(result?.message ?? "Inventory cleared.");
|
|
4608
4805
|
}
|
|
4609
4806
|
}
|
|
4610
4807
|
} catch (e) {
|
|
4611
|
-
return
|
|
4808
|
+
return fail2(errMsg2(e));
|
|
4612
4809
|
}
|
|
4613
4810
|
}
|
|
4614
4811
|
);
|
|
4615
4812
|
}
|
|
4616
4813
|
|
|
4617
4814
|
// src/tools/packs/reload.ts
|
|
4618
|
-
var
|
|
4815
|
+
var import_zod22 = require("zod");
|
|
4619
4816
|
|
|
4620
4817
|
// src/capture/devServerReload.ts
|
|
4621
4818
|
var import_ws = require("ws");
|
|
@@ -4678,9 +4875,6 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4678
4875
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4679
4876
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4680
4877
|
var PROBE_INTERVAL_MS = 400;
|
|
4681
|
-
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4682
|
-
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4683
|
-
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4684
4878
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4685
4879
|
async function ping(sync, deviceId, timeoutMs) {
|
|
4686
4880
|
try {
|
|
@@ -4719,12 +4913,12 @@ function registerReloadTools(server, sync) {
|
|
|
4719
4913
|
{
|
|
4720
4914
|
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.",
|
|
4721
4915
|
inputSchema: {
|
|
4722
|
-
strategy:
|
|
4916
|
+
strategy: import_zod22.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4723
4917
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4724
4918
|
),
|
|
4725
|
-
wait:
|
|
4726
|
-
timeoutMs:
|
|
4727
|
-
deviceId:
|
|
4919
|
+
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."),
|
|
4920
|
+
timeoutMs: import_zod22.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4921
|
+
deviceId: import_zod22.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4728
4922
|
}
|
|
4729
4923
|
},
|
|
4730
4924
|
async (args) => {
|
|
@@ -4766,39 +4960,39 @@ function registerReloadTools(server, sync) {
|
|
|
4766
4960
|
}
|
|
4767
4961
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4768
4962
|
if (args.wait === false) {
|
|
4769
|
-
return
|
|
4963
|
+
return text(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4770
4964
|
}
|
|
4771
4965
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4772
4966
|
if (elapsed == null) {
|
|
4773
|
-
return
|
|
4967
|
+
return fail2(
|
|
4774
4968
|
`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.`
|
|
4775
4969
|
);
|
|
4776
4970
|
}
|
|
4777
|
-
return
|
|
4971
|
+
return text(
|
|
4778
4972
|
`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.`
|
|
4779
4973
|
);
|
|
4780
4974
|
} catch (e) {
|
|
4781
|
-
return
|
|
4975
|
+
return fail2(errMsg2(e));
|
|
4782
4976
|
}
|
|
4783
4977
|
}
|
|
4784
4978
|
);
|
|
4785
4979
|
}
|
|
4786
4980
|
|
|
4787
4981
|
// src/tools/packs/triage.ts
|
|
4788
|
-
var
|
|
4789
|
-
var
|
|
4790
|
-
var
|
|
4982
|
+
var import_zod23 = require("zod");
|
|
4983
|
+
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4984
|
+
var fail8 = (message) => ({
|
|
4791
4985
|
isError: true,
|
|
4792
4986
|
content: [{ type: "text", text: message }]
|
|
4793
4987
|
});
|
|
4794
|
-
var
|
|
4988
|
+
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
4795
4989
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4796
4990
|
var MAX_STORAGE_KEYS = 20;
|
|
4797
4991
|
var MAX_STORAGE_VALUE = 400;
|
|
4798
4992
|
var STORAGE_VALUE_BUDGET = 1600;
|
|
4799
4993
|
var MAX_LINE = 160;
|
|
4800
4994
|
var cap = (s, n = MAX_LINE) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
4801
|
-
function
|
|
4995
|
+
function hhmmss3(ts) {
|
|
4802
4996
|
if (!ts) return "--:--:--";
|
|
4803
4997
|
const d = new Date(ts);
|
|
4804
4998
|
const p = (n) => String(n).padStart(2, "0");
|
|
@@ -4828,7 +5022,7 @@ async function section(title, run2) {
|
|
|
4828
5022
|
return body ? `${title}
|
|
4829
5023
|
${body}` : null;
|
|
4830
5024
|
} catch (e) {
|
|
4831
|
-
const msg =
|
|
5025
|
+
const msg = errMsg7(e);
|
|
4832
5026
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4833
5027
|
return `${title}
|
|
4834
5028
|
(no answer \u2014 see the note above)`;
|
|
@@ -4843,7 +5037,7 @@ function registerTriageTools(server, sync) {
|
|
|
4843
5037
|
{
|
|
4844
5038
|
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.",
|
|
4845
5039
|
inputSchema: {
|
|
4846
|
-
deviceId:
|
|
5040
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4847
5041
|
}
|
|
4848
5042
|
},
|
|
4849
5043
|
async (args) => {
|
|
@@ -4867,7 +5061,7 @@ function registerTriageTools(server, sync) {
|
|
|
4867
5061
|
return `(none \u2014 ${entries.length} entries captured)`;
|
|
4868
5062
|
}
|
|
4869
5063
|
return shown.map(
|
|
4870
|
-
(e) => `${
|
|
5064
|
+
(e) => `${hhmmss3(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
|
|
4871
5065
|
(e.message ?? "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).join(" \u23CE "),
|
|
4872
5066
|
220
|
|
4873
5067
|
)}`
|
|
@@ -4950,7 +5144,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4950
5144
|
|
|
4951
5145
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4952
5146
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4953
|
-
return
|
|
5147
|
+
return text6(
|
|
4954
5148
|
`${head}${banner}
|
|
4955
5149
|
|
|
4956
5150
|
${body}
|
|
@@ -4958,22 +5152,22 @@ ${body}
|
|
|
4958
5152
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4959
5153
|
);
|
|
4960
5154
|
} catch (e) {
|
|
4961
|
-
return
|
|
5155
|
+
return fail8(errMsg7(e));
|
|
4962
5156
|
}
|
|
4963
5157
|
}
|
|
4964
5158
|
);
|
|
4965
5159
|
}
|
|
4966
5160
|
|
|
4967
5161
|
// src/tools/packs/flow.ts
|
|
4968
|
-
var
|
|
5162
|
+
var import_zod24 = require("zod");
|
|
4969
5163
|
var UI_TOOL_ID = "highlight-updates";
|
|
4970
5164
|
var APP_TOOL_ID2 = "app";
|
|
4971
|
-
var
|
|
4972
|
-
var
|
|
5165
|
+
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5166
|
+
var fail9 = (message) => ({
|
|
4973
5167
|
isError: true,
|
|
4974
5168
|
content: [{ type: "text", text: message }]
|
|
4975
5169
|
});
|
|
4976
|
-
var
|
|
5170
|
+
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
4977
5171
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4978
5172
|
var POLL_MS = 250;
|
|
4979
5173
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4990,19 +5184,19 @@ function formatElement2(e) {
|
|
|
4990
5184
|
const display = e.text ?? e.label;
|
|
4991
5185
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4992
5186
|
}
|
|
4993
|
-
var stepSchema =
|
|
4994
|
-
tap:
|
|
4995
|
-
longPress:
|
|
4996
|
-
type:
|
|
4997
|
-
into:
|
|
4998
|
-
set:
|
|
4999
|
-
reload:
|
|
5000
|
-
waitMs:
|
|
5001
|
-
waitFor:
|
|
5002
|
-
waitGone:
|
|
5003
|
-
expect:
|
|
5004
|
-
expectGone:
|
|
5005
|
-
timeoutMs:
|
|
5187
|
+
var stepSchema = import_zod24.z.object({
|
|
5188
|
+
tap: import_zod24.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5189
|
+
longPress: import_zod24.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
5190
|
+
type: import_zod24.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5191
|
+
into: import_zod24.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5192
|
+
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."),
|
|
5193
|
+
reload: import_zod24.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5194
|
+
waitMs: import_zod24.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5195
|
+
waitFor: import_zod24.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5196
|
+
waitGone: import_zod24.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5197
|
+
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."),
|
|
5198
|
+
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."),
|
|
5199
|
+
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).")
|
|
5006
5200
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
5007
5201
|
function registerFlowTools(server, sync) {
|
|
5008
5202
|
server.registerTool(
|
|
@@ -5010,10 +5204,10 @@ function registerFlowTools(server, sync) {
|
|
|
5010
5204
|
{
|
|
5011
5205
|
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.",
|
|
5012
5206
|
inputSchema: {
|
|
5013
|
-
steps:
|
|
5014
|
-
include:
|
|
5015
|
-
continueOnFailure:
|
|
5016
|
-
deviceId:
|
|
5207
|
+
steps: import_zod24.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5208
|
+
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."),
|
|
5209
|
+
continueOnFailure: import_zod24.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5210
|
+
deviceId: deviceIdInput
|
|
5017
5211
|
}
|
|
5018
5212
|
},
|
|
5019
5213
|
async (args) => {
|
|
@@ -5185,7 +5379,7 @@ function registerFlowTools(server, sync) {
|
|
|
5185
5379
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
5186
5380
|
}
|
|
5187
5381
|
} catch (e) {
|
|
5188
|
-
stepErrors.push(
|
|
5382
|
+
stepErrors.push(errMsg8(e));
|
|
5189
5383
|
}
|
|
5190
5384
|
if (stepErrors.length) {
|
|
5191
5385
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -5208,7 +5402,7 @@ function registerFlowTools(server, sync) {
|
|
|
5208
5402
|
${shown.map(formatElement2).join("\n")}`
|
|
5209
5403
|
);
|
|
5210
5404
|
} catch (e) {
|
|
5211
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
5405
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(e))}`);
|
|
5212
5406
|
}
|
|
5213
5407
|
}
|
|
5214
5408
|
if (include === "all") {
|
|
@@ -5237,9 +5431,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
5237
5431
|
}
|
|
5238
5432
|
}
|
|
5239
5433
|
}
|
|
5240
|
-
return
|
|
5434
|
+
return text7(parts.join("\n\n"));
|
|
5241
5435
|
} catch (e) {
|
|
5242
|
-
return
|
|
5436
|
+
return fail9(errMsg8(e));
|
|
5243
5437
|
}
|
|
5244
5438
|
}
|
|
5245
5439
|
);
|
|
@@ -5260,25 +5454,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
5260
5454
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
5261
5455
|
|
|
5262
5456
|
// src/tools/packs/renders.ts
|
|
5263
|
-
var
|
|
5457
|
+
var import_zod25 = require("zod");
|
|
5264
5458
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
5265
|
-
var
|
|
5266
|
-
var
|
|
5459
|
+
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5460
|
+
var fail10 = (message) => ({
|
|
5267
5461
|
isError: true,
|
|
5268
5462
|
content: [{ type: "text", text: message }]
|
|
5269
5463
|
});
|
|
5270
|
-
var
|
|
5464
|
+
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
5271
5465
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5272
5466
|
var DEFAULT_IDLE_MS = 3e3;
|
|
5273
5467
|
var SETTLE_MS = 400;
|
|
5274
5468
|
var DEFAULT_TOP = 12;
|
|
5275
5469
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
5276
|
-
var stepSchema2 =
|
|
5277
|
-
tap:
|
|
5278
|
-
type:
|
|
5279
|
-
into:
|
|
5280
|
-
waitMs:
|
|
5281
|
-
repeat:
|
|
5470
|
+
var stepSchema2 = import_zod25.z.object({
|
|
5471
|
+
tap: import_zod25.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5472
|
+
type: import_zod25.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5473
|
+
into: import_zod25.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5474
|
+
waitMs: import_zod25.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5475
|
+
repeat: import_zod25.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
5282
5476
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
5283
5477
|
function selector2(needle) {
|
|
5284
5478
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -5371,14 +5565,14 @@ function registerRenderTools(server, sync) {
|
|
|
5371
5565
|
{
|
|
5372
5566
|
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.",
|
|
5373
5567
|
inputSchema: {
|
|
5374
|
-
steps:
|
|
5568
|
+
steps: import_zod25.z.array(stepSchema2).max(20).optional().describe(
|
|
5375
5569
|
"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."
|
|
5376
5570
|
),
|
|
5377
|
-
idleMs:
|
|
5378
|
-
label:
|
|
5379
|
-
compareToPrevious:
|
|
5380
|
-
top:
|
|
5381
|
-
deviceId:
|
|
5571
|
+
idleMs: import_zod25.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5572
|
+
label: import_zod25.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5573
|
+
compareToPrevious: import_zod25.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5574
|
+
top: import_zod25.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5575
|
+
deviceId: deviceIdInput
|
|
5382
5576
|
}
|
|
5383
5577
|
},
|
|
5384
5578
|
async (args) => {
|
|
@@ -5387,18 +5581,18 @@ function registerRenderTools(server, sync) {
|
|
|
5387
5581
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
5388
5582
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
5389
5583
|
if (!uiTool) {
|
|
5390
|
-
return
|
|
5584
|
+
return fail10(
|
|
5391
5585
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
5392
5586
|
);
|
|
5393
5587
|
}
|
|
5394
5588
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
5395
|
-
return
|
|
5589
|
+
return fail10(
|
|
5396
5590
|
"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."
|
|
5397
5591
|
);
|
|
5398
5592
|
}
|
|
5399
5593
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
5400
5594
|
if (began && began.ok === false) {
|
|
5401
|
-
return
|
|
5595
|
+
return fail10(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
5402
5596
|
}
|
|
5403
5597
|
await delay5(SETTLE_MS);
|
|
5404
5598
|
const startedAt = Date.now();
|
|
@@ -5443,7 +5637,7 @@ function registerRenderTools(server, sync) {
|
|
|
5443
5637
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5444
5638
|
const summary = ended?.summary;
|
|
5445
5639
|
if (!summary) {
|
|
5446
|
-
return
|
|
5640
|
+
return fail10(
|
|
5447
5641
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5448
5642
|
);
|
|
5449
5643
|
}
|
|
@@ -5468,9 +5662,9 @@ function registerRenderTools(server, sync) {
|
|
|
5468
5662
|
} else if (!args.compareToPrevious) {
|
|
5469
5663
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5470
5664
|
}
|
|
5471
|
-
return
|
|
5665
|
+
return text8(parts.join("\n"));
|
|
5472
5666
|
} catch (e) {
|
|
5473
|
-
return
|
|
5667
|
+
return fail10(errMsg9(e));
|
|
5474
5668
|
}
|
|
5475
5669
|
}
|
|
5476
5670
|
);
|
|
@@ -5754,6 +5948,7 @@ function createMcpServer(sync) {
|
|
|
5754
5948
|
registerStorageTools(server, sync);
|
|
5755
5949
|
registerRoutesTools(server, sync);
|
|
5756
5950
|
registerConsoleTools(server, sync);
|
|
5951
|
+
registerSentryTools(server, sync);
|
|
5757
5952
|
registerTimeMachineTools(server, sync);
|
|
5758
5953
|
registerScenariosTools(server, sync);
|
|
5759
5954
|
registerScreenshotTools(server, sync);
|
|
@@ -5766,7 +5961,7 @@ function createMcpServer(sync) {
|
|
|
5766
5961
|
return server;
|
|
5767
5962
|
}
|
|
5768
5963
|
var deviceIdField = {
|
|
5769
|
-
deviceId:
|
|
5964
|
+
deviceId: import_zod26.z.string().optional().describe(
|
|
5770
5965
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5771
5966
|
)
|
|
5772
5967
|
};
|