@buoy-gg/mcp 7.0.16 → 7.0.17
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-P2OALRPK.mjs → chunk-7NCVMOMV.mjs} +236 -116
- package/dist/cli.js +247 -127
- package/dist/cli.mjs +1 -1
- package/dist/index.js +247 -127
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -330,7 +330,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
330
330
|
|
|
331
331
|
// src/server.ts
|
|
332
332
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
333
|
-
var
|
|
333
|
+
var import_zod19 = require("zod");
|
|
334
334
|
|
|
335
335
|
// src/tools/generic.ts
|
|
336
336
|
var import_zod = require("zod");
|
|
@@ -341,7 +341,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
341
341
|
var import_node_path = __toESM(require("path"));
|
|
342
342
|
|
|
343
343
|
// src/version.ts
|
|
344
|
-
var VERSION = "7.0.
|
|
344
|
+
var VERSION = "7.0.17" ? "7.0.17" : "0.0.0-dev";
|
|
345
345
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
346
346
|
|
|
347
347
|
// src/update/checkForUpdate.ts
|
|
@@ -2326,8 +2326,119 @@ _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
|
2326
2326
|
);
|
|
2327
2327
|
}
|
|
2328
2328
|
|
|
2329
|
-
// src/tools/packs/
|
|
2329
|
+
// src/tools/packs/timeMachine.ts
|
|
2330
2330
|
var import_zod12 = require("zod");
|
|
2331
|
+
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2332
|
+
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2333
|
+
var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2334
|
+
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2335
|
+
function kb(bytes) {
|
|
2336
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
2337
|
+
}
|
|
2338
|
+
function formatOutcome(outcome) {
|
|
2339
|
+
const lines = Object.entries(outcome.results).map(([sid, r]) => {
|
|
2340
|
+
const skips = r.skipped.slice(0, 3).map((s) => `
|
|
2341
|
+
\xB7 ${s.id}: ${s.reason}`).join("");
|
|
2342
|
+
const warns = r.warnings.slice(0, 3).map((w) => `
|
|
2343
|
+
\u26A0 ${w}`).join("");
|
|
2344
|
+
return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
|
|
2345
|
+
});
|
|
2346
|
+
return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
|
|
2347
|
+
${lines.join("\n")}`;
|
|
2348
|
+
}
|
|
2349
|
+
function registerTimeMachineTools(server, sync) {
|
|
2350
|
+
server.registerTool(
|
|
2351
|
+
"get_time_machine",
|
|
2352
|
+
{
|
|
2353
|
+
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. Use time_machine_action to capture a snapshot before driving a flow, then restore it to re-run the flow from the same state.",
|
|
2354
|
+
inputSchema: {
|
|
2355
|
+
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2356
|
+
}
|
|
2357
|
+
},
|
|
2358
|
+
async (args) => {
|
|
2359
|
+
try {
|
|
2360
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2361
|
+
const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
|
|
2362
|
+
const p = payload ?? {};
|
|
2363
|
+
const snapshots = p.snapshots ?? [];
|
|
2364
|
+
const providers = p.providers ?? [];
|
|
2365
|
+
const providerLines = providers.map((pr) => {
|
|
2366
|
+
const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
|
|
2367
|
+
return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
|
|
2368
|
+
});
|
|
2369
|
+
const snapLines = snapshots.map((s) => {
|
|
2370
|
+
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
2371
|
+
const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
2372
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
2373
|
+
});
|
|
2374
|
+
const parts = [
|
|
2375
|
+
`**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
|
|
2376
|
+
snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
|
|
2377
|
+
`**Sources:**
|
|
2378
|
+
${providerLines.join("\n") || "_no providers registered_"}`
|
|
2379
|
+
];
|
|
2380
|
+
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2381
|
+
${formatOutcome(p.lastOutcome)}`);
|
|
2382
|
+
return text11(parts.join("\n\n"));
|
|
2383
|
+
} catch (e) {
|
|
2384
|
+
return fail12(errMsg12(e));
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
);
|
|
2388
|
+
server.registerTool(
|
|
2389
|
+
"time_machine_action",
|
|
2390
|
+
{
|
|
2391
|
+
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, restore one (mode 'live' replaces state in place; 'reload' restores persisted storage then reloads the JS bundle \u2014 baselines always reload), 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.",
|
|
2392
|
+
inputSchema: {
|
|
2393
|
+
action: import_zod12.z.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2394
|
+
id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2395
|
+
name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2396
|
+
mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
2397
|
+
sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2398
|
+
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2399
|
+
}
|
|
2400
|
+
},
|
|
2401
|
+
async (args) => {
|
|
2402
|
+
try {
|
|
2403
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2404
|
+
const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
|
|
2405
|
+
switch (args.action) {
|
|
2406
|
+
case "capture": {
|
|
2407
|
+
const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
|
|
2408
|
+
const s = result.snapshot;
|
|
2409
|
+
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2410
|
+
\u26A0 ${sid}: ${w}`)).join("");
|
|
2411
|
+
return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2412
|
+
}
|
|
2413
|
+
case "capture_baseline": {
|
|
2414
|
+
const result = await call("captureBaseline", {});
|
|
2415
|
+
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2416
|
+
}
|
|
2417
|
+
case "restore": {
|
|
2418
|
+
if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
|
|
2419
|
+
const outcome = await call("restore", { id: args.id, mode: args.mode ?? "live" });
|
|
2420
|
+
return text11(formatOutcome(outcome));
|
|
2421
|
+
}
|
|
2422
|
+
case "delete": {
|
|
2423
|
+
if (!args.id) return fail12("delete requires `id`.");
|
|
2424
|
+
await call("delete", { id: args.id });
|
|
2425
|
+
return text11(`Deleted snapshot \`${args.id}\`.`);
|
|
2426
|
+
}
|
|
2427
|
+
case "rename": {
|
|
2428
|
+
if (!args.id || !args.name) return fail12("rename requires `id` and `name`.");
|
|
2429
|
+
await call("rename", { id: args.id, name: args.name });
|
|
2430
|
+
return text11(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
} catch (e) {
|
|
2434
|
+
return fail12(errMsg12(e));
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/tools/packs/screenshot.ts
|
|
2441
|
+
var import_zod13 = require("zod");
|
|
2331
2442
|
|
|
2332
2443
|
// src/capture/simctl.ts
|
|
2333
2444
|
var import_node_child_process = require("child_process");
|
|
@@ -2407,8 +2518,8 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
2407
2518
|
var TOOL_ID = "highlight-updates";
|
|
2408
2519
|
var MARGIN_PT = 12;
|
|
2409
2520
|
var CAPTURE_PADDING_PT = 6;
|
|
2410
|
-
var
|
|
2411
|
-
var
|
|
2521
|
+
var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2522
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2412
2523
|
function reasonMessage(res, query) {
|
|
2413
2524
|
switch (res.reason) {
|
|
2414
2525
|
case "no-renders":
|
|
@@ -2438,10 +2549,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
2438
2549
|
{
|
|
2439
2550
|
description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. The component must have rendered at least once since the app opened for it to be trackable \u2014 if nothing matches, navigate to the screen that renders it and retry. iOS Simulator only, and the MCP server must be running on the same Mac as the booted Simulator (uses `xcrun simctl` + `sips`). Requires Buoy Pro.",
|
|
2440
2551
|
inputSchema: {
|
|
2441
|
-
query:
|
|
2442
|
-
nativeTag:
|
|
2443
|
-
scrollIntoView:
|
|
2444
|
-
deviceId:
|
|
2552
|
+
query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2553
|
+
nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
2554
|
+
scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
2555
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2445
2556
|
}
|
|
2446
2557
|
},
|
|
2447
2558
|
async (args) => {
|
|
@@ -2461,11 +2572,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2461
2572
|
target
|
|
2462
2573
|
);
|
|
2463
2574
|
if (!res.matched || !res.rect || !res.screen) {
|
|
2464
|
-
return
|
|
2575
|
+
return fail13(reasonMessage(res, args.query) + candidatesText(res.candidates));
|
|
2465
2576
|
}
|
|
2466
2577
|
const shot = await captureIosFramebuffer();
|
|
2467
2578
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
2468
|
-
return
|
|
2579
|
+
return fail13(shot.error ?? "Screenshot capture failed.");
|
|
2469
2580
|
}
|
|
2470
2581
|
const pxScale = shot.width / res.screen.width;
|
|
2471
2582
|
const r = res.rect;
|
|
@@ -2481,7 +2592,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2481
2592
|
}
|
|
2482
2593
|
);
|
|
2483
2594
|
if (!crop.ok || !crop.buffer) {
|
|
2484
|
-
return
|
|
2595
|
+
return fail13(crop.error ?? "Crop failed.");
|
|
2485
2596
|
}
|
|
2486
2597
|
const label = res.componentName || "component";
|
|
2487
2598
|
const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
|
|
@@ -2494,21 +2605,21 @@ function registerScreenshotTools(server, sync) {
|
|
|
2494
2605
|
]
|
|
2495
2606
|
};
|
|
2496
2607
|
} catch (e) {
|
|
2497
|
-
return
|
|
2608
|
+
return fail13(errMsg13(e));
|
|
2498
2609
|
}
|
|
2499
2610
|
}
|
|
2500
2611
|
);
|
|
2501
2612
|
}
|
|
2502
2613
|
|
|
2503
2614
|
// src/tools/packs/interact.ts
|
|
2504
|
-
var
|
|
2615
|
+
var import_zod14 = require("zod");
|
|
2505
2616
|
var TOOL_ID2 = "highlight-updates";
|
|
2506
|
-
var
|
|
2507
|
-
var
|
|
2617
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2618
|
+
var fail14 = (message) => ({
|
|
2508
2619
|
isError: true,
|
|
2509
2620
|
content: [{ type: "text", text: message }]
|
|
2510
2621
|
});
|
|
2511
|
-
var
|
|
2622
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2512
2623
|
function formatElement(e) {
|
|
2513
2624
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2514
2625
|
line += e.name;
|
|
@@ -2528,8 +2639,8 @@ function registerInteractTools(server, sync) {
|
|
|
2528
2639
|
{
|
|
2529
2640
|
description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
|
|
2530
2641
|
inputSchema: {
|
|
2531
|
-
interactiveOnly:
|
|
2532
|
-
deviceId:
|
|
2642
|
+
interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
2643
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2533
2644
|
}
|
|
2534
2645
|
},
|
|
2535
2646
|
async (args) => {
|
|
@@ -2539,16 +2650,16 @@ function registerInteractTools(server, sync) {
|
|
|
2539
2650
|
const all = res?.elements ?? [];
|
|
2540
2651
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2541
2652
|
if (elements.length === 0) {
|
|
2542
|
-
return
|
|
2653
|
+
return text12(
|
|
2543
2654
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
2544
2655
|
);
|
|
2545
2656
|
}
|
|
2546
2657
|
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 }).`;
|
|
2547
|
-
return
|
|
2658
|
+
return text12(`${header}
|
|
2548
2659
|
|
|
2549
2660
|
${elements.map(formatElement).join("\n")}`);
|
|
2550
2661
|
} catch (e) {
|
|
2551
|
-
return
|
|
2662
|
+
return fail14(errMsg14(e));
|
|
2552
2663
|
}
|
|
2553
2664
|
}
|
|
2554
2665
|
);
|
|
@@ -2557,20 +2668,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2557
2668
|
{
|
|
2558
2669
|
description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Requires Buoy Pro.",
|
|
2559
2670
|
inputSchema: {
|
|
2560
|
-
nativeTag:
|
|
2561
|
-
testID:
|
|
2562
|
-
query:
|
|
2563
|
-
value:
|
|
2564
|
-
text:
|
|
2565
|
-
longPress:
|
|
2566
|
-
scrollIntoView:
|
|
2567
|
-
deviceId:
|
|
2671
|
+
nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2672
|
+
testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
|
|
2673
|
+
query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2674
|
+
value: import_zod14.z.union([import_zod14.z.number(), import_zod14.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
2675
|
+
text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2676
|
+
longPress: import_zod14.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
2677
|
+
scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2678
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2568
2679
|
}
|
|
2569
2680
|
},
|
|
2570
2681
|
async (args) => {
|
|
2571
2682
|
try {
|
|
2572
2683
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2573
|
-
return
|
|
2684
|
+
return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2574
2685
|
}
|
|
2575
2686
|
const target = sync.requireProDevice(args.deviceId);
|
|
2576
2687
|
const res = await sync.callAction(
|
|
@@ -2593,26 +2704,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2593
2704
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2594
2705
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
2595
2706
|
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}`;
|
|
2596
|
-
return
|
|
2707
|
+
return text12(`${verb}${scrolledNote}.`);
|
|
2597
2708
|
}
|
|
2598
2709
|
let msg = res?.reason ?? "Tap failed.";
|
|
2599
2710
|
if (res?.candidates?.length) {
|
|
2600
2711
|
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");
|
|
2601
2712
|
}
|
|
2602
|
-
return
|
|
2713
|
+
return fail14(msg);
|
|
2603
2714
|
} catch (e) {
|
|
2604
|
-
return
|
|
2715
|
+
return fail14(errMsg14(e));
|
|
2605
2716
|
}
|
|
2606
2717
|
}
|
|
2607
2718
|
);
|
|
2608
2719
|
}
|
|
2609
2720
|
|
|
2610
2721
|
// src/tools/packs/jsThread.ts
|
|
2611
|
-
var
|
|
2722
|
+
var import_zod15 = require("zod");
|
|
2612
2723
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2613
|
-
var
|
|
2614
|
-
var
|
|
2615
|
-
var
|
|
2724
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2725
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2726
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2616
2727
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2617
2728
|
function sparkline(values) {
|
|
2618
2729
|
const max = Math.max(1, ...values);
|
|
@@ -2683,9 +2794,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2683
2794
|
{
|
|
2684
2795
|
description: "Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. Requires @buoy-gg/js-top on the device.",
|
|
2685
2796
|
inputSchema: {
|
|
2686
|
-
deviceId:
|
|
2687
|
-
durationMs:
|
|
2688
|
-
clearFirst:
|
|
2797
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2798
|
+
durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2799
|
+
clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2689
2800
|
}
|
|
2690
2801
|
},
|
|
2691
2802
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2698,7 +2809,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2698
2809
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2699
2810
|
target
|
|
2700
2811
|
);
|
|
2701
|
-
if (!snap) return
|
|
2812
|
+
if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2702
2813
|
const md = formatTopMarkdown(snap, duration);
|
|
2703
2814
|
const slim = {
|
|
2704
2815
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2717,13 +2828,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2717
2828
|
worstFreezeMs: r.worstFreezeMs
|
|
2718
2829
|
}))
|
|
2719
2830
|
};
|
|
2720
|
-
return
|
|
2831
|
+
return text13(`${md}
|
|
2721
2832
|
|
|
2722
2833
|
\`\`\`json
|
|
2723
2834
|
${JSON.stringify(slim)}
|
|
2724
2835
|
\`\`\``);
|
|
2725
2836
|
} catch (e) {
|
|
2726
|
-
return
|
|
2837
|
+
return fail15(errMsg15(e));
|
|
2727
2838
|
}
|
|
2728
2839
|
}
|
|
2729
2840
|
);
|
|
@@ -2732,8 +2843,8 @@ ${JSON.stringify(slim)}
|
|
|
2732
2843
|
{
|
|
2733
2844
|
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).',
|
|
2734
2845
|
inputSchema: {
|
|
2735
|
-
deviceId:
|
|
2736
|
-
key:
|
|
2846
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2847
|
+
key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2737
2848
|
}
|
|
2738
2849
|
},
|
|
2739
2850
|
async ({ deviceId, key }) => {
|
|
@@ -2746,18 +2857,18 @@ ${JSON.stringify(slim)}
|
|
|
2746
2857
|
target
|
|
2747
2858
|
);
|
|
2748
2859
|
if (!detail) {
|
|
2749
|
-
return
|
|
2860
|
+
return fail15(
|
|
2750
2861
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2751
2862
|
);
|
|
2752
2863
|
}
|
|
2753
2864
|
const md = formatDetailMarkdown(detail);
|
|
2754
|
-
return
|
|
2865
|
+
return text13(`${md}
|
|
2755
2866
|
|
|
2756
2867
|
\`\`\`json
|
|
2757
2868
|
${JSON.stringify(detail)}
|
|
2758
2869
|
\`\`\``);
|
|
2759
2870
|
} catch (e) {
|
|
2760
|
-
return
|
|
2871
|
+
return fail15(errMsg15(e));
|
|
2761
2872
|
}
|
|
2762
2873
|
}
|
|
2763
2874
|
);
|
|
@@ -2766,27 +2877,27 @@ ${JSON.stringify(detail)}
|
|
|
2766
2877
|
{
|
|
2767
2878
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2768
2879
|
inputSchema: {
|
|
2769
|
-
deviceId:
|
|
2880
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2770
2881
|
}
|
|
2771
2882
|
},
|
|
2772
2883
|
async ({ deviceId }) => {
|
|
2773
2884
|
try {
|
|
2774
2885
|
const target = sync.requireProDevice(deviceId);
|
|
2775
2886
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2776
|
-
return
|
|
2887
|
+
return text13("Cleared js-top stats.");
|
|
2777
2888
|
} catch (e) {
|
|
2778
|
-
return
|
|
2889
|
+
return fail15(errMsg15(e));
|
|
2779
2890
|
}
|
|
2780
2891
|
}
|
|
2781
2892
|
);
|
|
2782
2893
|
}
|
|
2783
2894
|
|
|
2784
2895
|
// src/tools/packs/images.ts
|
|
2785
|
-
var
|
|
2896
|
+
var import_zod16 = require("zod");
|
|
2786
2897
|
var IMAGES_TOOL_ID = "images";
|
|
2787
|
-
var
|
|
2788
|
-
var
|
|
2789
|
-
var
|
|
2898
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2899
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2900
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2790
2901
|
function uriTail(uri) {
|
|
2791
2902
|
if (!uri) return "(no source)";
|
|
2792
2903
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2874,9 +2985,9 @@ function registerImagesTools(server, sync) {
|
|
|
2874
2985
|
{
|
|
2875
2986
|
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.",
|
|
2876
2987
|
inputSchema: {
|
|
2877
|
-
deviceId:
|
|
2878
|
-
limit:
|
|
2879
|
-
status:
|
|
2988
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2989
|
+
limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
2990
|
+
status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2880
2991
|
}
|
|
2881
2992
|
},
|
|
2882
2993
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2889,7 +3000,7 @@ function registerImagesTools(server, sync) {
|
|
|
2889
3000
|
{ limit: capped, status },
|
|
2890
3001
|
target
|
|
2891
3002
|
);
|
|
2892
|
-
if (!list) return
|
|
3003
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2893
3004
|
const md = formatListMarkdown(list, capped);
|
|
2894
3005
|
const slim = {
|
|
2895
3006
|
stats: list.stats,
|
|
@@ -2916,13 +3027,13 @@ function registerImagesTools(server, sync) {
|
|
|
2916
3027
|
layoutShifts: r.layoutShifts
|
|
2917
3028
|
}))
|
|
2918
3029
|
};
|
|
2919
|
-
return
|
|
3030
|
+
return text14(`${md}
|
|
2920
3031
|
|
|
2921
3032
|
\`\`\`json
|
|
2922
3033
|
${JSON.stringify(slim)}
|
|
2923
3034
|
\`\`\``);
|
|
2924
3035
|
} catch (e) {
|
|
2925
|
-
return
|
|
3036
|
+
return fail16(errMsg16(e));
|
|
2926
3037
|
}
|
|
2927
3038
|
}
|
|
2928
3039
|
);
|
|
@@ -2931,9 +3042,9 @@ ${JSON.stringify(slim)}
|
|
|
2931
3042
|
{
|
|
2932
3043
|
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.",
|
|
2933
3044
|
inputSchema: {
|
|
2934
|
-
deviceId:
|
|
2935
|
-
id:
|
|
2936
|
-
action:
|
|
3045
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3046
|
+
id: import_zod16.z.number().describe("Record id from get_images."),
|
|
3047
|
+
action: import_zod16.z.enum([
|
|
2937
3048
|
"hard_reload",
|
|
2938
3049
|
"retry",
|
|
2939
3050
|
"evict_disk",
|
|
@@ -2945,7 +3056,7 @@ ${JSON.stringify(slim)}
|
|
|
2945
3056
|
"swap_url",
|
|
2946
3057
|
"restore"
|
|
2947
3058
|
]).describe("What to do."),
|
|
2948
|
-
url:
|
|
3059
|
+
url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
2949
3060
|
}
|
|
2950
3061
|
},
|
|
2951
3062
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2953,8 +3064,8 @@ ${JSON.stringify(slim)}
|
|
|
2953
3064
|
const target = sync.requireProDevice(deviceId);
|
|
2954
3065
|
if (action === "prove_savings") {
|
|
2955
3066
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2956
|
-
if (!savings) return
|
|
2957
|
-
if (!savings.ok) return
|
|
3067
|
+
if (!savings) return fail16("Device returned nothing for proveSavings.");
|
|
3068
|
+
if (!savings.ok) return fail16(savings.message);
|
|
2958
3069
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2959
3070
|
if (savings.originalBytes != null)
|
|
2960
3071
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -2966,11 +3077,11 @@ ${JSON.stringify(slim)}
|
|
|
2966
3077
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2967
3078
|
if (savings.optimizedUri)
|
|
2968
3079
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2969
|
-
return
|
|
3080
|
+
return text14(parts.join("\n"));
|
|
2970
3081
|
}
|
|
2971
3082
|
const entry = ACTION_MAP[action];
|
|
2972
|
-
if (!entry) return
|
|
2973
|
-
if (action === "swap_url" && !url) return
|
|
3083
|
+
if (!entry) return fail16(`Unknown action: ${action}`);
|
|
3084
|
+
if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
|
|
2974
3085
|
const result = await sync.callAction(
|
|
2975
3086
|
IMAGES_TOOL_ID,
|
|
2976
3087
|
entry.action,
|
|
@@ -2990,9 +3101,9 @@ ${JSON.stringify(slim)}
|
|
|
2990
3101
|
);
|
|
2991
3102
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2992
3103
|
}
|
|
2993
|
-
return
|
|
3104
|
+
return text14(lines.join("\n"));
|
|
2994
3105
|
} catch (e) {
|
|
2995
|
-
return
|
|
3106
|
+
return fail16(errMsg16(e));
|
|
2996
3107
|
}
|
|
2997
3108
|
}
|
|
2998
3109
|
);
|
|
@@ -3001,12 +3112,12 @@ ${JSON.stringify(slim)}
|
|
|
3001
3112
|
{
|
|
3002
3113
|
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.",
|
|
3003
3114
|
inputSchema: {
|
|
3004
|
-
deviceId:
|
|
3005
|
-
network:
|
|
3006
|
-
blankImages:
|
|
3007
|
-
massAction:
|
|
3008
|
-
clearRegistry:
|
|
3009
|
-
clearExpoCaches:
|
|
3115
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3116
|
+
network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3117
|
+
blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
|
|
3118
|
+
massAction: import_zod16.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
3119
|
+
clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
|
|
3120
|
+
clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3010
3121
|
}
|
|
3011
3122
|
},
|
|
3012
3123
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3033,22 +3144,22 @@ ${JSON.stringify(slim)}
|
|
|
3033
3144
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3034
3145
|
done.push(r?.message ?? "expo caches cleared");
|
|
3035
3146
|
}
|
|
3036
|
-
if (done.length === 0) return
|
|
3037
|
-
return
|
|
3147
|
+
if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3148
|
+
return text14(`Applied: ${done.join(" \xB7 ")}`);
|
|
3038
3149
|
} catch (e) {
|
|
3039
|
-
return
|
|
3150
|
+
return fail16(errMsg16(e));
|
|
3040
3151
|
}
|
|
3041
3152
|
}
|
|
3042
3153
|
);
|
|
3043
3154
|
}
|
|
3044
3155
|
|
|
3045
3156
|
// src/tools/packs/assets.ts
|
|
3046
|
-
var
|
|
3157
|
+
var import_zod17 = require("zod");
|
|
3047
3158
|
var ASSETS_TOOL_ID = "assets";
|
|
3048
|
-
var
|
|
3049
|
-
var
|
|
3050
|
-
var
|
|
3051
|
-
function
|
|
3159
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3160
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3161
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3162
|
+
function kb2(bytes) {
|
|
3052
3163
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3053
3164
|
if (bytes < 1024) return `${bytes}B`;
|
|
3054
3165
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
@@ -3056,7 +3167,7 @@ function kb(bytes) {
|
|
|
3056
3167
|
}
|
|
3057
3168
|
function sizeLabel(r) {
|
|
3058
3169
|
if (r.sizeBytes == null) return "\u2013";
|
|
3059
|
-
return r.sizeSource === "measured" ?
|
|
3170
|
+
return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
|
|
3060
3171
|
}
|
|
3061
3172
|
function scalesLabel(scales) {
|
|
3062
3173
|
if (!scales.length) return "\u2013";
|
|
@@ -3079,9 +3190,9 @@ function formatListMarkdown2(list, limit) {
|
|
|
3079
3190
|
const ss = list.scanStatus;
|
|
3080
3191
|
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3081
3192
|
lines.push("");
|
|
3082
|
-
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${
|
|
3193
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
|
|
3083
3194
|
lines.push(
|
|
3084
|
-
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${
|
|
3195
|
+
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb2(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
|
|
3085
3196
|
);
|
|
3086
3197
|
if (ss.fontFamilies.length > 0) {
|
|
3087
3198
|
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
@@ -3097,7 +3208,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3097
3208
|
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3098
3209
|
if (ins.duplicates.length) {
|
|
3099
3210
|
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3100
|
-
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${
|
|
3211
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
|
|
3101
3212
|
}
|
|
3102
3213
|
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3103
3214
|
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
@@ -3111,7 +3222,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3111
3222
|
}
|
|
3112
3223
|
const diff = list.diff;
|
|
3113
3224
|
if (diff) {
|
|
3114
|
-
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${
|
|
3225
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
|
|
3115
3226
|
lines.push("");
|
|
3116
3227
|
lines.push(
|
|
3117
3228
|
`**Since baseline** (${new Date(diff.baselineSavedAt).toISOString()}): ${diff.added.length} added \xB7 ${diff.removed.length} removed \xB7 ${diff.grown.length} grew \xB7 ${net}` + (diff.removed.length ? ` \u2014 removed: ${diff.removed.slice(0, 5).map((r) => `${r.name}.${r.type}`).join(", ")}${diff.removed.length > 5 ? "\u2026" : ""}` : "")
|
|
@@ -3138,10 +3249,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3138
3249
|
{
|
|
3139
3250
|
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.",
|
|
3140
3251
|
inputSchema: {
|
|
3141
|
-
deviceId:
|
|
3142
|
-
limit:
|
|
3143
|
-
kind:
|
|
3144
|
-
unusedOnly:
|
|
3252
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3253
|
+
limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3254
|
+
kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3255
|
+
unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3145
3256
|
}
|
|
3146
3257
|
},
|
|
3147
3258
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3154,7 +3265,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3154
3265
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3155
3266
|
target
|
|
3156
3267
|
);
|
|
3157
|
-
if (!list) return
|
|
3268
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3158
3269
|
if (unusedOnly) {
|
|
3159
3270
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3160
3271
|
}
|
|
@@ -3178,13 +3289,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3178
3289
|
sizeSource: r.sizeSource
|
|
3179
3290
|
}))
|
|
3180
3291
|
};
|
|
3181
|
-
return
|
|
3292
|
+
return text15(`${md}
|
|
3182
3293
|
|
|
3183
3294
|
\`\`\`json
|
|
3184
3295
|
${JSON.stringify(slim)}
|
|
3185
3296
|
\`\`\``);
|
|
3186
3297
|
} catch (e) {
|
|
3187
|
-
return
|
|
3298
|
+
return fail17(errMsg17(e));
|
|
3188
3299
|
}
|
|
3189
3300
|
}
|
|
3190
3301
|
);
|
|
@@ -3193,9 +3304,9 @@ ${JSON.stringify(slim)}
|
|
|
3193
3304
|
{
|
|
3194
3305
|
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).",
|
|
3195
3306
|
inputSchema: {
|
|
3196
|
-
deviceId:
|
|
3197
|
-
action:
|
|
3198
|
-
id:
|
|
3307
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3308
|
+
action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3309
|
+
id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3199
3310
|
}
|
|
3200
3311
|
},
|
|
3201
3312
|
async ({ deviceId, action, id }) => {
|
|
@@ -3205,19 +3316,19 @@ ${JSON.stringify(slim)}
|
|
|
3205
3316
|
case "rescan": {
|
|
3206
3317
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3207
3318
|
const ss = result?.scanStatus;
|
|
3208
|
-
if (!ss) return
|
|
3209
|
-
return
|
|
3319
|
+
if (!ss) return fail17("Device returned nothing for rescan.");
|
|
3320
|
+
return text15(
|
|
3210
3321
|
`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" : "")
|
|
3211
3322
|
);
|
|
3212
3323
|
}
|
|
3213
3324
|
case "measure_sizes": {
|
|
3214
3325
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3215
|
-
return
|
|
3326
|
+
return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3216
3327
|
}
|
|
3217
3328
|
case "detail": {
|
|
3218
|
-
if (id == null) return
|
|
3329
|
+
if (id == null) return fail17("detail requires the `id` param (from get_assets).");
|
|
3219
3330
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3220
|
-
if (!detail) return
|
|
3331
|
+
if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3221
3332
|
const lines = [
|
|
3222
3333
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3223
3334
|
"",
|
|
@@ -3226,50 +3337,50 @@ ${JSON.stringify(slim)}
|
|
|
3226
3337
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3227
3338
|
if (detail.sizesByScale) {
|
|
3228
3339
|
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3229
|
-
lines.push(`- Size @${scale}x: ${
|
|
3340
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
|
|
3230
3341
|
}
|
|
3231
3342
|
}
|
|
3232
3343
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3233
|
-
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${
|
|
3344
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
|
|
3234
3345
|
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3235
3346
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3236
3347
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3237
3348
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3238
|
-
return
|
|
3349
|
+
return text15(lines.join("\n"));
|
|
3239
3350
|
}
|
|
3240
3351
|
case "save_baseline": {
|
|
3241
3352
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3242
|
-
return
|
|
3353
|
+
return text15(
|
|
3243
3354
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3244
3355
|
);
|
|
3245
3356
|
}
|
|
3246
3357
|
case "clear_baseline": {
|
|
3247
3358
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3248
|
-
return
|
|
3359
|
+
return text15("Baseline cleared.");
|
|
3249
3360
|
}
|
|
3250
3361
|
case "clear_records": {
|
|
3251
3362
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3252
|
-
return
|
|
3363
|
+
return text15(result?.message ?? "Inventory cleared.");
|
|
3253
3364
|
}
|
|
3254
3365
|
}
|
|
3255
3366
|
} catch (e) {
|
|
3256
|
-
return
|
|
3367
|
+
return fail17(errMsg17(e));
|
|
3257
3368
|
}
|
|
3258
3369
|
}
|
|
3259
3370
|
);
|
|
3260
3371
|
}
|
|
3261
3372
|
|
|
3262
3373
|
// src/tools/packs/reload.ts
|
|
3263
|
-
var
|
|
3374
|
+
var import_zod18 = require("zod");
|
|
3264
3375
|
var APP_TOOL_ID = "app";
|
|
3265
3376
|
var RELOAD_ACTION = "reloadApp";
|
|
3266
3377
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3267
3378
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3268
3379
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3269
3380
|
var PROBE_INTERVAL_MS = 400;
|
|
3270
|
-
var
|
|
3271
|
-
var
|
|
3272
|
-
var
|
|
3381
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3382
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3383
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3273
3384
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3274
3385
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3275
3386
|
try {
|
|
@@ -3304,12 +3415,12 @@ function registerReloadTools(server, sync) {
|
|
|
3304
3415
|
{
|
|
3305
3416
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, or to re-run app startup. In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
3306
3417
|
inputSchema: {
|
|
3307
|
-
strategy:
|
|
3418
|
+
strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3308
3419
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3309
3420
|
),
|
|
3310
|
-
wait:
|
|
3311
|
-
timeoutMs:
|
|
3312
|
-
deviceId:
|
|
3421
|
+
wait: import_zod18.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3422
|
+
timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3423
|
+
deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3313
3424
|
}
|
|
3314
3425
|
},
|
|
3315
3426
|
async (args) => {
|
|
@@ -3325,19 +3436,19 @@ function registerReloadTools(server, sync) {
|
|
|
3325
3436
|
);
|
|
3326
3437
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3327
3438
|
if (args.wait === false) {
|
|
3328
|
-
return
|
|
3439
|
+
return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3329
3440
|
}
|
|
3330
3441
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3331
3442
|
if (elapsed == null) {
|
|
3332
|
-
return
|
|
3443
|
+
return fail18(
|
|
3333
3444
|
`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.`
|
|
3334
3445
|
);
|
|
3335
3446
|
}
|
|
3336
|
-
return
|
|
3447
|
+
return text16(
|
|
3337
3448
|
`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.`
|
|
3338
3449
|
);
|
|
3339
3450
|
} catch (e) {
|
|
3340
|
-
return
|
|
3451
|
+
return fail18(errMsg18(e));
|
|
3341
3452
|
}
|
|
3342
3453
|
}
|
|
3343
3454
|
);
|
|
@@ -3392,6 +3503,14 @@ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 De
|
|
|
3392
3503
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
3393
3504
|
re-read anything you cached.
|
|
3394
3505
|
|
|
3506
|
+
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
3507
|
+
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
3508
|
+
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
3509
|
+
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
3510
|
+
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
3511
|
+
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
3512
|
+
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
3513
|
+
|
|
3395
3514
|
For everything else:
|
|
3396
3515
|
- \`list_tool_actions\` to see what a tool can do,
|
|
3397
3516
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -3419,6 +3538,7 @@ function createMcpServer(sync) {
|
|
|
3419
3538
|
registerStorageTools(server, sync);
|
|
3420
3539
|
registerRoutesTools(server, sync);
|
|
3421
3540
|
registerConsoleTools(server, sync);
|
|
3541
|
+
registerTimeMachineTools(server, sync);
|
|
3422
3542
|
registerScreenshotTools(server, sync);
|
|
3423
3543
|
registerInteractTools(server, sync);
|
|
3424
3544
|
registerJsThreadTools(server, sync);
|
|
@@ -3428,7 +3548,7 @@ function createMcpServer(sync) {
|
|
|
3428
3548
|
return server;
|
|
3429
3549
|
}
|
|
3430
3550
|
var deviceIdField = {
|
|
3431
|
-
deviceId:
|
|
3551
|
+
deviceId: import_zod19.z.string().optional().describe(
|
|
3432
3552
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3433
3553
|
)
|
|
3434
3554
|
};
|