@buoy-gg/mcp 7.0.15 → 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-I3Z3IWDH.mjs → chunk-7NCVMOMV.mjs} +428 -84
- package/dist/cli.js +439 -95
- package/dist/cli.mjs +1 -1
- package/dist/index.js +439 -95
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -301,7 +301,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
301
301
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
302
302
|
|
|
303
303
|
// src/version.ts
|
|
304
|
-
var VERSION = "7.0.
|
|
304
|
+
var VERSION = "7.0.17" ? "7.0.17" : "0.0.0-dev";
|
|
305
305
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
306
306
|
|
|
307
307
|
// src/update/checkForUpdate.ts
|
|
@@ -2025,7 +2025,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2025
2025
|
|
|
2026
2026
|
// src/server.ts
|
|
2027
2027
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2028
|
-
import { z as
|
|
2028
|
+
import { z as z19 } from "zod";
|
|
2029
2029
|
|
|
2030
2030
|
// src/tools/packs/network.ts
|
|
2031
2031
|
import { z as z12 } from "zod";
|
|
@@ -2468,15 +2468,126 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2468
2468
|
);
|
|
2469
2469
|
}
|
|
2470
2470
|
|
|
2471
|
-
// src/tools/packs/
|
|
2471
|
+
// src/tools/packs/timeMachine.ts
|
|
2472
2472
|
import { z as z13 } from "zod";
|
|
2473
|
-
var
|
|
2473
|
+
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2474
2474
|
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2475
|
-
var fail13 = (message) => ({
|
|
2475
|
+
var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2476
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2477
|
+
function kb(bytes) {
|
|
2478
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
2479
|
+
}
|
|
2480
|
+
function formatOutcome(outcome) {
|
|
2481
|
+
const lines = Object.entries(outcome.results).map(([sid, r]) => {
|
|
2482
|
+
const skips = r.skipped.slice(0, 3).map((s) => `
|
|
2483
|
+
\xB7 ${s.id}: ${s.reason}`).join("");
|
|
2484
|
+
const warns = r.warnings.slice(0, 3).map((w) => `
|
|
2485
|
+
\u26A0 ${w}`).join("");
|
|
2486
|
+
return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
|
|
2487
|
+
});
|
|
2488
|
+
return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
|
|
2489
|
+
${lines.join("\n")}`;
|
|
2490
|
+
}
|
|
2491
|
+
function registerTimeMachineTools(server, sync) {
|
|
2492
|
+
server.registerTool(
|
|
2493
|
+
"get_time_machine",
|
|
2494
|
+
{
|
|
2495
|
+
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.",
|
|
2496
|
+
inputSchema: {
|
|
2497
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2498
|
+
}
|
|
2499
|
+
},
|
|
2500
|
+
async (args) => {
|
|
2501
|
+
try {
|
|
2502
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2503
|
+
const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
|
|
2504
|
+
const p = payload ?? {};
|
|
2505
|
+
const snapshots = p.snapshots ?? [];
|
|
2506
|
+
const providers = p.providers ?? [];
|
|
2507
|
+
const providerLines = providers.map((pr) => {
|
|
2508
|
+
const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
|
|
2509
|
+
return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
|
|
2510
|
+
});
|
|
2511
|
+
const snapLines = snapshots.map((s) => {
|
|
2512
|
+
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
2513
|
+
const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
2514
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
2515
|
+
});
|
|
2516
|
+
const parts = [
|
|
2517
|
+
`**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
|
|
2518
|
+
snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
|
|
2519
|
+
`**Sources:**
|
|
2520
|
+
${providerLines.join("\n") || "_no providers registered_"}`
|
|
2521
|
+
];
|
|
2522
|
+
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2523
|
+
${formatOutcome(p.lastOutcome)}`);
|
|
2524
|
+
return text11(parts.join("\n\n"));
|
|
2525
|
+
} catch (e) {
|
|
2526
|
+
return fail13(errMsg13(e));
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
);
|
|
2530
|
+
server.registerTool(
|
|
2531
|
+
"time_machine_action",
|
|
2532
|
+
{
|
|
2533
|
+
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.",
|
|
2534
|
+
inputSchema: {
|
|
2535
|
+
action: z13.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2536
|
+
id: z13.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2537
|
+
name: z13.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2538
|
+
mode: z13.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
2539
|
+
sourceIds: z13.array(z13.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2540
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2541
|
+
}
|
|
2542
|
+
},
|
|
2543
|
+
async (args) => {
|
|
2544
|
+
try {
|
|
2545
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2546
|
+
const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
|
|
2547
|
+
switch (args.action) {
|
|
2548
|
+
case "capture": {
|
|
2549
|
+
const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
|
|
2550
|
+
const s = result.snapshot;
|
|
2551
|
+
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2552
|
+
\u26A0 ${sid}: ${w}`)).join("");
|
|
2553
|
+
return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2554
|
+
}
|
|
2555
|
+
case "capture_baseline": {
|
|
2556
|
+
const result = await call("captureBaseline", {});
|
|
2557
|
+
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2558
|
+
}
|
|
2559
|
+
case "restore": {
|
|
2560
|
+
if (!args.id) return fail13("restore requires `id` (from get_time_machine).");
|
|
2561
|
+
const outcome = await call("restore", { id: args.id, mode: args.mode ?? "live" });
|
|
2562
|
+
return text11(formatOutcome(outcome));
|
|
2563
|
+
}
|
|
2564
|
+
case "delete": {
|
|
2565
|
+
if (!args.id) return fail13("delete requires `id`.");
|
|
2566
|
+
await call("delete", { id: args.id });
|
|
2567
|
+
return text11(`Deleted snapshot \`${args.id}\`.`);
|
|
2568
|
+
}
|
|
2569
|
+
case "rename": {
|
|
2570
|
+
if (!args.id || !args.name) return fail13("rename requires `id` and `name`.");
|
|
2571
|
+
await call("rename", { id: args.id, name: args.name });
|
|
2572
|
+
return text11(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
} catch (e) {
|
|
2576
|
+
return fail13(errMsg13(e));
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/tools/packs/interact.ts
|
|
2583
|
+
import { z as z14 } from "zod";
|
|
2584
|
+
var TOOL_ID2 = "highlight-updates";
|
|
2585
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2586
|
+
var fail14 = (message) => ({
|
|
2476
2587
|
isError: true,
|
|
2477
2588
|
content: [{ type: "text", text: message }]
|
|
2478
2589
|
});
|
|
2479
|
-
var
|
|
2590
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2480
2591
|
function formatElement(e) {
|
|
2481
2592
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2482
2593
|
line += e.name;
|
|
@@ -2496,8 +2607,8 @@ function registerInteractTools(server, sync) {
|
|
|
2496
2607
|
{
|
|
2497
2608
|
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.",
|
|
2498
2609
|
inputSchema: {
|
|
2499
|
-
interactiveOnly:
|
|
2500
|
-
deviceId:
|
|
2610
|
+
interactiveOnly: z14.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
2611
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2501
2612
|
}
|
|
2502
2613
|
},
|
|
2503
2614
|
async (args) => {
|
|
@@ -2507,16 +2618,16 @@ function registerInteractTools(server, sync) {
|
|
|
2507
2618
|
const all = res?.elements ?? [];
|
|
2508
2619
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2509
2620
|
if (elements.length === 0) {
|
|
2510
|
-
return
|
|
2621
|
+
return text12(
|
|
2511
2622
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
2512
2623
|
);
|
|
2513
2624
|
}
|
|
2514
2625
|
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 }).`;
|
|
2515
|
-
return
|
|
2626
|
+
return text12(`${header}
|
|
2516
2627
|
|
|
2517
2628
|
${elements.map(formatElement).join("\n")}`);
|
|
2518
2629
|
} catch (e) {
|
|
2519
|
-
return
|
|
2630
|
+
return fail14(errMsg14(e));
|
|
2520
2631
|
}
|
|
2521
2632
|
}
|
|
2522
2633
|
);
|
|
@@ -2525,20 +2636,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2525
2636
|
{
|
|
2526
2637
|
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.",
|
|
2527
2638
|
inputSchema: {
|
|
2528
|
-
nativeTag:
|
|
2529
|
-
testID:
|
|
2530
|
-
query:
|
|
2531
|
-
value:
|
|
2532
|
-
text:
|
|
2533
|
-
longPress:
|
|
2534
|
-
scrollIntoView:
|
|
2535
|
-
deviceId:
|
|
2639
|
+
nativeTag: z14.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2640
|
+
testID: z14.string().optional().describe("Exact testID of the element."),
|
|
2641
|
+
query: z14.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2642
|
+
value: z14.union([z14.number(), z14.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
2643
|
+
text: z14.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2644
|
+
longPress: z14.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
2645
|
+
scrollIntoView: z14.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2646
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2536
2647
|
}
|
|
2537
2648
|
},
|
|
2538
2649
|
async (args) => {
|
|
2539
2650
|
try {
|
|
2540
2651
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2541
|
-
return
|
|
2652
|
+
return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2542
2653
|
}
|
|
2543
2654
|
const target = sync.requireProDevice(args.deviceId);
|
|
2544
2655
|
const res = await sync.callAction(
|
|
@@ -2561,26 +2672,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2561
2672
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2562
2673
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
2563
2674
|
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}`;
|
|
2564
|
-
return
|
|
2675
|
+
return text12(`${verb}${scrolledNote}.`);
|
|
2565
2676
|
}
|
|
2566
2677
|
let msg = res?.reason ?? "Tap failed.";
|
|
2567
2678
|
if (res?.candidates?.length) {
|
|
2568
2679
|
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");
|
|
2569
2680
|
}
|
|
2570
|
-
return
|
|
2681
|
+
return fail14(msg);
|
|
2571
2682
|
} catch (e) {
|
|
2572
|
-
return
|
|
2683
|
+
return fail14(errMsg14(e));
|
|
2573
2684
|
}
|
|
2574
2685
|
}
|
|
2575
2686
|
);
|
|
2576
2687
|
}
|
|
2577
2688
|
|
|
2578
2689
|
// src/tools/packs/jsThread.ts
|
|
2579
|
-
import { z as
|
|
2690
|
+
import { z as z15 } from "zod";
|
|
2580
2691
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2581
|
-
var
|
|
2582
|
-
var
|
|
2583
|
-
var
|
|
2692
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2693
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2694
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2584
2695
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2585
2696
|
function sparkline(values) {
|
|
2586
2697
|
const max = Math.max(1, ...values);
|
|
@@ -2651,9 +2762,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2651
2762
|
{
|
|
2652
2763
|
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.",
|
|
2653
2764
|
inputSchema: {
|
|
2654
|
-
deviceId:
|
|
2655
|
-
durationMs:
|
|
2656
|
-
clearFirst:
|
|
2765
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2766
|
+
durationMs: z15.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2767
|
+
clearFirst: z15.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2657
2768
|
}
|
|
2658
2769
|
},
|
|
2659
2770
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2666,7 +2777,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2666
2777
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2667
2778
|
target
|
|
2668
2779
|
);
|
|
2669
|
-
if (!snap) return
|
|
2780
|
+
if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2670
2781
|
const md = formatTopMarkdown(snap, duration);
|
|
2671
2782
|
const slim = {
|
|
2672
2783
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2685,13 +2796,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2685
2796
|
worstFreezeMs: r.worstFreezeMs
|
|
2686
2797
|
}))
|
|
2687
2798
|
};
|
|
2688
|
-
return
|
|
2799
|
+
return text13(`${md}
|
|
2689
2800
|
|
|
2690
2801
|
\`\`\`json
|
|
2691
2802
|
${JSON.stringify(slim)}
|
|
2692
2803
|
\`\`\``);
|
|
2693
2804
|
} catch (e) {
|
|
2694
|
-
return
|
|
2805
|
+
return fail15(errMsg15(e));
|
|
2695
2806
|
}
|
|
2696
2807
|
}
|
|
2697
2808
|
);
|
|
@@ -2700,8 +2811,8 @@ ${JSON.stringify(slim)}
|
|
|
2700
2811
|
{
|
|
2701
2812
|
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).',
|
|
2702
2813
|
inputSchema: {
|
|
2703
|
-
deviceId:
|
|
2704
|
-
key:
|
|
2814
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2815
|
+
key: z15.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2705
2816
|
}
|
|
2706
2817
|
},
|
|
2707
2818
|
async ({ deviceId, key }) => {
|
|
@@ -2714,18 +2825,18 @@ ${JSON.stringify(slim)}
|
|
|
2714
2825
|
target
|
|
2715
2826
|
);
|
|
2716
2827
|
if (!detail) {
|
|
2717
|
-
return
|
|
2828
|
+
return fail15(
|
|
2718
2829
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2719
2830
|
);
|
|
2720
2831
|
}
|
|
2721
2832
|
const md = formatDetailMarkdown(detail);
|
|
2722
|
-
return
|
|
2833
|
+
return text13(`${md}
|
|
2723
2834
|
|
|
2724
2835
|
\`\`\`json
|
|
2725
2836
|
${JSON.stringify(detail)}
|
|
2726
2837
|
\`\`\``);
|
|
2727
2838
|
} catch (e) {
|
|
2728
|
-
return
|
|
2839
|
+
return fail15(errMsg15(e));
|
|
2729
2840
|
}
|
|
2730
2841
|
}
|
|
2731
2842
|
);
|
|
@@ -2734,27 +2845,27 @@ ${JSON.stringify(detail)}
|
|
|
2734
2845
|
{
|
|
2735
2846
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2736
2847
|
inputSchema: {
|
|
2737
|
-
deviceId:
|
|
2848
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2738
2849
|
}
|
|
2739
2850
|
},
|
|
2740
2851
|
async ({ deviceId }) => {
|
|
2741
2852
|
try {
|
|
2742
2853
|
const target = sync.requireProDevice(deviceId);
|
|
2743
2854
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2744
|
-
return
|
|
2855
|
+
return text13("Cleared js-top stats.");
|
|
2745
2856
|
} catch (e) {
|
|
2746
|
-
return
|
|
2857
|
+
return fail15(errMsg15(e));
|
|
2747
2858
|
}
|
|
2748
2859
|
}
|
|
2749
2860
|
);
|
|
2750
2861
|
}
|
|
2751
2862
|
|
|
2752
2863
|
// src/tools/packs/images.ts
|
|
2753
|
-
import { z as
|
|
2864
|
+
import { z as z16 } from "zod";
|
|
2754
2865
|
var IMAGES_TOOL_ID = "images";
|
|
2755
|
-
var
|
|
2756
|
-
var
|
|
2757
|
-
var
|
|
2866
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2867
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2868
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2758
2869
|
function uriTail(uri) {
|
|
2759
2870
|
if (!uri) return "(no source)";
|
|
2760
2871
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2842,9 +2953,9 @@ function registerImagesTools(server, sync) {
|
|
|
2842
2953
|
{
|
|
2843
2954
|
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.",
|
|
2844
2955
|
inputSchema: {
|
|
2845
|
-
deviceId:
|
|
2846
|
-
limit:
|
|
2847
|
-
status:
|
|
2956
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2957
|
+
limit: z16.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
2958
|
+
status: z16.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2848
2959
|
}
|
|
2849
2960
|
},
|
|
2850
2961
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2857,7 +2968,7 @@ function registerImagesTools(server, sync) {
|
|
|
2857
2968
|
{ limit: capped, status },
|
|
2858
2969
|
target
|
|
2859
2970
|
);
|
|
2860
|
-
if (!list) return
|
|
2971
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2861
2972
|
const md = formatListMarkdown(list, capped);
|
|
2862
2973
|
const slim = {
|
|
2863
2974
|
stats: list.stats,
|
|
@@ -2884,13 +2995,13 @@ function registerImagesTools(server, sync) {
|
|
|
2884
2995
|
layoutShifts: r.layoutShifts
|
|
2885
2996
|
}))
|
|
2886
2997
|
};
|
|
2887
|
-
return
|
|
2998
|
+
return text14(`${md}
|
|
2888
2999
|
|
|
2889
3000
|
\`\`\`json
|
|
2890
3001
|
${JSON.stringify(slim)}
|
|
2891
3002
|
\`\`\``);
|
|
2892
3003
|
} catch (e) {
|
|
2893
|
-
return
|
|
3004
|
+
return fail16(errMsg16(e));
|
|
2894
3005
|
}
|
|
2895
3006
|
}
|
|
2896
3007
|
);
|
|
@@ -2899,9 +3010,9 @@ ${JSON.stringify(slim)}
|
|
|
2899
3010
|
{
|
|
2900
3011
|
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.",
|
|
2901
3012
|
inputSchema: {
|
|
2902
|
-
deviceId:
|
|
2903
|
-
id:
|
|
2904
|
-
action:
|
|
3013
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3014
|
+
id: z16.number().describe("Record id from get_images."),
|
|
3015
|
+
action: z16.enum([
|
|
2905
3016
|
"hard_reload",
|
|
2906
3017
|
"retry",
|
|
2907
3018
|
"evict_disk",
|
|
@@ -2913,7 +3024,7 @@ ${JSON.stringify(slim)}
|
|
|
2913
3024
|
"swap_url",
|
|
2914
3025
|
"restore"
|
|
2915
3026
|
]).describe("What to do."),
|
|
2916
|
-
url:
|
|
3027
|
+
url: z16.string().optional().describe("Replacement URL (swap_url only).")
|
|
2917
3028
|
}
|
|
2918
3029
|
},
|
|
2919
3030
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2921,8 +3032,8 @@ ${JSON.stringify(slim)}
|
|
|
2921
3032
|
const target = sync.requireProDevice(deviceId);
|
|
2922
3033
|
if (action === "prove_savings") {
|
|
2923
3034
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2924
|
-
if (!savings) return
|
|
2925
|
-
if (!savings.ok) return
|
|
3035
|
+
if (!savings) return fail16("Device returned nothing for proveSavings.");
|
|
3036
|
+
if (!savings.ok) return fail16(savings.message);
|
|
2926
3037
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2927
3038
|
if (savings.originalBytes != null)
|
|
2928
3039
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -2934,11 +3045,11 @@ ${JSON.stringify(slim)}
|
|
|
2934
3045
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2935
3046
|
if (savings.optimizedUri)
|
|
2936
3047
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2937
|
-
return
|
|
3048
|
+
return text14(parts.join("\n"));
|
|
2938
3049
|
}
|
|
2939
3050
|
const entry = ACTION_MAP[action];
|
|
2940
|
-
if (!entry) return
|
|
2941
|
-
if (action === "swap_url" && !url) return
|
|
3051
|
+
if (!entry) return fail16(`Unknown action: ${action}`);
|
|
3052
|
+
if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
|
|
2942
3053
|
const result = await sync.callAction(
|
|
2943
3054
|
IMAGES_TOOL_ID,
|
|
2944
3055
|
entry.action,
|
|
@@ -2958,9 +3069,9 @@ ${JSON.stringify(slim)}
|
|
|
2958
3069
|
);
|
|
2959
3070
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2960
3071
|
}
|
|
2961
|
-
return
|
|
3072
|
+
return text14(lines.join("\n"));
|
|
2962
3073
|
} catch (e) {
|
|
2963
|
-
return
|
|
3074
|
+
return fail16(errMsg16(e));
|
|
2964
3075
|
}
|
|
2965
3076
|
}
|
|
2966
3077
|
);
|
|
@@ -2969,12 +3080,12 @@ ${JSON.stringify(slim)}
|
|
|
2969
3080
|
{
|
|
2970
3081
|
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.",
|
|
2971
3082
|
inputSchema: {
|
|
2972
|
-
deviceId:
|
|
2973
|
-
network:
|
|
2974
|
-
blankImages:
|
|
2975
|
-
massAction:
|
|
2976
|
-
clearRegistry:
|
|
2977
|
-
clearExpoCaches:
|
|
3083
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3084
|
+
network: z16.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3085
|
+
blankImages: z16.boolean().optional().describe("Render all images without a source."),
|
|
3086
|
+
massAction: z16.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."),
|
|
3087
|
+
clearRegistry: z16.boolean().optional().describe("Clear the captured image log."),
|
|
3088
|
+
clearExpoCaches: z16.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
2978
3089
|
}
|
|
2979
3090
|
},
|
|
2980
3091
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3001,26 +3112,243 @@ ${JSON.stringify(slim)}
|
|
|
3001
3112
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3002
3113
|
done.push(r?.message ?? "expo caches cleared");
|
|
3003
3114
|
}
|
|
3004
|
-
if (done.length === 0) return
|
|
3005
|
-
return
|
|
3115
|
+
if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3116
|
+
return text14(`Applied: ${done.join(" \xB7 ")}`);
|
|
3006
3117
|
} catch (e) {
|
|
3007
|
-
return
|
|
3118
|
+
return fail16(errMsg16(e));
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/tools/packs/assets.ts
|
|
3125
|
+
import { z as z17 } from "zod";
|
|
3126
|
+
var ASSETS_TOOL_ID = "assets";
|
|
3127
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3128
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3129
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3130
|
+
function kb2(bytes) {
|
|
3131
|
+
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3132
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3133
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3134
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
3135
|
+
}
|
|
3136
|
+
function sizeLabel(r) {
|
|
3137
|
+
if (r.sizeBytes == null) return "\u2013";
|
|
3138
|
+
return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
|
|
3139
|
+
}
|
|
3140
|
+
function scalesLabel(scales) {
|
|
3141
|
+
if (!scales.length) return "\u2013";
|
|
3142
|
+
return scales.map((s) => `@${s}x`).join(" ");
|
|
3143
|
+
}
|
|
3144
|
+
function flagsLabel(r, ins) {
|
|
3145
|
+
const flags = [];
|
|
3146
|
+
if (!r.loaded) flags.push("UNUSED");
|
|
3147
|
+
if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
|
|
3148
|
+
if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
|
|
3149
|
+
if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
|
|
3150
|
+
if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
|
|
3151
|
+
else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
|
|
3152
|
+
if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
|
|
3153
|
+
return flags.length ? flags.join(" ") : "\u2013";
|
|
3154
|
+
}
|
|
3155
|
+
function formatListMarkdown2(list, limit) {
|
|
3156
|
+
const lines = [];
|
|
3157
|
+
const s = list.stats;
|
|
3158
|
+
const ss = list.scanStatus;
|
|
3159
|
+
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3160
|
+
lines.push("");
|
|
3161
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
|
|
3162
|
+
lines.push(
|
|
3163
|
+
`${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 ")}`
|
|
3164
|
+
);
|
|
3165
|
+
if (ss.fontFamilies.length > 0) {
|
|
3166
|
+
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
3167
|
+
}
|
|
3168
|
+
if (!ss.graphSupported || ss.graphCount == null) {
|
|
3169
|
+
lines.push(
|
|
3170
|
+
"\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
|
|
3174
|
+
const ins = list.insights;
|
|
3175
|
+
const chips = [];
|
|
3176
|
+
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3177
|
+
if (ins.duplicates.length) {
|
|
3178
|
+
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3179
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
|
|
3180
|
+
}
|
|
3181
|
+
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3182
|
+
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
3183
|
+
if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
|
|
3184
|
+
if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
|
|
3185
|
+
if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
|
|
3186
|
+
if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
|
|
3187
|
+
if (chips.length) {
|
|
3188
|
+
lines.push("");
|
|
3189
|
+
lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
|
|
3190
|
+
}
|
|
3191
|
+
const diff = list.diff;
|
|
3192
|
+
if (diff) {
|
|
3193
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
|
|
3194
|
+
lines.push("");
|
|
3195
|
+
lines.push(
|
|
3196
|
+
`**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" : ""}` : "")
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
lines.push("");
|
|
3200
|
+
lines.push("| id | asset | kind | dims | scales | size | flags |");
|
|
3201
|
+
lines.push("|---|---|---|---|---|---|---|");
|
|
3202
|
+
for (const r of list.records) {
|
|
3203
|
+
const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
|
|
3204
|
+
lines.push(
|
|
3205
|
+
`| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
lines.push("");
|
|
3209
|
+
lines.push(
|
|
3210
|
+
"_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
|
|
3211
|
+
);
|
|
3212
|
+
return lines.join("\n");
|
|
3213
|
+
}
|
|
3214
|
+
function registerAssetsTools(server, sync) {
|
|
3215
|
+
server.registerTool(
|
|
3216
|
+
"get_assets",
|
|
3217
|
+
{
|
|
3218
|
+
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.",
|
|
3219
|
+
inputSchema: {
|
|
3220
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3221
|
+
limit: z17.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3222
|
+
kind: z17.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3223
|
+
unusedOnly: z17.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3224
|
+
}
|
|
3225
|
+
},
|
|
3226
|
+
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
3227
|
+
try {
|
|
3228
|
+
const target = sync.requireProDevice(deviceId);
|
|
3229
|
+
const capped = Math.min(Math.max(limit ?? 40, 1), 500);
|
|
3230
|
+
const list = await sync.callAction(
|
|
3231
|
+
ASSETS_TOOL_ID,
|
|
3232
|
+
"list",
|
|
3233
|
+
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3234
|
+
target
|
|
3235
|
+
);
|
|
3236
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3237
|
+
if (unusedOnly) {
|
|
3238
|
+
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3239
|
+
}
|
|
3240
|
+
const md = formatListMarkdown2(list, capped);
|
|
3241
|
+
const slim = {
|
|
3242
|
+
stats: list.stats,
|
|
3243
|
+
scanStatus: list.scanStatus,
|
|
3244
|
+
insights: list.insights,
|
|
3245
|
+
diff: list.diff,
|
|
3246
|
+
records: list.records.map((r) => ({
|
|
3247
|
+
id: r.id,
|
|
3248
|
+
name: r.name,
|
|
3249
|
+
type: r.type,
|
|
3250
|
+
kind: r.kind,
|
|
3251
|
+
hash: r.hash,
|
|
3252
|
+
width: r.width,
|
|
3253
|
+
height: r.height,
|
|
3254
|
+
scales: r.scales,
|
|
3255
|
+
loaded: r.loaded,
|
|
3256
|
+
sizeBytes: r.sizeBytes,
|
|
3257
|
+
sizeSource: r.sizeSource
|
|
3258
|
+
}))
|
|
3259
|
+
};
|
|
3260
|
+
return text15(`${md}
|
|
3261
|
+
|
|
3262
|
+
\`\`\`json
|
|
3263
|
+
${JSON.stringify(slim)}
|
|
3264
|
+
\`\`\``);
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
return fail17(errMsg17(e));
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
);
|
|
3270
|
+
server.registerTool(
|
|
3271
|
+
"asset_action",
|
|
3272
|
+
{
|
|
3273
|
+
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).",
|
|
3274
|
+
inputSchema: {
|
|
3275
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3276
|
+
action: z17.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3277
|
+
id: z17.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3278
|
+
}
|
|
3279
|
+
},
|
|
3280
|
+
async ({ deviceId, action, id }) => {
|
|
3281
|
+
try {
|
|
3282
|
+
const target = sync.requireProDevice(deviceId);
|
|
3283
|
+
switch (action) {
|
|
3284
|
+
case "rescan": {
|
|
3285
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3286
|
+
const ss = result?.scanStatus;
|
|
3287
|
+
if (!ss) return fail17("Device returned nothing for rescan.");
|
|
3288
|
+
return text15(
|
|
3289
|
+
`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" : "")
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
case "measure_sizes": {
|
|
3293
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3294
|
+
return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3295
|
+
}
|
|
3296
|
+
case "detail": {
|
|
3297
|
+
if (id == null) return fail17("detail requires the `id` param (from get_assets).");
|
|
3298
|
+
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3299
|
+
if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3300
|
+
const lines = [
|
|
3301
|
+
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3302
|
+
"",
|
|
3303
|
+
`- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
|
|
3304
|
+
];
|
|
3305
|
+
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3306
|
+
if (detail.sizesByScale) {
|
|
3307
|
+
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3308
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3312
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
|
|
3313
|
+
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3314
|
+
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3315
|
+
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3316
|
+
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3317
|
+
return text15(lines.join("\n"));
|
|
3318
|
+
}
|
|
3319
|
+
case "save_baseline": {
|
|
3320
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3321
|
+
return text15(
|
|
3322
|
+
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
case "clear_baseline": {
|
|
3326
|
+
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3327
|
+
return text15("Baseline cleared.");
|
|
3328
|
+
}
|
|
3329
|
+
case "clear_records": {
|
|
3330
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3331
|
+
return text15(result?.message ?? "Inventory cleared.");
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
} catch (e) {
|
|
3335
|
+
return fail17(errMsg17(e));
|
|
3008
3336
|
}
|
|
3009
3337
|
}
|
|
3010
3338
|
);
|
|
3011
3339
|
}
|
|
3012
3340
|
|
|
3013
3341
|
// src/tools/packs/reload.ts
|
|
3014
|
-
import { z as
|
|
3342
|
+
import { z as z18 } from "zod";
|
|
3015
3343
|
var APP_TOOL_ID = "app";
|
|
3016
3344
|
var RELOAD_ACTION = "reloadApp";
|
|
3017
3345
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3018
3346
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3019
3347
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3020
3348
|
var PROBE_INTERVAL_MS = 400;
|
|
3021
|
-
var
|
|
3022
|
-
var
|
|
3023
|
-
var
|
|
3349
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3350
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3351
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3024
3352
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3025
3353
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3026
3354
|
try {
|
|
@@ -3055,12 +3383,12 @@ function registerReloadTools(server, sync) {
|
|
|
3055
3383
|
{
|
|
3056
3384
|
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.",
|
|
3057
3385
|
inputSchema: {
|
|
3058
|
-
strategy:
|
|
3386
|
+
strategy: z18.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3059
3387
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3060
3388
|
),
|
|
3061
|
-
wait:
|
|
3062
|
-
timeoutMs:
|
|
3063
|
-
deviceId:
|
|
3389
|
+
wait: z18.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3390
|
+
timeoutMs: z18.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3391
|
+
deviceId: z18.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3064
3392
|
}
|
|
3065
3393
|
},
|
|
3066
3394
|
async (args) => {
|
|
@@ -3076,19 +3404,19 @@ function registerReloadTools(server, sync) {
|
|
|
3076
3404
|
);
|
|
3077
3405
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3078
3406
|
if (args.wait === false) {
|
|
3079
|
-
return
|
|
3407
|
+
return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3080
3408
|
}
|
|
3081
3409
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3082
3410
|
if (elapsed == null) {
|
|
3083
|
-
return
|
|
3411
|
+
return fail18(
|
|
3084
3412
|
`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.`
|
|
3085
3413
|
);
|
|
3086
3414
|
}
|
|
3087
|
-
return
|
|
3415
|
+
return text16(
|
|
3088
3416
|
`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.`
|
|
3089
3417
|
);
|
|
3090
3418
|
} catch (e) {
|
|
3091
|
-
return
|
|
3419
|
+
return fail18(errMsg18(e));
|
|
3092
3420
|
}
|
|
3093
3421
|
}
|
|
3094
3422
|
);
|
|
@@ -3132,11 +3460,25 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
3132
3460
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
3133
3461
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
3134
3462
|
|
|
3463
|
+
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
3464
|
+
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
3465
|
+
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
3466
|
+
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
3467
|
+
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
3468
|
+
|
|
3135
3469
|
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
3136
3470
|
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
3137
3471
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
3138
3472
|
re-read anything you cached.
|
|
3139
3473
|
|
|
3474
|
+
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
3475
|
+
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
3476
|
+
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
3477
|
+
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
3478
|
+
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
3479
|
+
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
3480
|
+
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
3481
|
+
|
|
3140
3482
|
For everything else:
|
|
3141
3483
|
- \`list_tool_actions\` to see what a tool can do,
|
|
3142
3484
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -3164,15 +3506,17 @@ function createMcpServer(sync) {
|
|
|
3164
3506
|
registerStorageTools(server, sync);
|
|
3165
3507
|
registerRoutesTools(server, sync);
|
|
3166
3508
|
registerConsoleTools(server, sync);
|
|
3509
|
+
registerTimeMachineTools(server, sync);
|
|
3167
3510
|
registerScreenshotTools(server, sync);
|
|
3168
3511
|
registerInteractTools(server, sync);
|
|
3169
3512
|
registerJsThreadTools(server, sync);
|
|
3170
3513
|
registerImagesTools(server, sync);
|
|
3514
|
+
registerAssetsTools(server, sync);
|
|
3171
3515
|
registerReloadTools(server, sync);
|
|
3172
3516
|
return server;
|
|
3173
3517
|
}
|
|
3174
3518
|
var deviceIdField = {
|
|
3175
|
-
deviceId:
|
|
3519
|
+
deviceId: z19.string().optional().describe(
|
|
3176
3520
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3177
3521
|
)
|
|
3178
3522
|
};
|