@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
|
@@ -226,6 +226,7 @@ var SyncClient = class {
|
|
|
226
226
|
isConnected: d.isConnected !== false,
|
|
227
227
|
isPro: d.isPro === true,
|
|
228
228
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
229
|
+
collision: d.collision === true,
|
|
229
230
|
tools: caps?.tools ?? []
|
|
230
231
|
};
|
|
231
232
|
});
|
|
@@ -613,7 +614,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
613
614
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
614
615
|
|
|
615
616
|
// src/version.ts
|
|
616
|
-
var VERSION = "7.0.
|
|
617
|
+
var VERSION = "7.0.34" ? "7.0.34" : "0.0.0-dev";
|
|
617
618
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
618
619
|
|
|
619
620
|
// src/update/checkForUpdate.ts
|
|
@@ -711,8 +712,12 @@ function registerGenericTools(server, sync) {
|
|
|
711
712
|
const connected = devices.filter((d) => d.isConnected);
|
|
712
713
|
const anyPro = connected.some((d) => d.isPro);
|
|
713
714
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
715
|
+
const collided = connected.filter((d) => d.collision);
|
|
714
716
|
return json({
|
|
715
717
|
devices,
|
|
718
|
+
...collided.length > 0 ? {
|
|
719
|
+
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.`
|
|
720
|
+
} : {},
|
|
716
721
|
...anyPro ? {} : {
|
|
717
722
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
718
723
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -1096,7 +1101,40 @@ function round(n, d) {
|
|
|
1096
1101
|
}
|
|
1097
1102
|
|
|
1098
1103
|
// src/tools/packs/perfMonitor.ts
|
|
1104
|
+
import { z as z3 } from "zod";
|
|
1105
|
+
|
|
1106
|
+
// src/tools/packKit.ts
|
|
1099
1107
|
import { z as z2 } from "zod";
|
|
1108
|
+
var text = (s) => ({
|
|
1109
|
+
content: [{ type: "text", text: s }]
|
|
1110
|
+
});
|
|
1111
|
+
var fail2 = (message) => ({
|
|
1112
|
+
isError: true,
|
|
1113
|
+
content: [{ type: "text", text: message }]
|
|
1114
|
+
});
|
|
1115
|
+
var errMsg2 = (e) => e instanceof Error ? e.message : String(e);
|
|
1116
|
+
var deviceIdInput = z2.string().optional().describe("Target device id (auto-selected if one is connected).");
|
|
1117
|
+
function clampLimit(value, fallback) {
|
|
1118
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
1119
|
+
}
|
|
1120
|
+
function staleHeader(stale) {
|
|
1121
|
+
return stale ? `${stale}
|
|
1122
|
+
|
|
1123
|
+
` : "";
|
|
1124
|
+
}
|
|
1125
|
+
function inactiveToolMessage(options) {
|
|
1126
|
+
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).`;
|
|
1127
|
+
}
|
|
1128
|
+
function withDevice(sync, run2) {
|
|
1129
|
+
return async (args) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
1132
|
+
return await run2(args, target);
|
|
1133
|
+
} catch (e) {
|
|
1134
|
+
return fail2(errMsg2(e));
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1100
1138
|
|
|
1101
1139
|
// src/tools/packs/perfMonitorTypes.ts
|
|
1102
1140
|
var PERF_TOOL_ID = "perf-monitor";
|
|
@@ -1105,13 +1143,10 @@ var PERF_TOOL_ID = "perf-monitor";
|
|
|
1105
1143
|
var POLL_INTERVAL_MS = 1500;
|
|
1106
1144
|
var delay2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1107
1145
|
var json2 = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
1108
|
-
var
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
name: z2.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1113
|
-
params: z2.record(z2.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
|
|
1114
|
-
route: z2.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1146
|
+
var caseSchema = z3.object({
|
|
1147
|
+
name: z3.string().describe("Display name for this variant (becomes the saved run's name)."),
|
|
1148
|
+
params: z3.record(z3.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),
|
|
1149
|
+
route: z3.string().optional().describe("Optional per-case route override; defaults to targetRoute.")
|
|
1115
1150
|
});
|
|
1116
1151
|
async function readDeviceSettings(sync, deviceId) {
|
|
1117
1152
|
try {
|
|
@@ -1230,22 +1265,22 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1230
1265
|
{
|
|
1231
1266
|
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.",
|
|
1232
1267
|
inputSchema: {
|
|
1233
|
-
targetRoute:
|
|
1234
|
-
cases:
|
|
1235
|
-
bounceRoute:
|
|
1236
|
-
runsPerCase:
|
|
1237
|
-
perCaseDurationMs:
|
|
1238
|
-
settleMs:
|
|
1239
|
-
coolDownMs:
|
|
1240
|
-
navTimeoutMs:
|
|
1241
|
-
discardWarmupRuns:
|
|
1242
|
-
shuffleCases:
|
|
1243
|
-
reloadBetweenCases:
|
|
1244
|
-
captureRenders:
|
|
1268
|
+
targetRoute: z3.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1269
|
+
cases: z3.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
1270
|
+
bounceRoute: z3.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),
|
|
1271
|
+
runsPerCase: z3.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),
|
|
1272
|
+
perCaseDurationMs: z3.number().optional().describe("How long to record each run. Default 5000."),
|
|
1273
|
+
settleMs: z3.number().optional().describe("Idle gap after navigation before recording. Default 600."),
|
|
1274
|
+
coolDownMs: z3.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1275
|
+
navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1276
|
+
discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1277
|
+
shuffleCases: z3.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias. Default on for >3 cases."),
|
|
1278
|
+
reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1279
|
+
captureRenders: z3.boolean().optional().describe(
|
|
1245
1280
|
"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."
|
|
1246
1281
|
),
|
|
1247
|
-
deviceId:
|
|
1248
|
-
maxWaitMs:
|
|
1282
|
+
deviceId: z3.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1283
|
+
maxWaitMs: z3.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
1249
1284
|
}
|
|
1250
1285
|
},
|
|
1251
1286
|
async (args) => {
|
|
@@ -1315,7 +1350,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1315
1350
|
"get_automation_status",
|
|
1316
1351
|
{
|
|
1317
1352
|
description: "Get the perf-monitor automation batch status (current phase + last completed batch) without waiting.",
|
|
1318
|
-
inputSchema: { deviceId:
|
|
1353
|
+
inputSchema: { deviceId: z3.string().optional().describe("Target device id (auto-selected if one is connected).") }
|
|
1319
1354
|
},
|
|
1320
1355
|
async ({ deviceId }) => {
|
|
1321
1356
|
try {
|
|
@@ -1336,8 +1371,8 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1336
1371
|
{
|
|
1337
1372
|
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).",
|
|
1338
1373
|
inputSchema: {
|
|
1339
|
-
deviceId:
|
|
1340
|
-
maxWaitMs:
|
|
1374
|
+
deviceId: deviceIdInput,
|
|
1375
|
+
maxWaitMs: z3.number().optional().describe("Max time to wait. Default 600000 (10 min).")
|
|
1341
1376
|
}
|
|
1342
1377
|
},
|
|
1343
1378
|
async ({ deviceId, maxWaitMs }) => {
|
|
@@ -1410,8 +1445,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1410
1445
|
{
|
|
1411
1446
|
description: "Re-rank a previously-run benchmark batch by its batchId (from the saved recordings index).",
|
|
1412
1447
|
inputSchema: {
|
|
1413
|
-
batchId:
|
|
1414
|
-
deviceId:
|
|
1448
|
+
batchId: z3.string().describe("The batch id to report on."),
|
|
1449
|
+
deviceId: deviceIdInput
|
|
1415
1450
|
}
|
|
1416
1451
|
},
|
|
1417
1452
|
async ({ batchId, deviceId }) => {
|
|
@@ -1438,9 +1473,9 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1438
1473
|
{
|
|
1439
1474
|
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.",
|
|
1440
1475
|
inputSchema: {
|
|
1441
|
-
reportIdA:
|
|
1442
|
-
reportIdB:
|
|
1443
|
-
deviceId:
|
|
1476
|
+
reportIdA: z3.string().describe("First (baseline) report id."),
|
|
1477
|
+
reportIdB: z3.string().describe("Second report id to compare against the baseline."),
|
|
1478
|
+
deviceId: deviceIdInput
|
|
1444
1479
|
}
|
|
1445
1480
|
},
|
|
1446
1481
|
async ({ reportIdA, reportIdB, deviceId }) => {
|
|
@@ -1468,8 +1503,8 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1468
1503
|
{
|
|
1469
1504
|
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.",
|
|
1470
1505
|
inputSchema: {
|
|
1471
|
-
pages:
|
|
1472
|
-
deviceId:
|
|
1506
|
+
pages: z3.number().optional().describe("How many pages/variants you plan to test, for a total-time estimate. Default 1."),
|
|
1507
|
+
deviceId: deviceIdInput
|
|
1473
1508
|
}
|
|
1474
1509
|
},
|
|
1475
1510
|
async ({ pages, deviceId }) => {
|
|
@@ -1499,16 +1534,16 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1499
1534
|
{
|
|
1500
1535
|
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.`,
|
|
1501
1536
|
inputSchema: {
|
|
1502
|
-
perCaseDurationMs:
|
|
1503
|
-
settleMs:
|
|
1504
|
-
coolDownMs:
|
|
1505
|
-
navTimeoutMs:
|
|
1506
|
-
runsPerCase:
|
|
1507
|
-
discardWarmupRuns:
|
|
1508
|
-
shuffleCases:
|
|
1509
|
-
reloadBetweenCases:
|
|
1510
|
-
captureRenders:
|
|
1511
|
-
deviceId:
|
|
1537
|
+
perCaseDurationMs: z3.number().optional().describe("Recording length per page (ms)."),
|
|
1538
|
+
settleMs: z3.number().optional().describe("Idle after navigation before recording (ms)."),
|
|
1539
|
+
coolDownMs: z3.number().optional().describe("Idle between runs/cases (ms)."),
|
|
1540
|
+
navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1541
|
+
runsPerCase: z3.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1542
|
+
discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1543
|
+
shuffleCases: z3.boolean().optional().describe("Interleave + shuffle case order to cancel thermal bias."),
|
|
1544
|
+
reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1545
|
+
captureRenders: z3.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1546
|
+
deviceId: deviceIdInput
|
|
1512
1547
|
}
|
|
1513
1548
|
},
|
|
1514
1549
|
async (args) => {
|
|
@@ -1556,11 +1591,8 @@ ${JSON.stringify(saved ?? settings, null, 2)}
|
|
|
1556
1591
|
}
|
|
1557
1592
|
|
|
1558
1593
|
// src/tools/packs/events.ts
|
|
1559
|
-
import { z as
|
|
1594
|
+
import { z as z4 } from "zod";
|
|
1560
1595
|
var EVENTS_TOOL_ID = "events";
|
|
1561
|
-
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1562
|
-
var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1563
|
-
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1564
1596
|
var SOURCE_ALIASES = {
|
|
1565
1597
|
storage: ["storage-async", "storage-mmkv"],
|
|
1566
1598
|
"react-query": ["react-query", "react-query-query", "react-query-mutation"]
|
|
@@ -1575,13 +1607,13 @@ function registerEventsTools(server, sync) {
|
|
|
1575
1607
|
{
|
|
1576
1608
|
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.",
|
|
1577
1609
|
inputSchema: {
|
|
1578
|
-
limit:
|
|
1579
|
-
sources:
|
|
1580
|
-
status:
|
|
1581
|
-
includeData:
|
|
1582
|
-
format:
|
|
1583
|
-
preset:
|
|
1584
|
-
deviceId:
|
|
1610
|
+
limit: z4.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1611
|
+
sources: z4.array(z4.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
|
|
1612
|
+
status: z4.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by status. Default all."),
|
|
1613
|
+
includeData: z4.boolean().optional().describe("Include each event's full raw payload (HEAVY \u2014 network bodies, state trees). Default false (compact)."),
|
|
1614
|
+
format: z4.enum(["markdown", "json", "text", "mermaid"]).optional().describe("Output format. Default markdown."),
|
|
1615
|
+
preset: z4.enum(["llm", "bugReport", "json", "errors", "minimal", "mermaid"]).optional().describe("Start from a named Copy-Settings preset (then your other args override it)."),
|
|
1616
|
+
deviceId: deviceIdInput
|
|
1585
1617
|
}
|
|
1586
1618
|
},
|
|
1587
1619
|
async (args) => {
|
|
@@ -1600,7 +1632,7 @@ function registerEventsTools(server, sync) {
|
|
|
1600
1632
|
target
|
|
1601
1633
|
);
|
|
1602
1634
|
if (!res || typeof res.output !== "string") {
|
|
1603
|
-
return
|
|
1635
|
+
return fail2(
|
|
1604
1636
|
"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)."
|
|
1605
1637
|
);
|
|
1606
1638
|
}
|
|
@@ -1608,26 +1640,23 @@ function registerEventsTools(server, sync) {
|
|
|
1608
1640
|
const note = `
|
|
1609
1641
|
|
|
1610
1642
|
_Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
|
|
1611
|
-
return
|
|
1643
|
+
return text((res.output || "(no events captured)") + note);
|
|
1612
1644
|
} catch (e) {
|
|
1613
|
-
return
|
|
1645
|
+
return fail2(errMsg2(e));
|
|
1614
1646
|
}
|
|
1615
1647
|
}
|
|
1616
1648
|
);
|
|
1617
1649
|
}
|
|
1618
1650
|
|
|
1619
1651
|
// src/tools/packs/reactQuery.ts
|
|
1620
|
-
import { z as
|
|
1652
|
+
import { z as z5 } from "zod";
|
|
1621
1653
|
var RQ_TOOL_ID = "query";
|
|
1622
1654
|
var RQ_TOOL_ID_LEGACY = "react-query";
|
|
1623
|
-
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1624
|
-
var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1625
|
-
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1626
1655
|
async function callQueryAction(sync, action, params, target) {
|
|
1627
1656
|
try {
|
|
1628
1657
|
return await sync.callAction(RQ_TOOL_ID, action, params, target);
|
|
1629
1658
|
} catch (e) {
|
|
1630
|
-
const msg =
|
|
1659
|
+
const msg = errMsg2(e);
|
|
1631
1660
|
if (!/unknown tool/i.test(msg)) throw e;
|
|
1632
1661
|
return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
|
|
1633
1662
|
}
|
|
@@ -1655,10 +1684,10 @@ function registerReactQueryTools(server, sync) {
|
|
|
1655
1684
|
{
|
|
1656
1685
|
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.",
|
|
1657
1686
|
inputSchema: {
|
|
1658
|
-
includeData:
|
|
1659
|
-
staleOnly:
|
|
1660
|
-
limit:
|
|
1661
|
-
deviceId:
|
|
1687
|
+
includeData: z5.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
1688
|
+
staleOnly: z5.boolean().optional().describe("Only stale queries. Default false."),
|
|
1689
|
+
limit: z5.number().optional().describe("Most-recently-updated N queries. Default 25."),
|
|
1690
|
+
deviceId: deviceIdInput
|
|
1662
1691
|
}
|
|
1663
1692
|
},
|
|
1664
1693
|
async (args) => {
|
|
@@ -1671,12 +1700,12 @@ function registerReactQueryTools(server, sync) {
|
|
|
1671
1700
|
target
|
|
1672
1701
|
);
|
|
1673
1702
|
if (!res || !Array.isArray(res.queries)) {
|
|
1674
|
-
return
|
|
1703
|
+
return fail2(
|
|
1675
1704
|
"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)."
|
|
1676
1705
|
);
|
|
1677
1706
|
}
|
|
1678
1707
|
if (res.queries.length === 0) {
|
|
1679
|
-
return
|
|
1708
|
+
return text(`React Query cache is empty (0 of ${res.total} queries${args.staleOnly ? " match staleOnly" : ""}).`);
|
|
1680
1709
|
}
|
|
1681
1710
|
const lines = res.queries.map((q, i) => {
|
|
1682
1711
|
const icon = STATUS_ICON[q.status] ?? "\u2022";
|
|
@@ -1689,11 +1718,11 @@ function registerReactQueryTools(server, sync) {
|
|
|
1689
1718
|
const staleCount = res.queries.filter((q) => q.isStale).length;
|
|
1690
1719
|
const header = `**React Query cache \u2014 ${res.total} queries (showing ${res.returned}, ${staleCount} stale):**`;
|
|
1691
1720
|
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._";
|
|
1692
|
-
return
|
|
1721
|
+
return text(`${header}
|
|
1693
1722
|
|
|
1694
1723
|
${lines.join("\n")}${note}`);
|
|
1695
1724
|
} catch (e) {
|
|
1696
|
-
return
|
|
1725
|
+
return fail2(errMsg2(e));
|
|
1697
1726
|
}
|
|
1698
1727
|
}
|
|
1699
1728
|
);
|
|
@@ -1702,9 +1731,9 @@ ${lines.join("\n")}${note}`);
|
|
|
1702
1731
|
{
|
|
1703
1732
|
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.",
|
|
1704
1733
|
inputSchema: {
|
|
1705
|
-
action:
|
|
1706
|
-
queryHash:
|
|
1707
|
-
deviceId:
|
|
1734
|
+
action: z5.enum(["invalidate", "refetch", "reset", "remove", "clear"]).describe("invalidate/refetch/reset/remove a single query (needs queryHash); clear empties the whole query cache."),
|
|
1735
|
+
queryHash: z5.string().optional().describe("The target query's hash (from get_react_query, shown in backticks). Required for all actions except clear."),
|
|
1736
|
+
deviceId: deviceIdInput
|
|
1708
1737
|
}
|
|
1709
1738
|
},
|
|
1710
1739
|
async (args) => {
|
|
@@ -1712,174 +1741,145 @@ ${lines.join("\n")}${note}`);
|
|
|
1712
1741
|
const target = sync.requireProDevice(args.deviceId);
|
|
1713
1742
|
if (args.action === "clear") {
|
|
1714
1743
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
1715
|
-
return
|
|
1744
|
+
return text("Cleared the entire React Query cache.");
|
|
1716
1745
|
}
|
|
1717
1746
|
if (!args.queryHash) {
|
|
1718
|
-
return
|
|
1747
|
+
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
1719
1748
|
}
|
|
1720
1749
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
1721
|
-
return
|
|
1750
|
+
return text(`Ran ${args.action} on query \`${args.queryHash}\`.`);
|
|
1722
1751
|
} catch (e) {
|
|
1723
|
-
return
|
|
1752
|
+
return fail2(errMsg2(e));
|
|
1724
1753
|
}
|
|
1725
1754
|
}
|
|
1726
1755
|
);
|
|
1727
1756
|
}
|
|
1728
1757
|
|
|
1729
1758
|
// src/tools/packs/zustand.ts
|
|
1730
|
-
import { z as
|
|
1759
|
+
import { z as z6 } from "zod";
|
|
1731
1760
|
var ZUSTAND_TOOL_ID = "zustand";
|
|
1732
|
-
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1733
|
-
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1734
|
-
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
1735
1761
|
function registerZustandTools(server, sync) {
|
|
1736
1762
|
server.registerTool(
|
|
1737
1763
|
"get_zustand_state",
|
|
1738
1764
|
{
|
|
1739
1765
|
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'].)",
|
|
1740
1766
|
inputSchema: {
|
|
1741
|
-
includeValues:
|
|
1742
|
-
limit:
|
|
1743
|
-
deviceId:
|
|
1767
|
+
includeValues: z6.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
1768
|
+
limit: z6.number().optional().describe("First N stores. Default all."),
|
|
1769
|
+
deviceId: deviceIdInput
|
|
1744
1770
|
}
|
|
1745
1771
|
},
|
|
1746
|
-
async (args) => {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1772
|
+
withDevice(sync, async (args, target) => {
|
|
1773
|
+
const res = await sync.callAction(
|
|
1774
|
+
ZUSTAND_TOOL_ID,
|
|
1775
|
+
"listStores",
|
|
1776
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
1777
|
+
target
|
|
1778
|
+
);
|
|
1779
|
+
if (!res || !Array.isArray(res.stores)) {
|
|
1780
|
+
return fail2(
|
|
1781
|
+
inactiveToolMessage({ toolId: ZUSTAND_TOOL_ID, mcpTool: "get_zustand_state", action: "listStores" })
|
|
1754
1782
|
);
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
const lines = res.stores.map((s, i) => {
|
|
1764
|
-
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
1765
|
-
const body = res.includedValues ? `
|
|
1783
|
+
}
|
|
1784
|
+
if (res.stores.length === 0) {
|
|
1785
|
+
return text("No Zustand stores are registered (call watchStores to register them).");
|
|
1786
|
+
}
|
|
1787
|
+
const lines = res.stores.map((s, i) => {
|
|
1788
|
+
const persist = s.isPersisted ? ` \xB7 persisted${s.persistName ? ` (${s.persistName})` : ""}` : "";
|
|
1789
|
+
const body = res.includedValues ? `
|
|
1766
1790
|
${JSON.stringify(s.currentState)}` : ` \xB7 keys: ${(s.keys ?? []).join(", ") || "\u2014"}`;
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1791
|
+
return `${i + 1}. **${s.name}** \xB7 ${s.changes} changes${persist}${body}`;
|
|
1792
|
+
});
|
|
1793
|
+
const note = res.includedValues ? "" : "\n\n_Compact view (keys only). Pass includeValues:true for full state. Mutate with zustand_action({store, state})._";
|
|
1794
|
+
return text(`**Zustand stores \u2014 ${res.total} (showing ${res.returned}):**
|
|
1771
1795
|
|
|
1772
1796
|
${lines.join("\n")}${note}`);
|
|
1773
|
-
|
|
1774
|
-
return fail5(errMsg5(e));
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1797
|
+
})
|
|
1777
1798
|
);
|
|
1778
1799
|
server.registerTool(
|
|
1779
1800
|
"zustand_action",
|
|
1780
1801
|
{
|
|
1781
1802
|
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.",
|
|
1782
1803
|
inputSchema: {
|
|
1783
|
-
store:
|
|
1784
|
-
state:
|
|
1785
|
-
replace:
|
|
1786
|
-
deviceId:
|
|
1804
|
+
store: z6.string().describe("Store name to target (from get_zustand_state)."),
|
|
1805
|
+
state: z6.record(z6.string(), z6.unknown()).describe("The new state object."),
|
|
1806
|
+
replace: z6.boolean().optional().describe("Replace the whole state (default true) or merge a partial (false)."),
|
|
1807
|
+
deviceId: deviceIdInput
|
|
1787
1808
|
}
|
|
1788
1809
|
},
|
|
1789
|
-
async (args) => {
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
return text4(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
1799
|
-
} catch (e) {
|
|
1800
|
-
return fail5(errMsg5(e));
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1810
|
+
withDevice(sync, async (args, target) => {
|
|
1811
|
+
await sync.callAction(
|
|
1812
|
+
ZUSTAND_TOOL_ID,
|
|
1813
|
+
"setState",
|
|
1814
|
+
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
1815
|
+
target
|
|
1816
|
+
);
|
|
1817
|
+
return text(`Set state on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
1818
|
+
})
|
|
1803
1819
|
);
|
|
1804
1820
|
}
|
|
1805
1821
|
|
|
1806
1822
|
// src/tools/packs/jotai.ts
|
|
1807
|
-
import { z as
|
|
1823
|
+
import { z as z7 } from "zod";
|
|
1808
1824
|
var JOTAI_TOOL_ID = "jotai";
|
|
1809
|
-
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1810
|
-
var fail6 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1811
|
-
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
1812
1825
|
function registerJotaiTools(server, sync) {
|
|
1813
1826
|
server.registerTool(
|
|
1814
1827
|
"get_jotai_state",
|
|
1815
1828
|
{
|
|
1816
1829
|
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'].)",
|
|
1817
1830
|
inputSchema: {
|
|
1818
|
-
includeValues:
|
|
1819
|
-
limit:
|
|
1820
|
-
deviceId:
|
|
1831
|
+
includeValues: z7.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
1832
|
+
limit: z7.number().optional().describe("First N atoms. Default all."),
|
|
1833
|
+
deviceId: deviceIdInput
|
|
1821
1834
|
}
|
|
1822
1835
|
},
|
|
1823
|
-
async (args) => {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1836
|
+
withDevice(sync, async (args, target) => {
|
|
1837
|
+
const res = await sync.callAction(
|
|
1838
|
+
JOTAI_TOOL_ID,
|
|
1839
|
+
"listAtoms",
|
|
1840
|
+
{ includeValues: args.includeValues ?? false, limit: args.limit },
|
|
1841
|
+
target
|
|
1842
|
+
);
|
|
1843
|
+
if (!res || !Array.isArray(res.atoms)) {
|
|
1844
|
+
return fail2(
|
|
1845
|
+
inactiveToolMessage({ toolId: JOTAI_TOOL_ID, mcpTool: "get_jotai_state", action: "listAtoms" })
|
|
1831
1846
|
);
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
}
|
|
1840
|
-
const lines = res.atoms.map((a, i) => {
|
|
1841
|
-
const kind = a.writable ? "writable" : "read-only";
|
|
1842
|
-
const value = res.includedValues ? `
|
|
1847
|
+
}
|
|
1848
|
+
if (res.atoms.length === 0) {
|
|
1849
|
+
return text("No Jotai atoms are being watched (call watchAtoms to register them).");
|
|
1850
|
+
}
|
|
1851
|
+
const lines = res.atoms.map((a, i) => {
|
|
1852
|
+
const kind = a.writable ? "writable" : "read-only";
|
|
1853
|
+
const value = res.includedValues ? `
|
|
1843
1854
|
${JSON.stringify(a.currentValue)}` : "";
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1855
|
+
return `${i + 1}. **${a.label}** \xB7 ${a.changes} changes \xB7 ${kind}${value}`;
|
|
1856
|
+
});
|
|
1857
|
+
const note = res.includedValues ? "" : "\n\n_Compact view. Pass includeValues:true for current values. Set a writable atom with jotai_action({atom, value})._";
|
|
1858
|
+
return text(`**Jotai atoms \u2014 ${res.total} (showing ${res.returned}):**
|
|
1848
1859
|
|
|
1849
1860
|
${lines.join("\n")}${note}`);
|
|
1850
|
-
|
|
1851
|
-
return fail6(errMsg6(e));
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1861
|
+
})
|
|
1854
1862
|
);
|
|
1855
1863
|
server.registerTool(
|
|
1856
1864
|
"jotai_action",
|
|
1857
1865
|
{
|
|
1858
1866
|
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.",
|
|
1859
1867
|
inputSchema: {
|
|
1860
|
-
atom:
|
|
1861
|
-
value:
|
|
1862
|
-
deviceId:
|
|
1868
|
+
atom: z7.string().describe("Atom label to set (from get_jotai_state; must be writable)."),
|
|
1869
|
+
value: z7.unknown().describe("The new value (any JSON \u2014 number, string, object, etc.)."),
|
|
1870
|
+
deviceId: deviceIdInput
|
|
1863
1871
|
}
|
|
1864
1872
|
},
|
|
1865
|
-
async (args) => {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
return text5(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
1870
|
-
} catch (e) {
|
|
1871
|
-
return fail6(errMsg6(e));
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1873
|
+
withDevice(sync, async (args, target) => {
|
|
1874
|
+
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
1875
|
+
return text(`Set Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
1876
|
+
})
|
|
1874
1877
|
);
|
|
1875
1878
|
}
|
|
1876
1879
|
|
|
1877
1880
|
// src/tools/packs/redux.ts
|
|
1878
|
-
import { z as
|
|
1881
|
+
import { z as z8 } from "zod";
|
|
1879
1882
|
var REDUX_TOOL_ID = "redux";
|
|
1880
|
-
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1881
|
-
var fail7 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1882
|
-
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
1883
1883
|
function noStoreMessage(reason) {
|
|
1884
1884
|
switch (reason) {
|
|
1885
1885
|
case "no-react-redux":
|
|
@@ -1898,8 +1898,8 @@ function registerReduxTools(server, sync) {
|
|
|
1898
1898
|
{
|
|
1899
1899
|
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'].)",
|
|
1900
1900
|
inputSchema: {
|
|
1901
|
-
includeValues:
|
|
1902
|
-
deviceId:
|
|
1901
|
+
includeValues: z8.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
1902
|
+
deviceId: deviceIdInput
|
|
1903
1903
|
}
|
|
1904
1904
|
},
|
|
1905
1905
|
async (args) => {
|
|
@@ -1912,15 +1912,15 @@ function registerReduxTools(server, sync) {
|
|
|
1912
1912
|
target
|
|
1913
1913
|
);
|
|
1914
1914
|
if (!res) {
|
|
1915
|
-
return
|
|
1915
|
+
return fail2(
|
|
1916
1916
|
"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)."
|
|
1917
1917
|
);
|
|
1918
1918
|
}
|
|
1919
|
-
if (!res.available) return
|
|
1919
|
+
if (!res.available) return fail2(noStoreMessage(res.reason));
|
|
1920
1920
|
const slices = res.slices.length ? res.slices.join(", ") : "(none)";
|
|
1921
1921
|
const captureNote = res.capture === "top-level-only" ? TOP_LEVEL_ONLY_NOTE : "";
|
|
1922
1922
|
if (args.includeValues) {
|
|
1923
|
-
return
|
|
1923
|
+
return text(
|
|
1924
1924
|
`**Redux store \u2014 slices: ${slices}**
|
|
1925
1925
|
|
|
1926
1926
|
\`\`\`json
|
|
@@ -1928,13 +1928,13 @@ ${JSON.stringify(res.state, null, 2)}
|
|
|
1928
1928
|
\`\`\`${captureNote}`
|
|
1929
1929
|
);
|
|
1930
1930
|
}
|
|
1931
|
-
return
|
|
1931
|
+
return text(
|
|
1932
1932
|
`**Redux store \u2014 slices: ${slices}**
|
|
1933
1933
|
|
|
1934
1934
|
_Compact view. Pass includeValues:true for the full state tree. Dispatch with redux_dispatch({type, payload})._${captureNote}`
|
|
1935
1935
|
);
|
|
1936
1936
|
} catch (e) {
|
|
1937
|
-
return
|
|
1937
|
+
return fail2(errMsg2(e));
|
|
1938
1938
|
}
|
|
1939
1939
|
}
|
|
1940
1940
|
);
|
|
@@ -1943,9 +1943,9 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
1943
1943
|
{
|
|
1944
1944
|
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 }.",
|
|
1945
1945
|
inputSchema: {
|
|
1946
|
-
type:
|
|
1947
|
-
payload:
|
|
1948
|
-
deviceId:
|
|
1946
|
+
type: z8.string().describe('The action type, e.g. "counter/increment" or "auth/logout".'),
|
|
1947
|
+
payload: z8.unknown().optional().describe("Optional action payload (any JSON)."),
|
|
1948
|
+
deviceId: deviceIdInput
|
|
1949
1949
|
}
|
|
1950
1950
|
},
|
|
1951
1951
|
async (args) => {
|
|
@@ -1954,21 +1954,18 @@ _Compact view. Pass includeValues:true for the full state tree. Dispatch with re
|
|
|
1954
1954
|
const action = { type: args.type };
|
|
1955
1955
|
if (args.payload !== void 0) action.payload = args.payload;
|
|
1956
1956
|
const res = await sync.callAction(REDUX_TOOL_ID, "dispatch", { action }, target);
|
|
1957
|
-
if (!res || !res.dispatched) return
|
|
1958
|
-
return
|
|
1957
|
+
if (!res || !res.dispatched) return fail2(noStoreMessage(res?.reason));
|
|
1958
|
+
return text(`Dispatched \`${args.type}\`${args.payload !== void 0 ? ` with payload ${JSON.stringify(args.payload)}` : ""}.`);
|
|
1959
1959
|
} catch (e) {
|
|
1960
|
-
return
|
|
1960
|
+
return fail2(errMsg2(e));
|
|
1961
1961
|
}
|
|
1962
1962
|
}
|
|
1963
1963
|
);
|
|
1964
1964
|
}
|
|
1965
1965
|
|
|
1966
1966
|
// src/tools/packs/storage.ts
|
|
1967
|
-
import { z as
|
|
1967
|
+
import { z as z9 } from "zod";
|
|
1968
1968
|
var STORAGE_TOOL_ID = "storage";
|
|
1969
|
-
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
1970
|
-
var fail8 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
1971
|
-
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
1972
1969
|
function truncate(v, n = 200) {
|
|
1973
1970
|
if (v === null || v === void 0) return "(null)";
|
|
1974
1971
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -1980,10 +1977,10 @@ function registerStorageTools(server, sync) {
|
|
|
1980
1977
|
{
|
|
1981
1978
|
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.",
|
|
1982
1979
|
inputSchema: {
|
|
1983
|
-
pattern:
|
|
1984
|
-
includeValues:
|
|
1985
|
-
limit:
|
|
1986
|
-
deviceId:
|
|
1980
|
+
pattern: z9.string().optional().describe("Only keys containing this substring (case-insensitive)."),
|
|
1981
|
+
includeValues: z9.boolean().optional().describe("Include each key's value (truncated). Default false (keys only)."),
|
|
1982
|
+
limit: z9.number().optional().describe("Max keys per backend section. Default 100."),
|
|
1983
|
+
deviceId: deviceIdInput
|
|
1987
1984
|
}
|
|
1988
1985
|
},
|
|
1989
1986
|
async (args) => {
|
|
@@ -1995,7 +1992,7 @@ function registerStorageTools(server, sync) {
|
|
|
1995
1992
|
const sections = [];
|
|
1996
1993
|
const allKeys = await sync.callAction(STORAGE_TOOL_ID, "async.getAllKeys", {}, target);
|
|
1997
1994
|
if (!Array.isArray(allKeys)) {
|
|
1998
|
-
return
|
|
1995
|
+
return fail2("No storage result from the device. The storage tool may be inactive.");
|
|
1999
1996
|
}
|
|
2000
1997
|
const asyncKeys = allKeys.filter(matches2).slice(0, limit);
|
|
2001
1998
|
if (asyncKeys.length > 0) {
|
|
@@ -2036,12 +2033,12 @@ ${lines.join("\n")}`);
|
|
|
2036
2033
|
${lines.join("\n")}`);
|
|
2037
2034
|
}
|
|
2038
2035
|
if (sections.length === 0) {
|
|
2039
|
-
return
|
|
2036
|
+
return text(`No storage keys${pattern ? ` matching "${args.pattern}"` : ""} in any backend.`);
|
|
2040
2037
|
}
|
|
2041
2038
|
const note = args.includeValues ? "" : "\n\n_Keys only. Pass includeValues:true for values, or a pattern to filter. Modify AsyncStorage with storage_action._";
|
|
2042
|
-
return
|
|
2039
|
+
return text(`${sections.join("\n\n")}${note}`);
|
|
2043
2040
|
} catch (e) {
|
|
2044
|
-
return
|
|
2041
|
+
return fail2(errMsg2(e));
|
|
2045
2042
|
}
|
|
2046
2043
|
}
|
|
2047
2044
|
);
|
|
@@ -2050,10 +2047,10 @@ ${lines.join("\n")}`);
|
|
|
2050
2047
|
{
|
|
2051
2048
|
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.",
|
|
2052
2049
|
inputSchema: {
|
|
2053
|
-
action:
|
|
2054
|
-
key:
|
|
2055
|
-
value:
|
|
2056
|
-
deviceId:
|
|
2050
|
+
action: z9.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear all app storage."),
|
|
2051
|
+
key: z9.string().optional().describe("The key (required for set and remove)."),
|
|
2052
|
+
value: z9.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2053
|
+
deviceId: deviceIdInput
|
|
2057
2054
|
}
|
|
2058
2055
|
},
|
|
2059
2056
|
async (args) => {
|
|
@@ -2061,33 +2058,30 @@ ${lines.join("\n")}`);
|
|
|
2061
2058
|
const target = sync.requireProDevice(args.deviceId);
|
|
2062
2059
|
if (args.action === "clear") {
|
|
2063
2060
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2064
|
-
return
|
|
2061
|
+
return text("Cleared all app storage (Buoy's own devtool keys preserved).");
|
|
2065
2062
|
}
|
|
2066
2063
|
if (!args.key) {
|
|
2067
|
-
return
|
|
2064
|
+
return fail2(`'${args.action}' needs a key.`);
|
|
2068
2065
|
}
|
|
2069
2066
|
if (args.action === "remove") {
|
|
2070
2067
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2071
|
-
return
|
|
2068
|
+
return text(`Removed storage key "${args.key}".`);
|
|
2072
2069
|
}
|
|
2073
2070
|
if (args.value === void 0) {
|
|
2074
|
-
return
|
|
2071
|
+
return fail2("'set' needs a value (a string).");
|
|
2075
2072
|
}
|
|
2076
2073
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2077
|
-
return
|
|
2074
|
+
return text(`Set storage key "${args.key}".`);
|
|
2078
2075
|
} catch (e) {
|
|
2079
|
-
return
|
|
2076
|
+
return fail2(errMsg2(e));
|
|
2080
2077
|
}
|
|
2081
2078
|
}
|
|
2082
2079
|
);
|
|
2083
2080
|
}
|
|
2084
2081
|
|
|
2085
2082
|
// src/tools/packs/routes.ts
|
|
2086
|
-
import { z as
|
|
2083
|
+
import { z as z10 } from "zod";
|
|
2087
2084
|
var ROUTES_TOOL_ID = "route-events";
|
|
2088
|
-
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2089
|
-
var fail9 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2090
|
-
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
2091
2085
|
function flattenRoutes(routes, acc = []) {
|
|
2092
2086
|
for (const r of routes ?? []) {
|
|
2093
2087
|
if (r.path) acc.push(r.path);
|
|
@@ -2101,8 +2095,8 @@ function registerRoutesTools(server, sync) {
|
|
|
2101
2095
|
{
|
|
2102
2096
|
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'].)",
|
|
2103
2097
|
inputSchema: {
|
|
2104
|
-
includeSitemap:
|
|
2105
|
-
deviceId:
|
|
2098
|
+
includeSitemap: z10.boolean().optional().describe("Also list all defined route paths (can be long). Default false."),
|
|
2099
|
+
deviceId: deviceIdInput
|
|
2106
2100
|
}
|
|
2107
2101
|
},
|
|
2108
2102
|
async (args) => {
|
|
@@ -2135,9 +2129,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2135
2129
|
**Sitemap:** ${allPaths.length} routes _(pass includeSitemap:true to list them)_`);
|
|
2136
2130
|
}
|
|
2137
2131
|
parts.push("\n_Navigate with navigate({ path })._");
|
|
2138
|
-
return
|
|
2132
|
+
return text(parts.join("\n"));
|
|
2139
2133
|
} catch (e) {
|
|
2140
|
-
return
|
|
2134
|
+
return fail2(errMsg2(e));
|
|
2141
2135
|
}
|
|
2142
2136
|
}
|
|
2143
2137
|
);
|
|
@@ -2146,9 +2140,9 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2146
2140
|
{
|
|
2147
2141
|
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 '/').",
|
|
2148
2142
|
inputSchema: {
|
|
2149
|
-
path:
|
|
2150
|
-
replace:
|
|
2151
|
-
deviceId:
|
|
2143
|
+
path: z10.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
|
|
2144
|
+
replace: z10.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
|
|
2145
|
+
deviceId: deviceIdInput
|
|
2152
2146
|
}
|
|
2153
2147
|
},
|
|
2154
2148
|
async (args) => {
|
|
@@ -2160,20 +2154,17 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2160
2154
|
{ path: args.path, replace: args.replace === true },
|
|
2161
2155
|
target
|
|
2162
2156
|
);
|
|
2163
|
-
return
|
|
2157
|
+
return text(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
|
|
2164
2158
|
} catch (e) {
|
|
2165
|
-
return
|
|
2159
|
+
return fail2(errMsg2(e));
|
|
2166
2160
|
}
|
|
2167
2161
|
}
|
|
2168
2162
|
);
|
|
2169
2163
|
}
|
|
2170
2164
|
|
|
2171
2165
|
// src/tools/packs/console.ts
|
|
2172
|
-
import { z as
|
|
2166
|
+
import { z as z11 } from "zod";
|
|
2173
2167
|
var CONSOLE_TOOL_ID = "console";
|
|
2174
|
-
var text9 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2175
|
-
var fail10 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2176
|
-
var errMsg10 = (e) => e instanceof Error ? e.message : String(e);
|
|
2177
2168
|
var SEVERITY = { verbose: 0, info: 1, warning: 2, error: 3 };
|
|
2178
2169
|
var TAG = { verbose: "\xB7", info: "info", warning: "warn", error: "ERR" };
|
|
2179
2170
|
function hhmmss(ts) {
|
|
@@ -2187,51 +2178,44 @@ function registerConsoleTools(server, sync) {
|
|
|
2187
2178
|
{
|
|
2188
2179
|
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').",
|
|
2189
2180
|
inputSchema: {
|
|
2190
|
-
level:
|
|
2191
|
-
limit:
|
|
2192
|
-
pattern:
|
|
2193
|
-
deviceId:
|
|
2181
|
+
level: z11.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2182
|
+
limit: z11.number().optional().describe("Most-recent N entries. Default 50."),
|
|
2183
|
+
pattern: z11.string().optional().describe("Only entries whose message contains this substring (case-insensitive)."),
|
|
2184
|
+
deviceId: deviceIdInput
|
|
2194
2185
|
}
|
|
2195
2186
|
},
|
|
2196
|
-
async (args) => {
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
|
|
2217
|
-
const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
|
|
2218
|
-
const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
|
|
2219
|
-
return text9(`${staleHeader}${header}
|
|
2187
|
+
withDevice(sync, async (args, target) => {
|
|
2188
|
+
const { payload, stale } = await sync.getSnapshot(CONSOLE_TOOL_ID, target);
|
|
2189
|
+
const banner = staleHeader(stale);
|
|
2190
|
+
const p = Array.isArray(payload) ? { entries: payload } : payload ?? {};
|
|
2191
|
+
const all = Array.isArray(p.entries) ? p.entries : [];
|
|
2192
|
+
const minSev = args.level ? SEVERITY[args.level] : 0;
|
|
2193
|
+
const pattern = args.pattern?.toLowerCase();
|
|
2194
|
+
const filtered = all.filter(
|
|
2195
|
+
(e) => SEVERITY[e.level] >= minSev && (!pattern || (e.message ?? "").toLowerCase().includes(pattern))
|
|
2196
|
+
);
|
|
2197
|
+
const sorted = [...filtered].sort((a, b) => a.id - b.id);
|
|
2198
|
+
const limit = clampLimit(args.limit, 50);
|
|
2199
|
+
const shown = sorted.slice(-limit);
|
|
2200
|
+
if (shown.length === 0) {
|
|
2201
|
+
return text(`${banner}No console entries${args.level ? ` at level \u2265 ${args.level}` : ""}${pattern ? ` matching "${args.pattern}"` : ""} (${all.length} total captured).`);
|
|
2202
|
+
}
|
|
2203
|
+
const lines = shown.map((e) => `${hhmmss(e.timestamp)} [${TAG[e.level] ?? e.level}] ${e.message}`);
|
|
2204
|
+
const levelNote = args.level ? `, level \u2265 ${args.level}` : "";
|
|
2205
|
+
const header = `**Console \u2014 ${all.length} entries (showing ${shown.length}${levelNote}):**`;
|
|
2206
|
+
return text(`${banner}${header}
|
|
2220
2207
|
|
|
2221
2208
|
\`\`\`
|
|
2222
2209
|
${lines.join("\n")}
|
|
2223
2210
|
\`\`\`
|
|
2224
2211
|
|
|
2225
2212
|
_Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
2226
|
-
|
|
2227
|
-
return fail10(errMsg10(e));
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2213
|
+
})
|
|
2230
2214
|
);
|
|
2231
2215
|
}
|
|
2232
2216
|
|
|
2233
2217
|
// src/tools/packs/screenshot.ts
|
|
2234
|
-
import { z as
|
|
2218
|
+
import { z as z12 } from "zod";
|
|
2235
2219
|
|
|
2236
2220
|
// src/capture/simctl.ts
|
|
2237
2221
|
import { execFile } from "child_process";
|
|
@@ -2345,8 +2329,8 @@ function frameToRect(frame, screen) {
|
|
|
2345
2329
|
var TOOL_ID = "highlight-updates";
|
|
2346
2330
|
var MARGIN_PT = 12;
|
|
2347
2331
|
var CAPTURE_PADDING_PT = 6;
|
|
2348
|
-
var
|
|
2349
|
-
var
|
|
2332
|
+
var fail3 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2333
|
+
var errMsg3 = (e) => e instanceof Error ? e.message : String(e);
|
|
2350
2334
|
function reasonMessage(res, query) {
|
|
2351
2335
|
switch (res.reason) {
|
|
2352
2336
|
case "no-renders":
|
|
@@ -2376,10 +2360,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
2376
2360
|
{
|
|
2377
2361
|
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.",
|
|
2378
2362
|
inputSchema: {
|
|
2379
|
-
query:
|
|
2380
|
-
nativeTag:
|
|
2381
|
-
scrollIntoView:
|
|
2382
|
-
deviceId:
|
|
2363
|
+
query: z12.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2364
|
+
nativeTag: z12.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
2365
|
+
scrollIntoView: z12.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
2366
|
+
deviceId: deviceIdInput
|
|
2383
2367
|
}
|
|
2384
2368
|
},
|
|
2385
2369
|
async (args) => {
|
|
@@ -2421,11 +2405,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2421
2405
|
}
|
|
2422
2406
|
}
|
|
2423
2407
|
if (!located.matched || !located.rect || !located.screen) {
|
|
2424
|
-
return
|
|
2408
|
+
return fail3(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
2425
2409
|
}
|
|
2426
2410
|
const shot = await captureIosFramebuffer();
|
|
2427
2411
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
2428
|
-
return
|
|
2412
|
+
return fail3(shot.error ?? "Screenshot capture failed.");
|
|
2429
2413
|
}
|
|
2430
2414
|
const pxScale = shot.width / located.screen.width;
|
|
2431
2415
|
const r = located.rect;
|
|
@@ -2441,7 +2425,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2441
2425
|
}
|
|
2442
2426
|
);
|
|
2443
2427
|
if (!crop.ok || !crop.buffer) {
|
|
2444
|
-
return
|
|
2428
|
+
return fail3(crop.error ?? "Crop failed.");
|
|
2445
2429
|
}
|
|
2446
2430
|
const label = located.componentName || "component";
|
|
2447
2431
|
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
@@ -2454,7 +2438,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2454
2438
|
]
|
|
2455
2439
|
};
|
|
2456
2440
|
} catch (e) {
|
|
2457
|
-
return
|
|
2441
|
+
return fail3(errMsg3(e));
|
|
2458
2442
|
}
|
|
2459
2443
|
}
|
|
2460
2444
|
);
|
|
@@ -2462,14 +2446,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2462
2446
|
|
|
2463
2447
|
// src/server.ts
|
|
2464
2448
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2465
|
-
import { z as
|
|
2449
|
+
import { z as z26 } from "zod";
|
|
2466
2450
|
|
|
2467
2451
|
// src/tools/packs/network.ts
|
|
2468
|
-
import { z as
|
|
2452
|
+
import { z as z13 } from "zod";
|
|
2469
2453
|
var NETWORK_TOOL_ID = "network";
|
|
2470
|
-
var text10 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2471
|
-
var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2472
|
-
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2473
2454
|
var masterNote = (state) => {
|
|
2474
2455
|
if (state?.enabled !== false) return "";
|
|
2475
2456
|
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";
|
|
@@ -2595,12 +2576,12 @@ function registerNetworkTools(server, sync) {
|
|
|
2595
2576
|
{
|
|
2596
2577
|
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).",
|
|
2597
2578
|
inputSchema: {
|
|
2598
|
-
limit:
|
|
2599
|
-
pattern:
|
|
2600
|
-
status:
|
|
2601
|
-
flagged:
|
|
2602
|
-
includeBodies:
|
|
2603
|
-
deviceId:
|
|
2579
|
+
limit: z13.number().optional().describe("Most-recent N requests. Default 25."),
|
|
2580
|
+
pattern: z13.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
2581
|
+
status: z13.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
2582
|
+
flagged: z13.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."),
|
|
2583
|
+
includeBodies: z13.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
2584
|
+
deviceId: deviceIdInput
|
|
2604
2585
|
}
|
|
2605
2586
|
},
|
|
2606
2587
|
async (args) => {
|
|
@@ -2624,7 +2605,7 @@ function registerNetworkTools(server, sync) {
|
|
|
2624
2605
|
);
|
|
2625
2606
|
const rows2 = wanted.filter((r) => r.event && matchesStatus(r.event, args.status)).filter((r) => !pattern || (r.event.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
2626
2607
|
if (rows2.length === 0) {
|
|
2627
|
-
return
|
|
2608
|
+
return text(
|
|
2628
2609
|
`No ${args.flagged === "any" ? "pinned or saved" : args.flagged} requests on ${target}.
|
|
2629
2610
|
|
|
2630
2611
|
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.`
|
|
@@ -2641,7 +2622,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
2641
2622
|
if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
|
|
2642
2623
|
return out;
|
|
2643
2624
|
}).join("\n");
|
|
2644
|
-
return
|
|
2625
|
+
return text(
|
|
2645
2626
|
`**Kept requests on ${target}** \u2014 ${rows2.length} of ${wanted.length}
|
|
2646
2627
|
|
|
2647
2628
|
${body2}
|
|
@@ -2652,7 +2633,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
2652
2633
|
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
2653
2634
|
if (rows.length === 0) {
|
|
2654
2635
|
const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
|
|
2655
|
-
return
|
|
2636
|
+
return text(`No requests on ${target}. ${why}`);
|
|
2656
2637
|
}
|
|
2657
2638
|
const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
2658
2639
|
const body = rows.map((e, i) => {
|
|
@@ -2662,7 +2643,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
2662
2643
|
return out;
|
|
2663
2644
|
}).join("\n");
|
|
2664
2645
|
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
2665
|
-
return
|
|
2646
|
+
return text(
|
|
2666
2647
|
`**Network requests on ${target}** \u2014 ${rows.length} of ${events.length}${keptNote}
|
|
2667
2648
|
|
|
2668
2649
|
${body}
|
|
@@ -2670,7 +2651,7 @@ ${body}
|
|
|
2670
2651
|
_\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)}`
|
|
2671
2652
|
);
|
|
2672
2653
|
} catch (e) {
|
|
2673
|
-
return
|
|
2654
|
+
return fail2(errMsg2(e));
|
|
2674
2655
|
}
|
|
2675
2656
|
}
|
|
2676
2657
|
);
|
|
@@ -2679,10 +2660,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2679
2660
|
{
|
|
2680
2661
|
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.",
|
|
2681
2662
|
inputSchema: {
|
|
2682
|
-
action:
|
|
2683
|
-
id:
|
|
2684
|
-
key:
|
|
2685
|
-
deviceId:
|
|
2663
|
+
action: z13.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)."),
|
|
2664
|
+
id: z13.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
2665
|
+
key: z13.string().optional().describe("Kept-record key for `remove` \u2014 from get_network_requests({flagged:'any'})."),
|
|
2666
|
+
deviceId: deviceIdInput
|
|
2686
2667
|
}
|
|
2687
2668
|
},
|
|
2688
2669
|
async (args) => {
|
|
@@ -2690,10 +2671,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2690
2671
|
const target = sync.requireProDevice(args.deviceId);
|
|
2691
2672
|
const needsId = ["pin", "unpin", "save", "unsave"].includes(args.action);
|
|
2692
2673
|
if (needsId && !args.id) {
|
|
2693
|
-
return
|
|
2674
|
+
return fail2(`"${args.action}" needs an \`id\`. List requests with get_network_requests to get one.`);
|
|
2694
2675
|
}
|
|
2695
2676
|
if (args.action === "remove" && !args.key) {
|
|
2696
|
-
return
|
|
2677
|
+
return fail2('"remove" needs a `key` (not a request id). Get it from get_network_requests({flagged:"any"}).');
|
|
2697
2678
|
}
|
|
2698
2679
|
switch (args.action) {
|
|
2699
2680
|
case "pin":
|
|
@@ -2704,10 +2685,10 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2704
2685
|
{ id: args.id, pinned: args.action === "pin" },
|
|
2705
2686
|
target
|
|
2706
2687
|
);
|
|
2707
|
-
if (res === null) return
|
|
2688
|
+
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.`);
|
|
2708
2689
|
const capped = typeof res === "object" && res !== null && res.reason === "pin-cap";
|
|
2709
|
-
if (capped) return
|
|
2710
|
-
return
|
|
2690
|
+
if (capped) return fail2(`Pin limit reached on ${target}. Unpin something first (network_action({action:"clearPinned"}) drops them all).`);
|
|
2691
|
+
return text(`${args.action === "pin" ? "\u{1F4CC} Pinned" : "Unpinned"} \`${args.id}\` on ${target}.`);
|
|
2711
2692
|
}
|
|
2712
2693
|
case "save":
|
|
2713
2694
|
case "unsave": {
|
|
@@ -2717,26 +2698,26 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2717
2698
|
{ id: args.id, saved: args.action === "save" },
|
|
2718
2699
|
target
|
|
2719
2700
|
);
|
|
2720
|
-
if (res === null) return
|
|
2701
|
+
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.`);
|
|
2721
2702
|
const capped = typeof res === "object" && res !== null && res.reason === "saved-cap";
|
|
2722
|
-
if (capped) return
|
|
2723
|
-
return
|
|
2703
|
+
if (capped) return fail2(`Saved-request limit reached on ${target}. Remove one first, or network_action({action:"clearSaved"}).`);
|
|
2704
|
+
return text(`${args.action === "save" ? "\u{1F516} Saved" : "Unsaved"} \`${args.id}\` on ${target}.`);
|
|
2724
2705
|
}
|
|
2725
2706
|
case "remove":
|
|
2726
2707
|
await sync.callAction(NETWORK_TOOL_ID, "removeSavedRecord", { key: args.key }, target);
|
|
2727
|
-
return
|
|
2708
|
+
return text(`Removed kept record \`${args.key}\` on ${target}.`);
|
|
2728
2709
|
case "clearPinned":
|
|
2729
2710
|
await sync.callAction(NETWORK_TOOL_ID, "clearPinnedRequests", {}, target);
|
|
2730
|
-
return
|
|
2711
|
+
return text(`Unpinned all requests on ${target} (saved ones kept).`);
|
|
2731
2712
|
case "clearSaved":
|
|
2732
2713
|
await sync.callAction(NETWORK_TOOL_ID, "clearSavedRequests", {}, target);
|
|
2733
|
-
return
|
|
2714
|
+
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
2734
2715
|
case "clearRequests":
|
|
2735
2716
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
2736
|
-
return
|
|
2717
|
+
return text(`Cleared the captured request list on ${target}. Pinned/saved requests are unaffected.`);
|
|
2737
2718
|
}
|
|
2738
2719
|
} catch (e) {
|
|
2739
|
-
return
|
|
2720
|
+
return fail2(errMsg2(e));
|
|
2740
2721
|
}
|
|
2741
2722
|
}
|
|
2742
2723
|
);
|
|
@@ -2745,48 +2726,48 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2745
2726
|
{
|
|
2746
2727
|
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.",
|
|
2747
2728
|
inputSchema: {
|
|
2748
|
-
action:
|
|
2729
|
+
action: z13.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
2749
2730
|
"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."
|
|
2750
2731
|
),
|
|
2751
|
-
urlPattern:
|
|
2732
|
+
urlPattern: z13.string().optional().describe(
|
|
2752
2733
|
"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."
|
|
2753
2734
|
),
|
|
2754
|
-
fromRequestId:
|
|
2735
|
+
fromRequestId: z13.string().optional().describe(
|
|
2755
2736
|
"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."
|
|
2756
2737
|
),
|
|
2757
|
-
methods:
|
|
2758
|
-
kind:
|
|
2738
|
+
methods: z13.array(z13.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
|
|
2739
|
+
kind: z13.enum(["respond", "fail", "delay"]).optional().describe(
|
|
2759
2740
|
"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."
|
|
2760
2741
|
),
|
|
2761
|
-
status:
|
|
2742
|
+
status: z13.number().optional().describe(
|
|
2762
2743
|
"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."
|
|
2763
2744
|
),
|
|
2764
|
-
body:
|
|
2745
|
+
body: z13.union([z13.string(), z13.record(z13.unknown()), z13.array(z13.unknown())]).optional().describe(
|
|
2765
2746
|
"For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
|
|
2766
2747
|
),
|
|
2767
|
-
statusText:
|
|
2748
|
+
statusText: z13.string().optional().describe(
|
|
2768
2749
|
"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."
|
|
2769
2750
|
),
|
|
2770
|
-
headers:
|
|
2771
|
-
failKind:
|
|
2772
|
-
delayMs:
|
|
2773
|
-
times:
|
|
2751
|
+
headers: z13.record(z13.string()).optional().describe("For kind 'respond': response headers to send."),
|
|
2752
|
+
failKind: z13.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
|
|
2753
|
+
delayMs: z13.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
|
|
2754
|
+
times: z13.number().optional().describe(
|
|
2774
2755
|
"Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
|
|
2775
2756
|
),
|
|
2776
|
-
alternate:
|
|
2757
|
+
alternate: z13.boolean().optional().describe(
|
|
2777
2758
|
"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."
|
|
2778
2759
|
),
|
|
2779
|
-
name:
|
|
2780
|
-
enabled:
|
|
2760
|
+
name: z13.string().optional().describe("Label shown in the developer's UI."),
|
|
2761
|
+
enabled: z13.boolean().optional().describe(
|
|
2781
2762
|
"For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
|
|
2782
2763
|
),
|
|
2783
|
-
id:
|
|
2764
|
+
id: z13.string().optional().describe(
|
|
2784
2765
|
"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."
|
|
2785
2766
|
),
|
|
2786
|
-
url:
|
|
2767
|
+
url: z13.string().optional().describe(
|
|
2787
2768
|
"For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
|
|
2788
2769
|
),
|
|
2789
|
-
deviceId:
|
|
2770
|
+
deviceId: deviceIdInput
|
|
2790
2771
|
}
|
|
2791
2772
|
},
|
|
2792
2773
|
async (args) => {
|
|
@@ -2795,7 +2776,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2795
2776
|
switch (args.action) {
|
|
2796
2777
|
case "add": {
|
|
2797
2778
|
if (!args.urlPattern && !args.fromRequestId) {
|
|
2798
|
-
return
|
|
2779
|
+
return fail2(
|
|
2799
2780
|
"`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*'."
|
|
2800
2781
|
);
|
|
2801
2782
|
}
|
|
@@ -2827,13 +2808,13 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2827
2808
|
);
|
|
2828
2809
|
const shaped = res;
|
|
2829
2810
|
if (!shaped?.ok) {
|
|
2830
|
-
return
|
|
2811
|
+
return fail2(shaped?.error ?? `Could not add the rule on ${target}.`);
|
|
2831
2812
|
}
|
|
2832
2813
|
const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
|
|
2833
2814
|
const armed = shaped.rule?.enabled === false ? `
|
|
2834
2815
|
|
|
2835
2816
|
It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
|
|
2836
|
-
return
|
|
2817
|
+
return text(
|
|
2837
2818
|
`Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
|
|
2838
2819
|
|
|
2839
2820
|
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.`
|
|
@@ -2841,14 +2822,14 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2841
2822
|
}
|
|
2842
2823
|
case "remove": {
|
|
2843
2824
|
if (!args.id) {
|
|
2844
|
-
return
|
|
2825
|
+
return fail2('`remove` needs an `id`. List them with network_override({action:"list"}).');
|
|
2845
2826
|
}
|
|
2846
2827
|
await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
|
|
2847
|
-
return
|
|
2828
|
+
return text(`Removed override \`${args.id}\` on ${target}.`);
|
|
2848
2829
|
}
|
|
2849
2830
|
case "clear":
|
|
2850
2831
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2851
|
-
return
|
|
2832
|
+
return text(`Removed all override rules on ${target}. Traffic is real again.`);
|
|
2852
2833
|
case "enable":
|
|
2853
2834
|
case "disable": {
|
|
2854
2835
|
const enabled = args.action === "enable";
|
|
@@ -2859,12 +2840,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2859
2840
|
{ id: args.id, enabled },
|
|
2860
2841
|
target
|
|
2861
2842
|
);
|
|
2862
|
-
return
|
|
2843
|
+
return text(
|
|
2863
2844
|
`Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2864
2845
|
);
|
|
2865
2846
|
}
|
|
2866
2847
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2867
|
-
return
|
|
2848
|
+
return text(
|
|
2868
2849
|
enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2869
2850
|
);
|
|
2870
2851
|
}
|
|
@@ -2879,11 +2860,11 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2879
2860
|
if (args.id) {
|
|
2880
2861
|
const rule = rules.find((r) => r.id === args.id);
|
|
2881
2862
|
if (!rule) {
|
|
2882
|
-
return
|
|
2863
|
+
return fail2(
|
|
2883
2864
|
`No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
|
|
2884
2865
|
);
|
|
2885
2866
|
}
|
|
2886
|
-
return
|
|
2867
|
+
return text(
|
|
2887
2868
|
`**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
|
|
2888
2869
|
|
|
2889
2870
|
\`\`\`json
|
|
@@ -2892,7 +2873,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2892
2873
|
);
|
|
2893
2874
|
}
|
|
2894
2875
|
if (rules.length === 0) {
|
|
2895
|
-
return
|
|
2876
|
+
return text(
|
|
2896
2877
|
`No override rules on ${target}.
|
|
2897
2878
|
|
|
2898
2879
|
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
@@ -2920,7 +2901,7 @@ Add one with network_override({action:"add", urlPattern:"*", status:500}) to for
|
|
|
2920
2901
|
return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
|
|
2921
2902
|
\u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
|
|
2922
2903
|
});
|
|
2923
|
-
return
|
|
2904
|
+
return text(
|
|
2924
2905
|
`**Override rules on ${target}**${masterNote(res)}
|
|
2925
2906
|
|
|
2926
2907
|
${rows.join("\n")}
|
|
@@ -2936,11 +2917,11 @@ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).
|
|
|
2936
2917
|
target
|
|
2937
2918
|
);
|
|
2938
2919
|
if (!res) {
|
|
2939
|
-
return
|
|
2920
|
+
return fail2(
|
|
2940
2921
|
`${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
|
|
2941
2922
|
);
|
|
2942
2923
|
}
|
|
2943
|
-
return
|
|
2924
|
+
return text(
|
|
2944
2925
|
`**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
|
|
2945
2926
|
|
|
2946
2927
|
\`\`\`json
|
|
@@ -2952,20 +2933,245 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2952
2933
|
}
|
|
2953
2934
|
}
|
|
2954
2935
|
} catch (e) {
|
|
2955
|
-
return
|
|
2936
|
+
return fail2(errMsg2(e));
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/tools/packs/sentry.ts
|
|
2943
|
+
import { z as z14 } from "zod";
|
|
2944
|
+
var SENTRY_TOOL_ID = "sentry";
|
|
2945
|
+
var text2 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2946
|
+
var fail4 = (message) => ({
|
|
2947
|
+
isError: true,
|
|
2948
|
+
content: [{ type: "text", text: message }]
|
|
2949
|
+
});
|
|
2950
|
+
var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
|
|
2951
|
+
function hhmmss2(ts) {
|
|
2952
|
+
const d = new Date(ts);
|
|
2953
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2954
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
2955
|
+
}
|
|
2956
|
+
function bytes(n) {
|
|
2957
|
+
if (n < 1024) return `${n}B`;
|
|
2958
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
2959
|
+
return `${(n / (1024 * 1024)).toFixed(2)}MB`;
|
|
2960
|
+
}
|
|
2961
|
+
function units(n, format) {
|
|
2962
|
+
if (format === "bytes") return bytes(n);
|
|
2963
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2964
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
2965
|
+
return String(n);
|
|
2966
|
+
}
|
|
2967
|
+
var FREE_QUOTA = {
|
|
2968
|
+
errors: 5e3,
|
|
2969
|
+
spans: 5e6,
|
|
2970
|
+
replays: 50,
|
|
2971
|
+
logBytes: 5 * 1024 ** 3,
|
|
2972
|
+
attachmentBytes: 1 * 1024 ** 3
|
|
2973
|
+
};
|
|
2974
|
+
var CATEGORY_FORMAT = {
|
|
2975
|
+
errors: "count",
|
|
2976
|
+
spans: "count",
|
|
2977
|
+
replays: "count",
|
|
2978
|
+
logBytes: "bytes",
|
|
2979
|
+
attachmentBytes: "bytes"
|
|
2980
|
+
};
|
|
2981
|
+
function statusNote(status) {
|
|
2982
|
+
switch (status) {
|
|
2983
|
+
case "attached":
|
|
2984
|
+
return "";
|
|
2985
|
+
case "sdk-not-found":
|
|
2986
|
+
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.";
|
|
2987
|
+
case "no-client":
|
|
2988
|
+
return "\n\n\u26A0\uFE0F The SDK is present but no client appeared \u2014 is Sentry.init() being called?";
|
|
2989
|
+
default:
|
|
2990
|
+
return "\n\n\u23F3 Still looking for the app's Sentry client.";
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
function registerSentryTools(server, sync) {
|
|
2994
|
+
server.registerTool(
|
|
2995
|
+
"get_sentry_envelopes",
|
|
2996
|
+
{
|
|
2997
|
+
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).",
|
|
2998
|
+
inputSchema: {
|
|
2999
|
+
type: z14.string().optional().describe(
|
|
3000
|
+
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
3001
|
+
),
|
|
3002
|
+
limit: z14.number().optional().describe("Most-recent N envelopes. Default 25."),
|
|
3003
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3004
|
+
}
|
|
3005
|
+
},
|
|
3006
|
+
async (args) => {
|
|
3007
|
+
try {
|
|
3008
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3009
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
3010
|
+
const staleHeader2 = stale ? `${stale}
|
|
3011
|
+
|
|
3012
|
+
` : "";
|
|
3013
|
+
const p = payload ?? {};
|
|
3014
|
+
const all = Array.isArray(p.envelopes) ? p.envelopes : [];
|
|
3015
|
+
const filtered = args.type ? all.filter((e) => e.items.some((i) => i.type === args.type)) : all;
|
|
3016
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 25;
|
|
3017
|
+
const shown = filtered.slice(-limit);
|
|
3018
|
+
if (shown.length === 0) {
|
|
3019
|
+
return text2(
|
|
3020
|
+
`${staleHeader2}No Sentry envelopes${args.type ? ` containing a '${args.type}' item` : ""} (${all.length} captured this session).${statusNote(p.status)}`
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
const lines = shown.map((e) => {
|
|
3024
|
+
const types = e.items.map((i) => i.type).join("+");
|
|
3025
|
+
const spans = e.items.reduce((sum, i) => sum + (i.spanCount ?? 0), 0);
|
|
3026
|
+
const spanNote = spans > 0 ? ` \xB7 ${spans} span${spans === 1 ? "" : "s"}` : "";
|
|
3027
|
+
const head = `${e.id}. ${hhmmss2(e.timestamp)} [${types}] ${bytes(e.totalBytes)}${spanNote}`;
|
|
3028
|
+
return `${head}
|
|
3029
|
+
\u2514\u2500 ${e.items[0]?.summary ?? "envelope"}`;
|
|
3030
|
+
});
|
|
3031
|
+
const header = `**Sentry \u2014 ${all.length} envelope${all.length === 1 ? "" : "s"} sent (showing ${shown.length}${args.type ? `, type '${args.type}'` : ""}):**`;
|
|
3032
|
+
return text2(
|
|
3033
|
+
`${staleHeader2}${header}${statusNote(p.status)}
|
|
3034
|
+
|
|
3035
|
+
${lines.join("\n")}
|
|
3036
|
+
|
|
3037
|
+
_Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Billing units via get_sentry_cost._`
|
|
3038
|
+
);
|
|
3039
|
+
} catch (e) {
|
|
3040
|
+
return fail4(errMsg4(e));
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
);
|
|
3044
|
+
server.registerTool(
|
|
3045
|
+
"get_sentry_cost",
|
|
3046
|
+
{
|
|
3047
|
+
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.",
|
|
3048
|
+
inputSchema: {
|
|
3049
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3050
|
+
}
|
|
3051
|
+
},
|
|
3052
|
+
async (args) => {
|
|
3053
|
+
try {
|
|
3054
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3055
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
3056
|
+
const staleHeader2 = stale ? `${stale}
|
|
3057
|
+
|
|
3058
|
+
` : "";
|
|
3059
|
+
const p = payload ?? {};
|
|
3060
|
+
const usage = p.usage;
|
|
3061
|
+
if (!usage?.session) {
|
|
3062
|
+
return text2(
|
|
3063
|
+
`${staleHeader2}No usage recorded yet \u2014 nothing has been sent to Sentry this session.${statusNote(p.status)}`
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
const session = usage.session;
|
|
3067
|
+
const projected = usage.projectedMonthly;
|
|
3068
|
+
const keys = Object.keys(session);
|
|
3069
|
+
const rows = keys.map((key) => {
|
|
3070
|
+
const format = CATEGORY_FORMAT[key];
|
|
3071
|
+
const now = units(session[key], format);
|
|
3072
|
+
if (!projected) return `- ${key}: ${now} this session`;
|
|
3073
|
+
const month = projected[key];
|
|
3074
|
+
const quota = FREE_QUOTA[key];
|
|
3075
|
+
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
3076
|
+
const pctNote = pct === null ? " (not on the free plan)" : ` \u2014 ${pct}% of the free plan's ${units(quota, format)}`;
|
|
3077
|
+
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
3078
|
+
});
|
|
3079
|
+
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
3080
|
+
const contributors = usage.topContributors?.length ? `
|
|
3081
|
+
|
|
3082
|
+
**What produced it:**
|
|
3083
|
+
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
3084
|
+
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._";
|
|
3085
|
+
return text2(
|
|
3086
|
+
`${staleHeader2}**Sentry cost \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
3087
|
+
|
|
3088
|
+
${rows.join("\n")}${contributors}${caveat}`
|
|
3089
|
+
);
|
|
3090
|
+
} catch (e) {
|
|
3091
|
+
return fail4(errMsg4(e));
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
);
|
|
3095
|
+
server.registerTool(
|
|
3096
|
+
"get_sentry_drops",
|
|
3097
|
+
{
|
|
3098
|
+
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.",
|
|
3099
|
+
inputSchema: {
|
|
3100
|
+
limit: z14.number().optional().describe("Most-recent N drop records. Default 30."),
|
|
3101
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3102
|
+
}
|
|
3103
|
+
},
|
|
3104
|
+
async (args) => {
|
|
3105
|
+
try {
|
|
3106
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3107
|
+
const { payload, stale } = await sync.getSnapshot(SENTRY_TOOL_ID, target);
|
|
3108
|
+
const staleHeader2 = stale ? `${stale}
|
|
3109
|
+
|
|
3110
|
+
` : "";
|
|
3111
|
+
const p = payload ?? {};
|
|
3112
|
+
const summary = p.drops?.summary ?? [];
|
|
3113
|
+
const recent = p.drops?.recent ?? [];
|
|
3114
|
+
if (summary.length === 0) {
|
|
3115
|
+
return text2(
|
|
3116
|
+
`${staleHeader2}Nothing dropped this session \u2014 every captured event reached the transport.${statusNote(p.status)}
|
|
3117
|
+
|
|
3118
|
+
_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._`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
3122
|
+
const shown = recent.slice(-limit);
|
|
3123
|
+
const summaryLines = summary.map(
|
|
3124
|
+
(s) => `- ${s.reason}: ${s.units} unit${s.units === 1 ? "" : "s"} across ${s.records} record${s.records === 1 ? "" : "s"}`
|
|
3125
|
+
);
|
|
3126
|
+
const rows = shown.map(
|
|
3127
|
+
(d) => `${hhmmss2(d.timestamp)} [${d.reason}] ${d.category}${d.quantity > 1 ? ` \xD7${d.quantity}` : ""} \u2014 ${d.detail} (${d.source})`
|
|
3128
|
+
);
|
|
3129
|
+
return text2(
|
|
3130
|
+
`${staleHeader2}**Sentry drops \u2014 by reason:**
|
|
3131
|
+
|
|
3132
|
+
${summaryLines.join("\n")}
|
|
3133
|
+
|
|
3134
|
+
**Recent (${shown.length}):**
|
|
3135
|
+
|
|
3136
|
+
\`\`\`
|
|
3137
|
+
${rows.join("\n")}
|
|
3138
|
+
\`\`\`
|
|
3139
|
+
|
|
3140
|
+
_"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._`
|
|
3141
|
+
);
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
return fail4(errMsg4(e));
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
);
|
|
3147
|
+
server.registerTool(
|
|
3148
|
+
"sentry_action",
|
|
3149
|
+
{
|
|
3150
|
+
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.",
|
|
3151
|
+
inputSchema: {
|
|
3152
|
+
action: z14.enum(["clear"]).describe("The action to perform."),
|
|
3153
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3154
|
+
}
|
|
3155
|
+
},
|
|
3156
|
+
async (args) => {
|
|
3157
|
+
try {
|
|
3158
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3159
|
+
await sync.callAction(SENTRY_TOOL_ID, "clearEnvelopes", {}, target);
|
|
3160
|
+
return text2(
|
|
3161
|
+
"Cleared the Sentry inspector \u2014 envelopes and drop records. Drive your flow, then read get_sentry_envelopes or get_sentry_cost."
|
|
3162
|
+
);
|
|
3163
|
+
} catch (e) {
|
|
3164
|
+
return fail4(errMsg4(e));
|
|
2956
3165
|
}
|
|
2957
3166
|
}
|
|
2958
3167
|
);
|
|
2959
3168
|
}
|
|
2960
3169
|
|
|
2961
3170
|
// src/tools/packs/timeMachine.ts
|
|
2962
|
-
import { z as
|
|
3171
|
+
import { z as z15 } from "zod";
|
|
2963
3172
|
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2967
|
-
function kb(bytes) {
|
|
2968
|
-
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
3173
|
+
function kb(bytes2) {
|
|
3174
|
+
return bytes2 < 1024 ? `${bytes2} B` : `${(bytes2 / 1024).toFixed(1)} KB`;
|
|
2969
3175
|
}
|
|
2970
3176
|
function formatOutcome(outcome) {
|
|
2971
3177
|
const lines = Object.entries(outcome.results).map(([sid, r2]) => {
|
|
@@ -2990,7 +3196,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
2990
3196
|
{
|
|
2991
3197
|
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.",
|
|
2992
3198
|
inputSchema: {
|
|
2993
|
-
deviceId:
|
|
3199
|
+
deviceId: deviceIdInput
|
|
2994
3200
|
}
|
|
2995
3201
|
},
|
|
2996
3202
|
async (args) => {
|
|
@@ -3023,9 +3229,9 @@ ${providerLines.join("\n") || "_no providers registered_"}`
|
|
|
3023
3229
|
];
|
|
3024
3230
|
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
3025
3231
|
${formatOutcome(p.lastOutcome)}`);
|
|
3026
|
-
return
|
|
3232
|
+
return text(parts.join("\n\n"));
|
|
3027
3233
|
} catch (e) {
|
|
3028
|
-
return
|
|
3234
|
+
return fail2(errMsg2(e));
|
|
3029
3235
|
}
|
|
3030
3236
|
}
|
|
3031
3237
|
);
|
|
@@ -3034,14 +3240,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3034
3240
|
{
|
|
3035
3241
|
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.",
|
|
3036
3242
|
inputSchema: {
|
|
3037
|
-
action:
|
|
3038
|
-
id:
|
|
3039
|
-
name:
|
|
3040
|
-
mode:
|
|
3041
|
-
excludeKeys:
|
|
3042
|
-
restoreRoute:
|
|
3043
|
-
sourceIds:
|
|
3044
|
-
deviceId:
|
|
3243
|
+
action: z15.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3244
|
+
id: z15.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
3245
|
+
name: z15.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
3246
|
+
mode: z15.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
3247
|
+
excludeKeys: z15.array(z15.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").'),
|
|
3248
|
+
restoreRoute: z15.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."),
|
|
3249
|
+
sourceIds: z15.array(z15.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
3250
|
+
deviceId: deviceIdInput
|
|
3045
3251
|
}
|
|
3046
3252
|
},
|
|
3047
3253
|
async (args) => {
|
|
@@ -3054,14 +3260,14 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3054
3260
|
const s = result.snapshot;
|
|
3055
3261
|
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
3056
3262
|
\u26A0 ${sid}: ${w}`)).join("");
|
|
3057
|
-
return
|
|
3263
|
+
return text(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
3058
3264
|
}
|
|
3059
3265
|
case "capture_baseline": {
|
|
3060
3266
|
const result = await call("captureBaseline", {});
|
|
3061
|
-
return
|
|
3267
|
+
return text(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
3062
3268
|
}
|
|
3063
3269
|
case "preview": {
|
|
3064
|
-
if (!args.id) return
|
|
3270
|
+
if (!args.id) return fail2("preview requires `id` (from get_time_machine).");
|
|
3065
3271
|
const result = await call("preview", { id: args.id });
|
|
3066
3272
|
const lines = [];
|
|
3067
3273
|
let totalApplies = 0;
|
|
@@ -3080,46 +3286,46 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3080
3286
|
for (const note of src.notes) lines.push(` \xB7 ${note}`);
|
|
3081
3287
|
}
|
|
3082
3288
|
const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
|
|
3083
|
-
return
|
|
3289
|
+
return text(`${head}
|
|
3084
3290
|
${lines.join("\n")}`);
|
|
3085
3291
|
}
|
|
3086
3292
|
case "restore": {
|
|
3087
|
-
if (!args.id) return
|
|
3293
|
+
if (!args.id) return fail2("restore requires `id` (from get_time_machine).");
|
|
3088
3294
|
const outcome = await call("restore", {
|
|
3089
3295
|
id: args.id,
|
|
3090
3296
|
mode: args.mode ?? "live",
|
|
3091
3297
|
excludeKeys: args.excludeKeys,
|
|
3092
3298
|
restoreRoute: args.restoreRoute
|
|
3093
3299
|
});
|
|
3094
|
-
return
|
|
3300
|
+
return text(formatOutcome(outcome));
|
|
3095
3301
|
}
|
|
3096
3302
|
case "delete": {
|
|
3097
|
-
if (!args.id) return
|
|
3303
|
+
if (!args.id) return fail2("delete requires `id`.");
|
|
3098
3304
|
await call("delete", { id: args.id });
|
|
3099
|
-
return
|
|
3305
|
+
return text(`Deleted snapshot \`${args.id}\`.`);
|
|
3100
3306
|
}
|
|
3101
3307
|
case "rename": {
|
|
3102
|
-
if (!args.id || !args.name) return
|
|
3308
|
+
if (!args.id || !args.name) return fail2("rename requires `id` and `name`.");
|
|
3103
3309
|
await call("rename", { id: args.id, name: args.name });
|
|
3104
|
-
return
|
|
3310
|
+
return text(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
3105
3311
|
}
|
|
3106
3312
|
}
|
|
3107
3313
|
} catch (e) {
|
|
3108
|
-
return
|
|
3314
|
+
return fail2(errMsg2(e));
|
|
3109
3315
|
}
|
|
3110
3316
|
}
|
|
3111
3317
|
);
|
|
3112
3318
|
}
|
|
3113
3319
|
|
|
3114
3320
|
// src/tools/packs/scenarios.ts
|
|
3115
|
-
import { z as
|
|
3321
|
+
import { z as z16 } from "zod";
|
|
3116
3322
|
var SCENARIOS_TOOL_ID = "scenarios";
|
|
3117
|
-
var
|
|
3118
|
-
var
|
|
3323
|
+
var text3 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3324
|
+
var fail5 = (message) => ({
|
|
3119
3325
|
isError: true,
|
|
3120
3326
|
content: [{ type: "text", text: message }]
|
|
3121
3327
|
});
|
|
3122
|
-
var
|
|
3328
|
+
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
3123
3329
|
function varSpec(v) {
|
|
3124
3330
|
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
3125
3331
|
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
@@ -3181,7 +3387,7 @@ function registerScenariosTools(server, sync) {
|
|
|
3181
3387
|
{
|
|
3182
3388
|
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.",
|
|
3183
3389
|
inputSchema: {
|
|
3184
|
-
deviceId:
|
|
3390
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3185
3391
|
}
|
|
3186
3392
|
},
|
|
3187
3393
|
async (args) => {
|
|
@@ -3211,9 +3417,9 @@ ${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing r
|
|
|
3211
3417
|
${drafts.map(summarize).join("\n")}`
|
|
3212
3418
|
);
|
|
3213
3419
|
}
|
|
3214
|
-
return
|
|
3420
|
+
return text3(parts.join("\n\n"));
|
|
3215
3421
|
} catch (e) {
|
|
3216
|
-
return
|
|
3422
|
+
return fail5(errMsg5(e));
|
|
3217
3423
|
}
|
|
3218
3424
|
}
|
|
3219
3425
|
);
|
|
@@ -3222,66 +3428,66 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3222
3428
|
{
|
|
3223
3429
|
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.`,
|
|
3224
3430
|
inputSchema: {
|
|
3225
|
-
action:
|
|
3226
|
-
folder:
|
|
3431
|
+
action: z16.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3432
|
+
folder: z16.string().optional().describe(
|
|
3227
3433
|
"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."
|
|
3228
3434
|
),
|
|
3229
|
-
id:
|
|
3435
|
+
id: z16.string().optional().describe(
|
|
3230
3436
|
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3231
3437
|
),
|
|
3232
|
-
params:
|
|
3438
|
+
params: z16.record(z16.union([z16.string(), z16.number(), z16.boolean()])).optional().describe(
|
|
3233
3439
|
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3234
3440
|
),
|
|
3235
|
-
scenario:
|
|
3236
|
-
id:
|
|
3237
|
-
name:
|
|
3238
|
-
description:
|
|
3239
|
-
expectedOutcome:
|
|
3441
|
+
scenario: z16.object({
|
|
3442
|
+
id: z16.string().describe("Stable kebab-case id."),
|
|
3443
|
+
name: z16.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3444
|
+
description: z16.string().optional(),
|
|
3445
|
+
expectedOutcome: z16.string().optional().describe(
|
|
3240
3446
|
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3241
3447
|
),
|
|
3242
|
-
author:
|
|
3243
|
-
authoredAgainst:
|
|
3244
|
-
folder:
|
|
3448
|
+
author: z16.string().optional(),
|
|
3449
|
+
authoredAgainst: z16.string().optional().describe("App version this was written against."),
|
|
3450
|
+
folder: z16.string().optional().describe(
|
|
3245
3451
|
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3246
3452
|
),
|
|
3247
|
-
tags:
|
|
3248
|
-
vars:
|
|
3249
|
-
|
|
3250
|
-
key:
|
|
3251
|
-
label:
|
|
3252
|
-
type:
|
|
3253
|
-
options:
|
|
3254
|
-
default:
|
|
3453
|
+
tags: z16.array(z16.string()).optional(),
|
|
3454
|
+
vars: z16.array(
|
|
3455
|
+
z16.object({
|
|
3456
|
+
key: z16.string(),
|
|
3457
|
+
label: z16.string().optional(),
|
|
3458
|
+
type: z16.enum(["string", "number", "boolean", "enum"]),
|
|
3459
|
+
options: z16.array(z16.union([z16.string(), z16.number(), z16.boolean()])).optional(),
|
|
3460
|
+
default: z16.union([z16.string(), z16.number(), z16.boolean()]).optional()
|
|
3255
3461
|
})
|
|
3256
3462
|
).optional().describe(
|
|
3257
3463
|
"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."
|
|
3258
3464
|
),
|
|
3259
|
-
steps:
|
|
3260
|
-
|
|
3261
|
-
id:
|
|
3262
|
-
tool:
|
|
3465
|
+
steps: z16.array(
|
|
3466
|
+
z16.object({
|
|
3467
|
+
id: z16.string().optional(),
|
|
3468
|
+
tool: z16.string().describe(
|
|
3263
3469
|
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3264
3470
|
),
|
|
3265
|
-
action:
|
|
3471
|
+
action: z16.string().describe(
|
|
3266
3472
|
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3267
3473
|
),
|
|
3268
|
-
params:
|
|
3269
|
-
label:
|
|
3474
|
+
params: z16.unknown().optional(),
|
|
3475
|
+
label: z16.string().optional()
|
|
3270
3476
|
})
|
|
3271
3477
|
).describe("Steps, in order."),
|
|
3272
|
-
undoSteps:
|
|
3273
|
-
|
|
3274
|
-
id:
|
|
3275
|
-
tool:
|
|
3276
|
-
action:
|
|
3277
|
-
params:
|
|
3278
|
-
label:
|
|
3478
|
+
undoSteps: z16.array(
|
|
3479
|
+
z16.object({
|
|
3480
|
+
id: z16.string().optional(),
|
|
3481
|
+
tool: z16.string(),
|
|
3482
|
+
action: z16.string(),
|
|
3483
|
+
params: z16.unknown().optional(),
|
|
3484
|
+
label: z16.string().optional()
|
|
3279
3485
|
})
|
|
3280
3486
|
).optional().describe(
|
|
3281
3487
|
"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."
|
|
3282
3488
|
)
|
|
3283
3489
|
}).optional().describe("The scenario to save (action 'save')."),
|
|
3284
|
-
deviceId:
|
|
3490
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3285
3491
|
}
|
|
3286
3492
|
},
|
|
3287
3493
|
async (args) => {
|
|
@@ -3290,7 +3496,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3290
3496
|
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3291
3497
|
switch (args.action) {
|
|
3292
3498
|
case "preview": {
|
|
3293
|
-
if (!args.id) return
|
|
3499
|
+
if (!args.id) return fail5("preview requires `id` (from get_scenarios).");
|
|
3294
3500
|
const r = await call("preview", {
|
|
3295
3501
|
id: args.id,
|
|
3296
3502
|
params: args.params ?? {}
|
|
@@ -3313,16 +3519,16 @@ ${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
|
3313
3519
|
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3314
3520
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3315
3521
|
);
|
|
3316
|
-
return
|
|
3522
|
+
return text3(lines.join("\n\n"));
|
|
3317
3523
|
}
|
|
3318
3524
|
case "run": {
|
|
3319
|
-
if (!args.id) return
|
|
3525
|
+
if (!args.id) return fail5("run requires `id` (from get_scenarios).");
|
|
3320
3526
|
const r = await call("run", {
|
|
3321
3527
|
id: args.id,
|
|
3322
3528
|
params: args.params ?? {}
|
|
3323
3529
|
});
|
|
3324
3530
|
if (r.preflightErrors?.length) {
|
|
3325
|
-
return
|
|
3531
|
+
return fail5(
|
|
3326
3532
|
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3327
3533
|
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3328
3534
|
);
|
|
@@ -3334,14 +3540,14 @@ ${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
|
3334
3540
|
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3335
3541
|
}).join("\n");
|
|
3336
3542
|
if (!r.ok) {
|
|
3337
|
-
return
|
|
3543
|
+
return fail5(
|
|
3338
3544
|
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3339
3545
|
${steps}
|
|
3340
3546
|
|
|
3341
3547
|
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3342
3548
|
);
|
|
3343
3549
|
}
|
|
3344
|
-
return
|
|
3550
|
+
return text3(
|
|
3345
3551
|
[
|
|
3346
3552
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3347
3553
|
steps,
|
|
@@ -3365,49 +3571,49 @@ ${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
|
3365
3571
|
const errs = rep.errors.length ? `
|
|
3366
3572
|
|
|
3367
3573
|
Errors: ${rep.errors.join("; ")}` : "";
|
|
3368
|
-
return
|
|
3574
|
+
return text3(`**Deactivated.**
|
|
3369
3575
|
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3370
3576
|
}
|
|
3371
3577
|
case "save": {
|
|
3372
|
-
if (!args.scenario) return
|
|
3578
|
+
if (!args.scenario) return fail5("save requires `scenario`.");
|
|
3373
3579
|
const r = await call("save", { scenario: args.scenario });
|
|
3374
3580
|
if (!r.ok) {
|
|
3375
|
-
return
|
|
3581
|
+
return fail5(
|
|
3376
3582
|
`Save rejected:
|
|
3377
3583
|
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3378
3584
|
);
|
|
3379
3585
|
}
|
|
3380
|
-
return
|
|
3586
|
+
return text3(
|
|
3381
3587
|
`Saved \`${r.id}\` **as a draft**.
|
|
3382
3588
|
|
|
3383
3589
|
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.`
|
|
3384
3590
|
);
|
|
3385
3591
|
}
|
|
3386
3592
|
case "setFolder": {
|
|
3387
|
-
if (!args.id) return
|
|
3593
|
+
if (!args.id) return fail5("setFolder requires `id` (from get_scenarios).");
|
|
3388
3594
|
const r = await call("setFolder", {
|
|
3389
3595
|
id: args.id,
|
|
3390
3596
|
folder: args.folder
|
|
3391
3597
|
});
|
|
3392
|
-
return
|
|
3598
|
+
return text3(
|
|
3393
3599
|
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3394
3600
|
);
|
|
3395
3601
|
}
|
|
3396
3602
|
case "delete": {
|
|
3397
|
-
if (!args.id) return
|
|
3603
|
+
if (!args.id) return fail5("delete requires `id`.");
|
|
3398
3604
|
await call("delete", { id: args.id });
|
|
3399
|
-
return
|
|
3605
|
+
return text3(`Deleted \`${args.id}\` from the device library.`);
|
|
3400
3606
|
}
|
|
3401
3607
|
}
|
|
3402
3608
|
} catch (e) {
|
|
3403
|
-
return
|
|
3609
|
+
return fail5(errMsg5(e));
|
|
3404
3610
|
}
|
|
3405
3611
|
}
|
|
3406
3612
|
);
|
|
3407
3613
|
}
|
|
3408
3614
|
|
|
3409
3615
|
// src/tools/packs/camera.ts
|
|
3410
|
-
import { z as
|
|
3616
|
+
import { z as z17 } from "zod";
|
|
3411
3617
|
|
|
3412
3618
|
// src/capture/camera.ts
|
|
3413
3619
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -3563,9 +3769,9 @@ _Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricin
|
|
|
3563
3769
|
}
|
|
3564
3770
|
|
|
3565
3771
|
// src/tools/packs/camera.ts
|
|
3566
|
-
var
|
|
3567
|
-
var gate = (tool) => readEntitlement().isPro ? null :
|
|
3568
|
-
var
|
|
3772
|
+
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3773
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail6(proRequired(tool));
|
|
3774
|
+
var fail6 = (message) => ({
|
|
3569
3775
|
isError: true,
|
|
3570
3776
|
content: [{ type: "text", text: message }]
|
|
3571
3777
|
});
|
|
@@ -3575,11 +3781,11 @@ function registerCameraTools(server) {
|
|
|
3575
3781
|
{
|
|
3576
3782
|
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.",
|
|
3577
3783
|
inputSchema: {
|
|
3578
|
-
kind:
|
|
3579
|
-
value:
|
|
3784
|
+
kind: z17.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3785
|
+
value: z17.string().optional().describe(
|
|
3580
3786
|
"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`."
|
|
3581
3787
|
),
|
|
3582
|
-
format:
|
|
3788
|
+
format: z17.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3583
3789
|
}
|
|
3584
3790
|
},
|
|
3585
3791
|
async (args) => {
|
|
@@ -3590,12 +3796,12 @@ function registerCameraTools(server) {
|
|
|
3590
3796
|
if (args.format) cmd.push("--format", args.format);
|
|
3591
3797
|
const result = await run(cmd);
|
|
3592
3798
|
if (!result.ok) {
|
|
3593
|
-
return
|
|
3799
|
+
return fail6(
|
|
3594
3800
|
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3595
3801
|
);
|
|
3596
3802
|
}
|
|
3597
3803
|
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;
|
|
3598
|
-
return
|
|
3804
|
+
return text4(
|
|
3599
3805
|
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3600
3806
|
|
|
3601
3807
|
_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._`
|
|
@@ -3613,15 +3819,15 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3613
3819
|
if (blocked) return blocked;
|
|
3614
3820
|
const result = await run(["status"]);
|
|
3615
3821
|
if (!result.ok || !result.data) {
|
|
3616
|
-
return
|
|
3822
|
+
return fail6(result.error ?? "could not read the simulator camera status");
|
|
3617
3823
|
}
|
|
3618
3824
|
const { publishing, fps, codes } = result.data;
|
|
3619
3825
|
if (!publishing) {
|
|
3620
|
-
return
|
|
3826
|
+
return text4(
|
|
3621
3827
|
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3622
3828
|
);
|
|
3623
3829
|
}
|
|
3624
|
-
return
|
|
3830
|
+
return text4(
|
|
3625
3831
|
[
|
|
3626
3832
|
`Publishing at ${fps} fps.`,
|
|
3627
3833
|
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
@@ -3636,8 +3842,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3636
3842
|
{
|
|
3637
3843
|
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.",
|
|
3638
3844
|
inputSchema: {
|
|
3639
|
-
bundleId:
|
|
3640
|
-
udid:
|
|
3845
|
+
bundleId: z17.string().describe("The app's bundle identifier."),
|
|
3846
|
+
udid: z17.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3641
3847
|
}
|
|
3642
3848
|
},
|
|
3643
3849
|
async (args) => {
|
|
@@ -3647,9 +3853,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3647
3853
|
if (args.udid) cmd.push("--udid", args.udid);
|
|
3648
3854
|
const result = await run(cmd);
|
|
3649
3855
|
if (!result.ok) {
|
|
3650
|
-
return
|
|
3856
|
+
return fail6(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3651
3857
|
}
|
|
3652
|
-
return
|
|
3858
|
+
return text4(
|
|
3653
3859
|
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3654
3860
|
);
|
|
3655
3861
|
}
|
|
@@ -3659,7 +3865,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3659
3865
|
{
|
|
3660
3866
|
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.",
|
|
3661
3867
|
inputSchema: {
|
|
3662
|
-
bundleId:
|
|
3868
|
+
bundleId: z17.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3663
3869
|
}
|
|
3664
3870
|
},
|
|
3665
3871
|
async (args) => {
|
|
@@ -3667,11 +3873,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3667
3873
|
if (args.bundleId) cmd.push(args.bundleId);
|
|
3668
3874
|
const result = await run(cmd);
|
|
3669
3875
|
if (!result.data?.checks) {
|
|
3670
|
-
return
|
|
3876
|
+
return fail6(result.error ?? "could not run the simulator camera diagnosis");
|
|
3671
3877
|
}
|
|
3672
3878
|
const ent = readEntitlement();
|
|
3673
3879
|
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._";
|
|
3674
|
-
return
|
|
3880
|
+
return text4(describeDiagnosis(result.data.checks) + tierNote);
|
|
3675
3881
|
}
|
|
3676
3882
|
);
|
|
3677
3883
|
server.registerTool(
|
|
@@ -3685,9 +3891,9 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3685
3891
|
if (blocked) return blocked;
|
|
3686
3892
|
const result = await run(["stop"]);
|
|
3687
3893
|
if (!result.ok) {
|
|
3688
|
-
return
|
|
3894
|
+
return fail6(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3689
3895
|
}
|
|
3690
|
-
return
|
|
3896
|
+
return text4("The simulator camera is off.");
|
|
3691
3897
|
}
|
|
3692
3898
|
);
|
|
3693
3899
|
server.registerTool(
|
|
@@ -3695,7 +3901,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3695
3901
|
{
|
|
3696
3902
|
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.",
|
|
3697
3903
|
inputSchema: {
|
|
3698
|
-
udid:
|
|
3904
|
+
udid: z17.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3699
3905
|
}
|
|
3700
3906
|
},
|
|
3701
3907
|
async (args) => {
|
|
@@ -3703,7 +3909,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3703
3909
|
if (blocked) return blocked;
|
|
3704
3910
|
const devicesResult = await run(["devices"]);
|
|
3705
3911
|
if (!devicesResult.ok || !devicesResult.data) {
|
|
3706
|
-
return
|
|
3912
|
+
return fail6(devicesResult.error ?? "could not list booted simulators");
|
|
3707
3913
|
}
|
|
3708
3914
|
const devices = devicesResult.data.devices ?? [];
|
|
3709
3915
|
let apps = null;
|
|
@@ -3714,7 +3920,7 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3714
3920
|
]);
|
|
3715
3921
|
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3716
3922
|
}
|
|
3717
|
-
return
|
|
3923
|
+
return text4(describeDevices(devices, apps));
|
|
3718
3924
|
}
|
|
3719
3925
|
);
|
|
3720
3926
|
server.registerTool(
|
|
@@ -3731,11 +3937,11 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3731
3937
|
run(["windows"])
|
|
3732
3938
|
]);
|
|
3733
3939
|
if (!cams.ok && !wins.ok) {
|
|
3734
|
-
return
|
|
3940
|
+
return fail6(
|
|
3735
3941
|
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3736
3942
|
);
|
|
3737
3943
|
}
|
|
3738
|
-
return
|
|
3944
|
+
return text4(
|
|
3739
3945
|
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3740
3946
|
);
|
|
3741
3947
|
}
|
|
@@ -3745,8 +3951,8 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3745
3951
|
{
|
|
3746
3952
|
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.",
|
|
3747
3953
|
inputSchema: {
|
|
3748
|
-
action:
|
|
3749
|
-
udid:
|
|
3954
|
+
action: z17.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3955
|
+
udid: z17.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3750
3956
|
}
|
|
3751
3957
|
},
|
|
3752
3958
|
async (args) => {
|
|
@@ -3754,36 +3960,36 @@ _Apps already running do not pick up a camera they started without \u2014 use \`
|
|
|
3754
3960
|
if (blocked) return blocked;
|
|
3755
3961
|
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3756
3962
|
if (!result.ok) {
|
|
3757
|
-
return
|
|
3963
|
+
return fail6(
|
|
3758
3964
|
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3759
3965
|
);
|
|
3760
3966
|
}
|
|
3761
3967
|
const d = result.data ?? {};
|
|
3762
3968
|
if (args.action === "status") {
|
|
3763
|
-
if (!d.enabled) return
|
|
3764
|
-
return
|
|
3969
|
+
if (!d.enabled) return text4("Zero-setup mode is **off**.");
|
|
3970
|
+
return text4(
|
|
3765
3971
|
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."
|
|
3766
3972
|
);
|
|
3767
3973
|
}
|
|
3768
3974
|
if (args.action === "on") {
|
|
3769
|
-
return
|
|
3975
|
+
return text4(
|
|
3770
3976
|
"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._"
|
|
3771
3977
|
);
|
|
3772
3978
|
}
|
|
3773
|
-
return
|
|
3979
|
+
return text4("Zero-setup mode is **off**.");
|
|
3774
3980
|
}
|
|
3775
3981
|
);
|
|
3776
3982
|
}
|
|
3777
3983
|
|
|
3778
3984
|
// src/tools/packs/interact.ts
|
|
3779
|
-
import { z as
|
|
3985
|
+
import { z as z18 } from "zod";
|
|
3780
3986
|
var TOOL_ID2 = "highlight-updates";
|
|
3781
|
-
var
|
|
3782
|
-
var
|
|
3987
|
+
var text5 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3988
|
+
var fail7 = (message) => ({
|
|
3783
3989
|
isError: true,
|
|
3784
3990
|
content: [{ type: "text", text: message }]
|
|
3785
3991
|
});
|
|
3786
|
-
var
|
|
3992
|
+
var errMsg6 = (e) => e instanceof Error ? e.message : String(e);
|
|
3787
3993
|
function formatElement(e) {
|
|
3788
3994
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3789
3995
|
line += e.name;
|
|
@@ -3806,8 +4012,8 @@ function registerInteractTools(server, sync) {
|
|
|
3806
4012
|
{
|
|
3807
4013
|
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.",
|
|
3808
4014
|
inputSchema: {
|
|
3809
|
-
interactiveOnly:
|
|
3810
|
-
deviceId:
|
|
4015
|
+
interactiveOnly: z18.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4016
|
+
deviceId: deviceIdInput
|
|
3811
4017
|
}
|
|
3812
4018
|
},
|
|
3813
4019
|
async (args) => {
|
|
@@ -3817,16 +4023,16 @@ function registerInteractTools(server, sync) {
|
|
|
3817
4023
|
const all = res?.elements ?? [];
|
|
3818
4024
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3819
4025
|
if (elements.length === 0) {
|
|
3820
|
-
return
|
|
4026
|
+
return text5(
|
|
3821
4027
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3822
4028
|
);
|
|
3823
4029
|
}
|
|
3824
4030
|
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 }).`;
|
|
3825
|
-
return
|
|
4031
|
+
return text5(`${header}
|
|
3826
4032
|
|
|
3827
4033
|
${elements.map(formatElement).join("\n")}`);
|
|
3828
4034
|
} catch (e) {
|
|
3829
|
-
return
|
|
4035
|
+
return fail7(errMsg6(e));
|
|
3830
4036
|
}
|
|
3831
4037
|
}
|
|
3832
4038
|
);
|
|
@@ -3835,20 +4041,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3835
4041
|
{
|
|
3836
4042
|
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.",
|
|
3837
4043
|
inputSchema: {
|
|
3838
|
-
nativeTag:
|
|
3839
|
-
testID:
|
|
3840
|
-
query:
|
|
3841
|
-
value:
|
|
3842
|
-
text:
|
|
3843
|
-
longPress:
|
|
3844
|
-
scrollIntoView:
|
|
3845
|
-
deviceId:
|
|
4044
|
+
nativeTag: z18.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4045
|
+
testID: z18.string().optional().describe("Exact testID of the element."),
|
|
4046
|
+
query: z18.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
4047
|
+
value: z18.union([z18.number(), z18.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
4048
|
+
text: z18.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4049
|
+
longPress: z18.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
4050
|
+
scrollIntoView: z18.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
4051
|
+
deviceId: deviceIdInput
|
|
3846
4052
|
}
|
|
3847
4053
|
},
|
|
3848
4054
|
async (args) => {
|
|
3849
4055
|
try {
|
|
3850
4056
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3851
|
-
return
|
|
4057
|
+
return fail7("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3852
4058
|
}
|
|
3853
4059
|
const target = sync.requireProDevice(args.deviceId);
|
|
3854
4060
|
const res = await sync.callAction(
|
|
@@ -3873,26 +4079,23 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3873
4079
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3874
4080
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3875
4081
|
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}`;
|
|
3876
|
-
return
|
|
4082
|
+
return text5(`${verb}${scrolledNote}.${flowTip}`);
|
|
3877
4083
|
}
|
|
3878
4084
|
let msg = res?.reason ?? "Tap failed.";
|
|
3879
4085
|
if (res?.candidates?.length) {
|
|
3880
4086
|
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");
|
|
3881
4087
|
}
|
|
3882
|
-
return
|
|
4088
|
+
return fail7(msg);
|
|
3883
4089
|
} catch (e) {
|
|
3884
|
-
return
|
|
4090
|
+
return fail7(errMsg6(e));
|
|
3885
4091
|
}
|
|
3886
4092
|
}
|
|
3887
4093
|
);
|
|
3888
4094
|
}
|
|
3889
4095
|
|
|
3890
4096
|
// src/tools/packs/jsThread.ts
|
|
3891
|
-
import { z as
|
|
4097
|
+
import { z as z19 } from "zod";
|
|
3892
4098
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3893
|
-
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3894
|
-
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3895
|
-
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3896
4099
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3897
4100
|
function sparkline(values) {
|
|
3898
4101
|
const max = Math.max(1, ...values);
|
|
@@ -3963,9 +4166,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3963
4166
|
{
|
|
3964
4167
|
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.',
|
|
3965
4168
|
inputSchema: {
|
|
3966
|
-
deviceId:
|
|
3967
|
-
durationMs:
|
|
3968
|
-
clearFirst:
|
|
4169
|
+
deviceId: deviceIdInput,
|
|
4170
|
+
durationMs: z19.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4171
|
+
clearFirst: z19.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3969
4172
|
}
|
|
3970
4173
|
},
|
|
3971
4174
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3978,7 +4181,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3978
4181
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3979
4182
|
target
|
|
3980
4183
|
);
|
|
3981
|
-
if (!snap) return
|
|
4184
|
+
if (!snap) return fail2("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3982
4185
|
const md = formatTopMarkdown(snap, duration);
|
|
3983
4186
|
const slim = {
|
|
3984
4187
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3997,13 +4200,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3997
4200
|
worstFreezeMs: r.worstFreezeMs
|
|
3998
4201
|
}))
|
|
3999
4202
|
};
|
|
4000
|
-
return
|
|
4203
|
+
return text(`${md}
|
|
4001
4204
|
|
|
4002
4205
|
\`\`\`json
|
|
4003
4206
|
${JSON.stringify(slim)}
|
|
4004
4207
|
\`\`\``);
|
|
4005
4208
|
} catch (e) {
|
|
4006
|
-
return
|
|
4209
|
+
return fail2(errMsg2(e));
|
|
4007
4210
|
}
|
|
4008
4211
|
}
|
|
4009
4212
|
);
|
|
@@ -4012,8 +4215,8 @@ ${JSON.stringify(slim)}
|
|
|
4012
4215
|
{
|
|
4013
4216
|
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).',
|
|
4014
4217
|
inputSchema: {
|
|
4015
|
-
deviceId:
|
|
4016
|
-
key:
|
|
4218
|
+
deviceId: deviceIdInput,
|
|
4219
|
+
key: z19.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
4017
4220
|
}
|
|
4018
4221
|
},
|
|
4019
4222
|
async ({ deviceId, key }) => {
|
|
@@ -4026,18 +4229,18 @@ ${JSON.stringify(slim)}
|
|
|
4026
4229
|
target
|
|
4027
4230
|
);
|
|
4028
4231
|
if (!detail) {
|
|
4029
|
-
return
|
|
4232
|
+
return fail2(
|
|
4030
4233
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
4031
4234
|
);
|
|
4032
4235
|
}
|
|
4033
4236
|
const md = formatDetailMarkdown(detail);
|
|
4034
|
-
return
|
|
4237
|
+
return text(`${md}
|
|
4035
4238
|
|
|
4036
4239
|
\`\`\`json
|
|
4037
4240
|
${JSON.stringify(detail)}
|
|
4038
4241
|
\`\`\``);
|
|
4039
4242
|
} catch (e) {
|
|
4040
|
-
return
|
|
4243
|
+
return fail2(errMsg2(e));
|
|
4041
4244
|
}
|
|
4042
4245
|
}
|
|
4043
4246
|
);
|
|
@@ -4046,27 +4249,24 @@ ${JSON.stringify(detail)}
|
|
|
4046
4249
|
{
|
|
4047
4250
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
4048
4251
|
inputSchema: {
|
|
4049
|
-
deviceId:
|
|
4252
|
+
deviceId: deviceIdInput
|
|
4050
4253
|
}
|
|
4051
4254
|
},
|
|
4052
4255
|
async ({ deviceId }) => {
|
|
4053
4256
|
try {
|
|
4054
4257
|
const target = sync.requireProDevice(deviceId);
|
|
4055
4258
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
4056
|
-
return
|
|
4259
|
+
return text("Cleared js-top stats.");
|
|
4057
4260
|
} catch (e) {
|
|
4058
|
-
return
|
|
4261
|
+
return fail2(errMsg2(e));
|
|
4059
4262
|
}
|
|
4060
4263
|
}
|
|
4061
4264
|
);
|
|
4062
4265
|
}
|
|
4063
4266
|
|
|
4064
4267
|
// src/tools/packs/images.ts
|
|
4065
|
-
import { z as
|
|
4268
|
+
import { z as z20 } from "zod";
|
|
4066
4269
|
var IMAGES_TOOL_ID = "images";
|
|
4067
|
-
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4068
|
-
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4069
|
-
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
4070
4270
|
function uriTail(uri) {
|
|
4071
4271
|
if (!uri) return "(no source)";
|
|
4072
4272
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -4086,8 +4286,8 @@ function cacheLabel(r) {
|
|
|
4086
4286
|
if (r.progressSeen) return "network";
|
|
4087
4287
|
return "?";
|
|
4088
4288
|
}
|
|
4089
|
-
function mb(
|
|
4090
|
-
return `${(
|
|
4289
|
+
function mb(bytes2) {
|
|
4290
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`;
|
|
4091
4291
|
}
|
|
4092
4292
|
function formatListMarkdown(list, limit) {
|
|
4093
4293
|
const lines = [];
|
|
@@ -4154,9 +4354,9 @@ function registerImagesTools(server, sync) {
|
|
|
4154
4354
|
{
|
|
4155
4355
|
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.",
|
|
4156
4356
|
inputSchema: {
|
|
4157
|
-
deviceId:
|
|
4158
|
-
limit:
|
|
4159
|
-
status:
|
|
4357
|
+
deviceId: deviceIdInput,
|
|
4358
|
+
limit: z20.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4359
|
+
status: z20.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
4160
4360
|
}
|
|
4161
4361
|
},
|
|
4162
4362
|
async ({ deviceId, limit, status }) => {
|
|
@@ -4169,7 +4369,7 @@ function registerImagesTools(server, sync) {
|
|
|
4169
4369
|
{ limit: capped, status },
|
|
4170
4370
|
target
|
|
4171
4371
|
);
|
|
4172
|
-
if (!list) return
|
|
4372
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
4173
4373
|
const md = formatListMarkdown(list, capped);
|
|
4174
4374
|
const slim = {
|
|
4175
4375
|
stats: list.stats,
|
|
@@ -4196,13 +4396,13 @@ function registerImagesTools(server, sync) {
|
|
|
4196
4396
|
layoutShifts: r.layoutShifts
|
|
4197
4397
|
}))
|
|
4198
4398
|
};
|
|
4199
|
-
return
|
|
4399
|
+
return text(`${md}
|
|
4200
4400
|
|
|
4201
4401
|
\`\`\`json
|
|
4202
4402
|
${JSON.stringify(slim)}
|
|
4203
4403
|
\`\`\``);
|
|
4204
4404
|
} catch (e) {
|
|
4205
|
-
return
|
|
4405
|
+
return fail2(errMsg2(e));
|
|
4206
4406
|
}
|
|
4207
4407
|
}
|
|
4208
4408
|
);
|
|
@@ -4211,9 +4411,9 @@ ${JSON.stringify(slim)}
|
|
|
4211
4411
|
{
|
|
4212
4412
|
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.",
|
|
4213
4413
|
inputSchema: {
|
|
4214
|
-
deviceId:
|
|
4215
|
-
id:
|
|
4216
|
-
action:
|
|
4414
|
+
deviceId: deviceIdInput,
|
|
4415
|
+
id: z20.number().describe("Record id from get_images."),
|
|
4416
|
+
action: z20.enum([
|
|
4217
4417
|
"hard_reload",
|
|
4218
4418
|
"retry",
|
|
4219
4419
|
"evict_disk",
|
|
@@ -4225,7 +4425,7 @@ ${JSON.stringify(slim)}
|
|
|
4225
4425
|
"swap_url",
|
|
4226
4426
|
"restore"
|
|
4227
4427
|
]).describe("What to do."),
|
|
4228
|
-
url:
|
|
4428
|
+
url: z20.string().optional().describe("Replacement URL (swap_url only).")
|
|
4229
4429
|
}
|
|
4230
4430
|
},
|
|
4231
4431
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -4233,8 +4433,8 @@ ${JSON.stringify(slim)}
|
|
|
4233
4433
|
const target = sync.requireProDevice(deviceId);
|
|
4234
4434
|
if (action === "prove_savings") {
|
|
4235
4435
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
4236
|
-
if (!savings) return
|
|
4237
|
-
if (!savings.ok) return
|
|
4436
|
+
if (!savings) return fail2("Device returned nothing for proveSavings.");
|
|
4437
|
+
if (!savings.ok) return fail2(savings.message);
|
|
4238
4438
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
4239
4439
|
if (savings.originalBytes != null)
|
|
4240
4440
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -4246,11 +4446,11 @@ ${JSON.stringify(slim)}
|
|
|
4246
4446
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
4247
4447
|
if (savings.optimizedUri)
|
|
4248
4448
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
4249
|
-
return
|
|
4449
|
+
return text(parts.join("\n"));
|
|
4250
4450
|
}
|
|
4251
4451
|
const entry = ACTION_MAP[action];
|
|
4252
|
-
if (!entry) return
|
|
4253
|
-
if (action === "swap_url" && !url) return
|
|
4452
|
+
if (!entry) return fail2(`Unknown action: ${action}`);
|
|
4453
|
+
if (action === "swap_url" && !url) return fail2("swap_url requires the `url` param.");
|
|
4254
4454
|
const result = await sync.callAction(
|
|
4255
4455
|
IMAGES_TOOL_ID,
|
|
4256
4456
|
entry.action,
|
|
@@ -4270,9 +4470,9 @@ ${JSON.stringify(slim)}
|
|
|
4270
4470
|
);
|
|
4271
4471
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
4272
4472
|
}
|
|
4273
|
-
return
|
|
4473
|
+
return text(lines.join("\n"));
|
|
4274
4474
|
} catch (e) {
|
|
4275
|
-
return
|
|
4475
|
+
return fail2(errMsg2(e));
|
|
4276
4476
|
}
|
|
4277
4477
|
}
|
|
4278
4478
|
);
|
|
@@ -4281,12 +4481,12 @@ ${JSON.stringify(slim)}
|
|
|
4281
4481
|
{
|
|
4282
4482
|
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.",
|
|
4283
4483
|
inputSchema: {
|
|
4284
|
-
deviceId:
|
|
4285
|
-
network:
|
|
4286
|
-
blankImages:
|
|
4287
|
-
massAction:
|
|
4288
|
-
clearRegistry:
|
|
4289
|
-
clearExpoCaches:
|
|
4484
|
+
deviceId: deviceIdInput,
|
|
4485
|
+
network: z20.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4486
|
+
blankImages: z20.boolean().optional().describe("Render all images without a source."),
|
|
4487
|
+
massAction: z20.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."),
|
|
4488
|
+
clearRegistry: z20.boolean().optional().describe("Clear the captured image log."),
|
|
4489
|
+
clearExpoCaches: z20.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4290
4490
|
}
|
|
4291
4491
|
},
|
|
4292
4492
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -4313,17 +4513,17 @@ ${JSON.stringify(slim)}
|
|
|
4313
4513
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
4314
4514
|
done.push(r?.message ?? "expo caches cleared");
|
|
4315
4515
|
}
|
|
4316
|
-
if (done.length === 0) return
|
|
4317
|
-
return
|
|
4516
|
+
if (done.length === 0) return fail2("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4517
|
+
return text(`Applied: ${done.join(" \xB7 ")}`);
|
|
4318
4518
|
} catch (e) {
|
|
4319
|
-
return
|
|
4519
|
+
return fail2(errMsg2(e));
|
|
4320
4520
|
}
|
|
4321
4521
|
}
|
|
4322
4522
|
);
|
|
4323
4523
|
}
|
|
4324
4524
|
|
|
4325
4525
|
// src/tools/packs/assets.ts
|
|
4326
|
-
import { z as
|
|
4526
|
+
import { z as z21 } from "zod";
|
|
4327
4527
|
|
|
4328
4528
|
// src/tools/packs/assetsGraphNote.ts
|
|
4329
4529
|
function graphUnavailableNote(ss) {
|
|
@@ -4338,14 +4538,11 @@ function graphUnavailableNote(ss) {
|
|
|
4338
4538
|
|
|
4339
4539
|
// src/tools/packs/assets.ts
|
|
4340
4540
|
var ASSETS_TOOL_ID = "assets";
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
4347
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
4348
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
4541
|
+
function kb2(bytes2) {
|
|
4542
|
+
if (bytes2 == null || bytes2 <= 0) return "\u2013";
|
|
4543
|
+
if (bytes2 < 1024) return `${bytes2}B`;
|
|
4544
|
+
if (bytes2 < 1024 * 1024) return `${(bytes2 / 1024).toFixed(1)}KB`;
|
|
4545
|
+
return `${(bytes2 / (1024 * 1024)).toFixed(2)}MB`;
|
|
4349
4546
|
}
|
|
4350
4547
|
function sizeLabel(r) {
|
|
4351
4548
|
if (r.sizeBytes == null) return "\u2013";
|
|
@@ -4428,10 +4625,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4428
4625
|
{
|
|
4429
4626
|
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.",
|
|
4430
4627
|
inputSchema: {
|
|
4431
|
-
deviceId:
|
|
4432
|
-
limit:
|
|
4433
|
-
kind:
|
|
4434
|
-
unusedOnly:
|
|
4628
|
+
deviceId: deviceIdInput,
|
|
4629
|
+
limit: z21.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4630
|
+
kind: z21.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4631
|
+
unusedOnly: z21.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4435
4632
|
}
|
|
4436
4633
|
},
|
|
4437
4634
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4444,7 +4641,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4444
4641
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4445
4642
|
target
|
|
4446
4643
|
);
|
|
4447
|
-
if (!list) return
|
|
4644
|
+
if (!list) return fail2("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4448
4645
|
if (unusedOnly) {
|
|
4449
4646
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4450
4647
|
}
|
|
@@ -4468,13 +4665,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4468
4665
|
sizeSource: r.sizeSource
|
|
4469
4666
|
}))
|
|
4470
4667
|
};
|
|
4471
|
-
return
|
|
4668
|
+
return text(`${md}
|
|
4472
4669
|
|
|
4473
4670
|
\`\`\`json
|
|
4474
4671
|
${JSON.stringify(slim)}
|
|
4475
4672
|
\`\`\``);
|
|
4476
4673
|
} catch (e) {
|
|
4477
|
-
return
|
|
4674
|
+
return fail2(errMsg2(e));
|
|
4478
4675
|
}
|
|
4479
4676
|
}
|
|
4480
4677
|
);
|
|
@@ -4483,9 +4680,9 @@ ${JSON.stringify(slim)}
|
|
|
4483
4680
|
{
|
|
4484
4681
|
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).",
|
|
4485
4682
|
inputSchema: {
|
|
4486
|
-
deviceId:
|
|
4487
|
-
action:
|
|
4488
|
-
id:
|
|
4683
|
+
deviceId: deviceIdInput,
|
|
4684
|
+
action: z21.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4685
|
+
id: z21.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4489
4686
|
}
|
|
4490
4687
|
},
|
|
4491
4688
|
async ({ deviceId, action, id }) => {
|
|
@@ -4495,19 +4692,19 @@ ${JSON.stringify(slim)}
|
|
|
4495
4692
|
case "rescan": {
|
|
4496
4693
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4497
4694
|
const ss = result?.scanStatus;
|
|
4498
|
-
if (!ss) return
|
|
4499
|
-
return
|
|
4695
|
+
if (!ss) return fail2("Device returned nothing for rescan.");
|
|
4696
|
+
return text(
|
|
4500
4697
|
`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" : "")
|
|
4501
4698
|
);
|
|
4502
4699
|
}
|
|
4503
4700
|
case "measure_sizes": {
|
|
4504
4701
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4505
|
-
return
|
|
4702
|
+
return text(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4506
4703
|
}
|
|
4507
4704
|
case "detail": {
|
|
4508
|
-
if (id == null) return
|
|
4705
|
+
if (id == null) return fail2("detail requires the `id` param (from get_assets).");
|
|
4509
4706
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4510
|
-
if (!detail) return
|
|
4707
|
+
if (!detail) return fail2(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4511
4708
|
const lines = [
|
|
4512
4709
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4513
4710
|
"",
|
|
@@ -4515,8 +4712,8 @@ ${JSON.stringify(slim)}
|
|
|
4515
4712
|
];
|
|
4516
4713
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
4517
4714
|
if (detail.sizesByScale) {
|
|
4518
|
-
for (const [scale,
|
|
4519
|
-
lines.push(`- Size @${scale}x: ${kb2(
|
|
4715
|
+
for (const [scale, bytes2] of Object.entries(detail.sizesByScale)) {
|
|
4716
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes2)}`);
|
|
4520
4717
|
}
|
|
4521
4718
|
}
|
|
4522
4719
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
@@ -4525,32 +4722,32 @@ ${JSON.stringify(slim)}
|
|
|
4525
4722
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4526
4723
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4527
4724
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4528
|
-
return
|
|
4725
|
+
return text(lines.join("\n"));
|
|
4529
4726
|
}
|
|
4530
4727
|
case "save_baseline": {
|
|
4531
4728
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4532
|
-
return
|
|
4729
|
+
return text(
|
|
4533
4730
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4534
4731
|
);
|
|
4535
4732
|
}
|
|
4536
4733
|
case "clear_baseline": {
|
|
4537
4734
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4538
|
-
return
|
|
4735
|
+
return text("Baseline cleared.");
|
|
4539
4736
|
}
|
|
4540
4737
|
case "clear_records": {
|
|
4541
4738
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4542
|
-
return
|
|
4739
|
+
return text(result?.message ?? "Inventory cleared.");
|
|
4543
4740
|
}
|
|
4544
4741
|
}
|
|
4545
4742
|
} catch (e) {
|
|
4546
|
-
return
|
|
4743
|
+
return fail2(errMsg2(e));
|
|
4547
4744
|
}
|
|
4548
4745
|
}
|
|
4549
4746
|
);
|
|
4550
4747
|
}
|
|
4551
4748
|
|
|
4552
4749
|
// src/tools/packs/reload.ts
|
|
4553
|
-
import { z as
|
|
4750
|
+
import { z as z22 } from "zod";
|
|
4554
4751
|
|
|
4555
4752
|
// src/capture/devServerReload.ts
|
|
4556
4753
|
import { WebSocket } from "ws";
|
|
@@ -4613,9 +4810,6 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4613
4810
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4614
4811
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4615
4812
|
var PROBE_INTERVAL_MS = 400;
|
|
4616
|
-
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4617
|
-
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4618
|
-
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4619
4813
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4620
4814
|
async function ping(sync, deviceId, timeoutMs) {
|
|
4621
4815
|
try {
|
|
@@ -4654,12 +4848,12 @@ function registerReloadTools(server, sync) {
|
|
|
4654
4848
|
{
|
|
4655
4849
|
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.",
|
|
4656
4850
|
inputSchema: {
|
|
4657
|
-
strategy:
|
|
4851
|
+
strategy: z22.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4658
4852
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4659
4853
|
),
|
|
4660
|
-
wait:
|
|
4661
|
-
timeoutMs:
|
|
4662
|
-
deviceId:
|
|
4854
|
+
wait: z22.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4855
|
+
timeoutMs: z22.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4856
|
+
deviceId: z22.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4663
4857
|
}
|
|
4664
4858
|
},
|
|
4665
4859
|
async (args) => {
|
|
@@ -4701,39 +4895,39 @@ function registerReloadTools(server, sync) {
|
|
|
4701
4895
|
}
|
|
4702
4896
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4703
4897
|
if (args.wait === false) {
|
|
4704
|
-
return
|
|
4898
|
+
return text(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4705
4899
|
}
|
|
4706
4900
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4707
4901
|
if (elapsed == null) {
|
|
4708
|
-
return
|
|
4902
|
+
return fail2(
|
|
4709
4903
|
`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.`
|
|
4710
4904
|
);
|
|
4711
4905
|
}
|
|
4712
|
-
return
|
|
4906
|
+
return text(
|
|
4713
4907
|
`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.`
|
|
4714
4908
|
);
|
|
4715
4909
|
} catch (e) {
|
|
4716
|
-
return
|
|
4910
|
+
return fail2(errMsg2(e));
|
|
4717
4911
|
}
|
|
4718
4912
|
}
|
|
4719
4913
|
);
|
|
4720
4914
|
}
|
|
4721
4915
|
|
|
4722
4916
|
// src/tools/packs/triage.ts
|
|
4723
|
-
import { z as
|
|
4724
|
-
var
|
|
4725
|
-
var
|
|
4917
|
+
import { z as z23 } from "zod";
|
|
4918
|
+
var text6 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4919
|
+
var fail8 = (message) => ({
|
|
4726
4920
|
isError: true,
|
|
4727
4921
|
content: [{ type: "text", text: message }]
|
|
4728
4922
|
});
|
|
4729
|
-
var
|
|
4923
|
+
var errMsg7 = (e) => e instanceof Error ? e.message : String(e);
|
|
4730
4924
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4731
4925
|
var MAX_STORAGE_KEYS = 20;
|
|
4732
4926
|
var MAX_STORAGE_VALUE = 400;
|
|
4733
4927
|
var STORAGE_VALUE_BUDGET = 1600;
|
|
4734
4928
|
var MAX_LINE = 160;
|
|
4735
4929
|
var cap = (s, n = MAX_LINE) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
4736
|
-
function
|
|
4930
|
+
function hhmmss3(ts) {
|
|
4737
4931
|
if (!ts) return "--:--:--";
|
|
4738
4932
|
const d = new Date(ts);
|
|
4739
4933
|
const p = (n) => String(n).padStart(2, "0");
|
|
@@ -4763,7 +4957,7 @@ async function section(title, run2) {
|
|
|
4763
4957
|
return body ? `${title}
|
|
4764
4958
|
${body}` : null;
|
|
4765
4959
|
} catch (e) {
|
|
4766
|
-
const msg =
|
|
4960
|
+
const msg = errMsg7(e);
|
|
4767
4961
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4768
4962
|
return `${title}
|
|
4769
4963
|
(no answer \u2014 see the note above)`;
|
|
@@ -4778,7 +4972,7 @@ function registerTriageTools(server, sync) {
|
|
|
4778
4972
|
{
|
|
4779
4973
|
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.",
|
|
4780
4974
|
inputSchema: {
|
|
4781
|
-
deviceId:
|
|
4975
|
+
deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4782
4976
|
}
|
|
4783
4977
|
},
|
|
4784
4978
|
async (args) => {
|
|
@@ -4802,7 +4996,7 @@ function registerTriageTools(server, sync) {
|
|
|
4802
4996
|
return `(none \u2014 ${entries.length} entries captured)`;
|
|
4803
4997
|
}
|
|
4804
4998
|
return shown.map(
|
|
4805
|
-
(e) => `${
|
|
4999
|
+
(e) => `${hhmmss3(e.timestamp)} [${e.level === "error" ? "ERR" : "warn"}] ${cap(
|
|
4806
5000
|
(e.message ?? "").split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 3).join(" \u23CE "),
|
|
4807
5001
|
220
|
|
4808
5002
|
)}`
|
|
@@ -4885,7 +5079,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4885
5079
|
|
|
4886
5080
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4887
5081
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4888
|
-
return
|
|
5082
|
+
return text6(
|
|
4889
5083
|
`${head}${banner}
|
|
4890
5084
|
|
|
4891
5085
|
${body}
|
|
@@ -4893,22 +5087,22 @@ ${body}
|
|
|
4893
5087
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4894
5088
|
);
|
|
4895
5089
|
} catch (e) {
|
|
4896
|
-
return
|
|
5090
|
+
return fail8(errMsg7(e));
|
|
4897
5091
|
}
|
|
4898
5092
|
}
|
|
4899
5093
|
);
|
|
4900
5094
|
}
|
|
4901
5095
|
|
|
4902
5096
|
// src/tools/packs/flow.ts
|
|
4903
|
-
import { z as
|
|
5097
|
+
import { z as z24 } from "zod";
|
|
4904
5098
|
var UI_TOOL_ID = "highlight-updates";
|
|
4905
5099
|
var APP_TOOL_ID2 = "app";
|
|
4906
|
-
var
|
|
4907
|
-
var
|
|
5100
|
+
var text7 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5101
|
+
var fail9 = (message) => ({
|
|
4908
5102
|
isError: true,
|
|
4909
5103
|
content: [{ type: "text", text: message }]
|
|
4910
5104
|
});
|
|
4911
|
-
var
|
|
5105
|
+
var errMsg8 = (e) => e instanceof Error ? e.message : String(e);
|
|
4912
5106
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4913
5107
|
var POLL_MS = 250;
|
|
4914
5108
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4925,19 +5119,19 @@ function formatElement2(e) {
|
|
|
4925
5119
|
const display = e.text ?? e.label;
|
|
4926
5120
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4927
5121
|
}
|
|
4928
|
-
var stepSchema =
|
|
4929
|
-
tap:
|
|
4930
|
-
longPress:
|
|
4931
|
-
type:
|
|
4932
|
-
into:
|
|
4933
|
-
set:
|
|
4934
|
-
reload:
|
|
4935
|
-
waitMs:
|
|
4936
|
-
waitFor:
|
|
4937
|
-
waitGone:
|
|
4938
|
-
expect:
|
|
4939
|
-
expectGone:
|
|
4940
|
-
timeoutMs:
|
|
5122
|
+
var stepSchema = z24.object({
|
|
5123
|
+
tap: z24.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5124
|
+
longPress: z24.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
5125
|
+
type: z24.string().optional().describe("Text to type (needs `into`)."),
|
|
5126
|
+
into: z24.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5127
|
+
set: z24.union([z24.number(), z24.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
5128
|
+
reload: z24.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5129
|
+
waitMs: z24.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5130
|
+
waitFor: z24.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5131
|
+
waitGone: z24.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5132
|
+
expect: z24.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
5133
|
+
expectGone: z24.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
5134
|
+
timeoutMs: z24.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4941
5135
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4942
5136
|
function registerFlowTools(server, sync) {
|
|
4943
5137
|
server.registerTool(
|
|
@@ -4945,10 +5139,10 @@ function registerFlowTools(server, sync) {
|
|
|
4945
5139
|
{
|
|
4946
5140
|
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.",
|
|
4947
5141
|
inputSchema: {
|
|
4948
|
-
steps:
|
|
4949
|
-
include:
|
|
4950
|
-
continueOnFailure:
|
|
4951
|
-
deviceId:
|
|
5142
|
+
steps: z24.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5143
|
+
include: z24.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."),
|
|
5144
|
+
continueOnFailure: z24.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5145
|
+
deviceId: deviceIdInput
|
|
4952
5146
|
}
|
|
4953
5147
|
},
|
|
4954
5148
|
async (args) => {
|
|
@@ -5120,7 +5314,7 @@ function registerFlowTools(server, sync) {
|
|
|
5120
5314
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
5121
5315
|
}
|
|
5122
5316
|
} catch (e) {
|
|
5123
|
-
stepErrors.push(
|
|
5317
|
+
stepErrors.push(errMsg8(e));
|
|
5124
5318
|
}
|
|
5125
5319
|
if (stepErrors.length) {
|
|
5126
5320
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -5143,7 +5337,7 @@ function registerFlowTools(server, sync) {
|
|
|
5143
5337
|
${shown.map(formatElement2).join("\n")}`
|
|
5144
5338
|
);
|
|
5145
5339
|
} catch (e) {
|
|
5146
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
5340
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg8(e))}`);
|
|
5147
5341
|
}
|
|
5148
5342
|
}
|
|
5149
5343
|
if (include === "all") {
|
|
@@ -5172,9 +5366,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
5172
5366
|
}
|
|
5173
5367
|
}
|
|
5174
5368
|
}
|
|
5175
|
-
return
|
|
5369
|
+
return text7(parts.join("\n\n"));
|
|
5176
5370
|
} catch (e) {
|
|
5177
|
-
return
|
|
5371
|
+
return fail9(errMsg8(e));
|
|
5178
5372
|
}
|
|
5179
5373
|
}
|
|
5180
5374
|
);
|
|
@@ -5195,25 +5389,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
5195
5389
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
5196
5390
|
|
|
5197
5391
|
// src/tools/packs/renders.ts
|
|
5198
|
-
import { z as
|
|
5392
|
+
import { z as z25 } from "zod";
|
|
5199
5393
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
5200
|
-
var
|
|
5201
|
-
var
|
|
5394
|
+
var text8 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5395
|
+
var fail10 = (message) => ({
|
|
5202
5396
|
isError: true,
|
|
5203
5397
|
content: [{ type: "text", text: message }]
|
|
5204
5398
|
});
|
|
5205
|
-
var
|
|
5399
|
+
var errMsg9 = (e) => e instanceof Error ? e.message : String(e);
|
|
5206
5400
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5207
5401
|
var DEFAULT_IDLE_MS = 3e3;
|
|
5208
5402
|
var SETTLE_MS = 400;
|
|
5209
5403
|
var DEFAULT_TOP = 12;
|
|
5210
5404
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
5211
|
-
var stepSchema2 =
|
|
5212
|
-
tap:
|
|
5213
|
-
type:
|
|
5214
|
-
into:
|
|
5215
|
-
waitMs:
|
|
5216
|
-
repeat:
|
|
5405
|
+
var stepSchema2 = z25.object({
|
|
5406
|
+
tap: z25.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5407
|
+
type: z25.string().optional().describe("Text to type (needs `into`)."),
|
|
5408
|
+
into: z25.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5409
|
+
waitMs: z25.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5410
|
+
repeat: z25.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
5217
5411
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
5218
5412
|
function selector2(needle) {
|
|
5219
5413
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -5306,14 +5500,14 @@ function registerRenderTools(server, sync) {
|
|
|
5306
5500
|
{
|
|
5307
5501
|
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.",
|
|
5308
5502
|
inputSchema: {
|
|
5309
|
-
steps:
|
|
5503
|
+
steps: z25.array(stepSchema2).max(20).optional().describe(
|
|
5310
5504
|
"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."
|
|
5311
5505
|
),
|
|
5312
|
-
idleMs:
|
|
5313
|
-
label:
|
|
5314
|
-
compareToPrevious:
|
|
5315
|
-
top:
|
|
5316
|
-
deviceId:
|
|
5506
|
+
idleMs: z25.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5507
|
+
label: z25.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5508
|
+
compareToPrevious: z25.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5509
|
+
top: z25.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5510
|
+
deviceId: deviceIdInput
|
|
5317
5511
|
}
|
|
5318
5512
|
},
|
|
5319
5513
|
async (args) => {
|
|
@@ -5322,18 +5516,18 @@ function registerRenderTools(server, sync) {
|
|
|
5322
5516
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
5323
5517
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
5324
5518
|
if (!uiTool) {
|
|
5325
|
-
return
|
|
5519
|
+
return fail10(
|
|
5326
5520
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
5327
5521
|
);
|
|
5328
5522
|
}
|
|
5329
5523
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
5330
|
-
return
|
|
5524
|
+
return fail10(
|
|
5331
5525
|
"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."
|
|
5332
5526
|
);
|
|
5333
5527
|
}
|
|
5334
5528
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
5335
5529
|
if (began && began.ok === false) {
|
|
5336
|
-
return
|
|
5530
|
+
return fail10(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
5337
5531
|
}
|
|
5338
5532
|
await delay5(SETTLE_MS);
|
|
5339
5533
|
const startedAt = Date.now();
|
|
@@ -5378,7 +5572,7 @@ function registerRenderTools(server, sync) {
|
|
|
5378
5572
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5379
5573
|
const summary = ended?.summary;
|
|
5380
5574
|
if (!summary) {
|
|
5381
|
-
return
|
|
5575
|
+
return fail10(
|
|
5382
5576
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5383
5577
|
);
|
|
5384
5578
|
}
|
|
@@ -5403,9 +5597,9 @@ function registerRenderTools(server, sync) {
|
|
|
5403
5597
|
} else if (!args.compareToPrevious) {
|
|
5404
5598
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5405
5599
|
}
|
|
5406
|
-
return
|
|
5600
|
+
return text8(parts.join("\n"));
|
|
5407
5601
|
} catch (e) {
|
|
5408
|
-
return
|
|
5602
|
+
return fail10(errMsg9(e));
|
|
5409
5603
|
}
|
|
5410
5604
|
}
|
|
5411
5605
|
);
|
|
@@ -5689,6 +5883,7 @@ function createMcpServer(sync) {
|
|
|
5689
5883
|
registerStorageTools(server, sync);
|
|
5690
5884
|
registerRoutesTools(server, sync);
|
|
5691
5885
|
registerConsoleTools(server, sync);
|
|
5886
|
+
registerSentryTools(server, sync);
|
|
5692
5887
|
registerTimeMachineTools(server, sync);
|
|
5693
5888
|
registerScenariosTools(server, sync);
|
|
5694
5889
|
registerScreenshotTools(server, sync);
|
|
@@ -5701,7 +5896,7 @@ function createMcpServer(sync) {
|
|
|
5701
5896
|
return server;
|
|
5702
5897
|
}
|
|
5703
5898
|
var deviceIdField = {
|
|
5704
|
-
deviceId:
|
|
5899
|
+
deviceId: z26.string().optional().describe(
|
|
5705
5900
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5706
5901
|
)
|
|
5707
5902
|
};
|