@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/index.js
CHANGED
|
@@ -365,7 +365,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
365
365
|
|
|
366
366
|
// src/server.ts
|
|
367
367
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
368
|
-
var
|
|
368
|
+
var import_zod19 = require("zod");
|
|
369
369
|
|
|
370
370
|
// src/tools/generic.ts
|
|
371
371
|
var import_zod = require("zod");
|
|
@@ -376,7 +376,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
376
376
|
var import_node_path = __toESM(require("path"));
|
|
377
377
|
|
|
378
378
|
// src/version.ts
|
|
379
|
-
var VERSION = "7.0.
|
|
379
|
+
var VERSION = "7.0.17" ? "7.0.17" : "0.0.0-dev";
|
|
380
380
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
381
381
|
|
|
382
382
|
// src/update/checkForUpdate.ts
|
|
@@ -2361,8 +2361,119 @@ _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
|
|
|
2361
2361
|
);
|
|
2362
2362
|
}
|
|
2363
2363
|
|
|
2364
|
-
// src/tools/packs/
|
|
2364
|
+
// src/tools/packs/timeMachine.ts
|
|
2365
2365
|
var import_zod12 = require("zod");
|
|
2366
|
+
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2367
|
+
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2368
|
+
var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2369
|
+
var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
|
|
2370
|
+
function kb(bytes) {
|
|
2371
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
2372
|
+
}
|
|
2373
|
+
function formatOutcome(outcome) {
|
|
2374
|
+
const lines = Object.entries(outcome.results).map(([sid, r]) => {
|
|
2375
|
+
const skips = r.skipped.slice(0, 3).map((s) => `
|
|
2376
|
+
\xB7 ${s.id}: ${s.reason}`).join("");
|
|
2377
|
+
const warns = r.warnings.slice(0, 3).map((w) => `
|
|
2378
|
+
\u26A0 ${w}`).join("");
|
|
2379
|
+
return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
|
|
2380
|
+
});
|
|
2381
|
+
return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
|
|
2382
|
+
${lines.join("\n")}`;
|
|
2383
|
+
}
|
|
2384
|
+
function registerTimeMachineTools(server, sync) {
|
|
2385
|
+
server.registerTool(
|
|
2386
|
+
"get_time_machine",
|
|
2387
|
+
{
|
|
2388
|
+
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.",
|
|
2389
|
+
inputSchema: {
|
|
2390
|
+
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2391
|
+
}
|
|
2392
|
+
},
|
|
2393
|
+
async (args) => {
|
|
2394
|
+
try {
|
|
2395
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2396
|
+
const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
|
|
2397
|
+
const p = payload ?? {};
|
|
2398
|
+
const snapshots = p.snapshots ?? [];
|
|
2399
|
+
const providers = p.providers ?? [];
|
|
2400
|
+
const providerLines = providers.map((pr) => {
|
|
2401
|
+
const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
|
|
2402
|
+
return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
|
|
2403
|
+
});
|
|
2404
|
+
const snapLines = snapshots.map((s) => {
|
|
2405
|
+
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
2406
|
+
const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
2407
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
2408
|
+
});
|
|
2409
|
+
const parts = [
|
|
2410
|
+
`**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
|
|
2411
|
+
snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
|
|
2412
|
+
`**Sources:**
|
|
2413
|
+
${providerLines.join("\n") || "_no providers registered_"}`
|
|
2414
|
+
];
|
|
2415
|
+
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2416
|
+
${formatOutcome(p.lastOutcome)}`);
|
|
2417
|
+
return text11(parts.join("\n\n"));
|
|
2418
|
+
} catch (e) {
|
|
2419
|
+
return fail12(errMsg12(e));
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
);
|
|
2423
|
+
server.registerTool(
|
|
2424
|
+
"time_machine_action",
|
|
2425
|
+
{
|
|
2426
|
+
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.",
|
|
2427
|
+
inputSchema: {
|
|
2428
|
+
action: import_zod12.z.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2429
|
+
id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2430
|
+
name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2431
|
+
mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
2432
|
+
sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2433
|
+
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
2436
|
+
async (args) => {
|
|
2437
|
+
try {
|
|
2438
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2439
|
+
const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
|
|
2440
|
+
switch (args.action) {
|
|
2441
|
+
case "capture": {
|
|
2442
|
+
const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
|
|
2443
|
+
const s = result.snapshot;
|
|
2444
|
+
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2445
|
+
\u26A0 ${sid}: ${w}`)).join("");
|
|
2446
|
+
return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2447
|
+
}
|
|
2448
|
+
case "capture_baseline": {
|
|
2449
|
+
const result = await call("captureBaseline", {});
|
|
2450
|
+
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2451
|
+
}
|
|
2452
|
+
case "restore": {
|
|
2453
|
+
if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
|
|
2454
|
+
const outcome = await call("restore", { id: args.id, mode: args.mode ?? "live" });
|
|
2455
|
+
return text11(formatOutcome(outcome));
|
|
2456
|
+
}
|
|
2457
|
+
case "delete": {
|
|
2458
|
+
if (!args.id) return fail12("delete requires `id`.");
|
|
2459
|
+
await call("delete", { id: args.id });
|
|
2460
|
+
return text11(`Deleted snapshot \`${args.id}\`.`);
|
|
2461
|
+
}
|
|
2462
|
+
case "rename": {
|
|
2463
|
+
if (!args.id || !args.name) return fail12("rename requires `id` and `name`.");
|
|
2464
|
+
await call("rename", { id: args.id, name: args.name });
|
|
2465
|
+
return text11(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
} catch (e) {
|
|
2469
|
+
return fail12(errMsg12(e));
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/tools/packs/screenshot.ts
|
|
2476
|
+
var import_zod13 = require("zod");
|
|
2366
2477
|
|
|
2367
2478
|
// src/capture/simctl.ts
|
|
2368
2479
|
var import_node_child_process = require("child_process");
|
|
@@ -2442,8 +2553,8 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
2442
2553
|
var TOOL_ID = "highlight-updates";
|
|
2443
2554
|
var MARGIN_PT = 12;
|
|
2444
2555
|
var CAPTURE_PADDING_PT = 6;
|
|
2445
|
-
var
|
|
2446
|
-
var
|
|
2556
|
+
var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2557
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2447
2558
|
function reasonMessage(res, query) {
|
|
2448
2559
|
switch (res.reason) {
|
|
2449
2560
|
case "no-renders":
|
|
@@ -2473,10 +2584,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
2473
2584
|
{
|
|
2474
2585
|
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.",
|
|
2475
2586
|
inputSchema: {
|
|
2476
|
-
query:
|
|
2477
|
-
nativeTag:
|
|
2478
|
-
scrollIntoView:
|
|
2479
|
-
deviceId:
|
|
2587
|
+
query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2588
|
+
nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
2589
|
+
scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
2590
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2480
2591
|
}
|
|
2481
2592
|
},
|
|
2482
2593
|
async (args) => {
|
|
@@ -2496,11 +2607,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2496
2607
|
target
|
|
2497
2608
|
);
|
|
2498
2609
|
if (!res.matched || !res.rect || !res.screen) {
|
|
2499
|
-
return
|
|
2610
|
+
return fail13(reasonMessage(res, args.query) + candidatesText(res.candidates));
|
|
2500
2611
|
}
|
|
2501
2612
|
const shot = await captureIosFramebuffer();
|
|
2502
2613
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
2503
|
-
return
|
|
2614
|
+
return fail13(shot.error ?? "Screenshot capture failed.");
|
|
2504
2615
|
}
|
|
2505
2616
|
const pxScale = shot.width / res.screen.width;
|
|
2506
2617
|
const r = res.rect;
|
|
@@ -2516,7 +2627,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2516
2627
|
}
|
|
2517
2628
|
);
|
|
2518
2629
|
if (!crop.ok || !crop.buffer) {
|
|
2519
|
-
return
|
|
2630
|
+
return fail13(crop.error ?? "Crop failed.");
|
|
2520
2631
|
}
|
|
2521
2632
|
const label = res.componentName || "component";
|
|
2522
2633
|
const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
|
|
@@ -2529,21 +2640,21 @@ function registerScreenshotTools(server, sync) {
|
|
|
2529
2640
|
]
|
|
2530
2641
|
};
|
|
2531
2642
|
} catch (e) {
|
|
2532
|
-
return
|
|
2643
|
+
return fail13(errMsg13(e));
|
|
2533
2644
|
}
|
|
2534
2645
|
}
|
|
2535
2646
|
);
|
|
2536
2647
|
}
|
|
2537
2648
|
|
|
2538
2649
|
// src/tools/packs/interact.ts
|
|
2539
|
-
var
|
|
2650
|
+
var import_zod14 = require("zod");
|
|
2540
2651
|
var TOOL_ID2 = "highlight-updates";
|
|
2541
|
-
var
|
|
2542
|
-
var
|
|
2652
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2653
|
+
var fail14 = (message) => ({
|
|
2543
2654
|
isError: true,
|
|
2544
2655
|
content: [{ type: "text", text: message }]
|
|
2545
2656
|
});
|
|
2546
|
-
var
|
|
2657
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2547
2658
|
function formatElement(e) {
|
|
2548
2659
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2549
2660
|
line += e.name;
|
|
@@ -2563,8 +2674,8 @@ function registerInteractTools(server, sync) {
|
|
|
2563
2674
|
{
|
|
2564
2675
|
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.",
|
|
2565
2676
|
inputSchema: {
|
|
2566
|
-
interactiveOnly:
|
|
2567
|
-
deviceId:
|
|
2677
|
+
interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
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) => {
|
|
@@ -2574,16 +2685,16 @@ function registerInteractTools(server, sync) {
|
|
|
2574
2685
|
const all = res?.elements ?? [];
|
|
2575
2686
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2576
2687
|
if (elements.length === 0) {
|
|
2577
|
-
return
|
|
2688
|
+
return text12(
|
|
2578
2689
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
2579
2690
|
);
|
|
2580
2691
|
}
|
|
2581
2692
|
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 }).`;
|
|
2582
|
-
return
|
|
2693
|
+
return text12(`${header}
|
|
2583
2694
|
|
|
2584
2695
|
${elements.map(formatElement).join("\n")}`);
|
|
2585
2696
|
} catch (e) {
|
|
2586
|
-
return
|
|
2697
|
+
return fail14(errMsg14(e));
|
|
2587
2698
|
}
|
|
2588
2699
|
}
|
|
2589
2700
|
);
|
|
@@ -2592,20 +2703,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2592
2703
|
{
|
|
2593
2704
|
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.",
|
|
2594
2705
|
inputSchema: {
|
|
2595
|
-
nativeTag:
|
|
2596
|
-
testID:
|
|
2597
|
-
query:
|
|
2598
|
-
value:
|
|
2599
|
-
text:
|
|
2600
|
-
longPress:
|
|
2601
|
-
scrollIntoView:
|
|
2602
|
-
deviceId:
|
|
2706
|
+
nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2707
|
+
testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
|
|
2708
|
+
query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2709
|
+
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)."),
|
|
2710
|
+
text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2711
|
+
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."),
|
|
2712
|
+
scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2713
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2603
2714
|
}
|
|
2604
2715
|
},
|
|
2605
2716
|
async (args) => {
|
|
2606
2717
|
try {
|
|
2607
2718
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2608
|
-
return
|
|
2719
|
+
return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2609
2720
|
}
|
|
2610
2721
|
const target = sync.requireProDevice(args.deviceId);
|
|
2611
2722
|
const res = await sync.callAction(
|
|
@@ -2628,26 +2739,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2628
2739
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2629
2740
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
2630
2741
|
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}`;
|
|
2631
|
-
return
|
|
2742
|
+
return text12(`${verb}${scrolledNote}.`);
|
|
2632
2743
|
}
|
|
2633
2744
|
let msg = res?.reason ?? "Tap failed.";
|
|
2634
2745
|
if (res?.candidates?.length) {
|
|
2635
2746
|
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");
|
|
2636
2747
|
}
|
|
2637
|
-
return
|
|
2748
|
+
return fail14(msg);
|
|
2638
2749
|
} catch (e) {
|
|
2639
|
-
return
|
|
2750
|
+
return fail14(errMsg14(e));
|
|
2640
2751
|
}
|
|
2641
2752
|
}
|
|
2642
2753
|
);
|
|
2643
2754
|
}
|
|
2644
2755
|
|
|
2645
2756
|
// src/tools/packs/jsThread.ts
|
|
2646
|
-
var
|
|
2757
|
+
var import_zod15 = require("zod");
|
|
2647
2758
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2648
|
-
var
|
|
2649
|
-
var
|
|
2650
|
-
var
|
|
2759
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2760
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2761
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2651
2762
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2652
2763
|
function sparkline(values) {
|
|
2653
2764
|
const max = Math.max(1, ...values);
|
|
@@ -2718,9 +2829,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2718
2829
|
{
|
|
2719
2830
|
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.",
|
|
2720
2831
|
inputSchema: {
|
|
2721
|
-
deviceId:
|
|
2722
|
-
durationMs:
|
|
2723
|
-
clearFirst:
|
|
2832
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2833
|
+
durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2834
|
+
clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2724
2835
|
}
|
|
2725
2836
|
},
|
|
2726
2837
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2733,7 +2844,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2733
2844
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2734
2845
|
target
|
|
2735
2846
|
);
|
|
2736
|
-
if (!snap) return
|
|
2847
|
+
if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2737
2848
|
const md = formatTopMarkdown(snap, duration);
|
|
2738
2849
|
const slim = {
|
|
2739
2850
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2752,13 +2863,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2752
2863
|
worstFreezeMs: r.worstFreezeMs
|
|
2753
2864
|
}))
|
|
2754
2865
|
};
|
|
2755
|
-
return
|
|
2866
|
+
return text13(`${md}
|
|
2756
2867
|
|
|
2757
2868
|
\`\`\`json
|
|
2758
2869
|
${JSON.stringify(slim)}
|
|
2759
2870
|
\`\`\``);
|
|
2760
2871
|
} catch (e) {
|
|
2761
|
-
return
|
|
2872
|
+
return fail15(errMsg15(e));
|
|
2762
2873
|
}
|
|
2763
2874
|
}
|
|
2764
2875
|
);
|
|
@@ -2767,8 +2878,8 @@ ${JSON.stringify(slim)}
|
|
|
2767
2878
|
{
|
|
2768
2879
|
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).',
|
|
2769
2880
|
inputSchema: {
|
|
2770
|
-
deviceId:
|
|
2771
|
-
key:
|
|
2881
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2882
|
+
key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2772
2883
|
}
|
|
2773
2884
|
},
|
|
2774
2885
|
async ({ deviceId, key }) => {
|
|
@@ -2781,18 +2892,18 @@ ${JSON.stringify(slim)}
|
|
|
2781
2892
|
target
|
|
2782
2893
|
);
|
|
2783
2894
|
if (!detail) {
|
|
2784
|
-
return
|
|
2895
|
+
return fail15(
|
|
2785
2896
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2786
2897
|
);
|
|
2787
2898
|
}
|
|
2788
2899
|
const md = formatDetailMarkdown(detail);
|
|
2789
|
-
return
|
|
2900
|
+
return text13(`${md}
|
|
2790
2901
|
|
|
2791
2902
|
\`\`\`json
|
|
2792
2903
|
${JSON.stringify(detail)}
|
|
2793
2904
|
\`\`\``);
|
|
2794
2905
|
} catch (e) {
|
|
2795
|
-
return
|
|
2906
|
+
return fail15(errMsg15(e));
|
|
2796
2907
|
}
|
|
2797
2908
|
}
|
|
2798
2909
|
);
|
|
@@ -2801,27 +2912,27 @@ ${JSON.stringify(detail)}
|
|
|
2801
2912
|
{
|
|
2802
2913
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2803
2914
|
inputSchema: {
|
|
2804
|
-
deviceId:
|
|
2915
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2805
2916
|
}
|
|
2806
2917
|
},
|
|
2807
2918
|
async ({ deviceId }) => {
|
|
2808
2919
|
try {
|
|
2809
2920
|
const target = sync.requireProDevice(deviceId);
|
|
2810
2921
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2811
|
-
return
|
|
2922
|
+
return text13("Cleared js-top stats.");
|
|
2812
2923
|
} catch (e) {
|
|
2813
|
-
return
|
|
2924
|
+
return fail15(errMsg15(e));
|
|
2814
2925
|
}
|
|
2815
2926
|
}
|
|
2816
2927
|
);
|
|
2817
2928
|
}
|
|
2818
2929
|
|
|
2819
2930
|
// src/tools/packs/images.ts
|
|
2820
|
-
var
|
|
2931
|
+
var import_zod16 = require("zod");
|
|
2821
2932
|
var IMAGES_TOOL_ID = "images";
|
|
2822
|
-
var
|
|
2823
|
-
var
|
|
2824
|
-
var
|
|
2933
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2934
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2935
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2825
2936
|
function uriTail(uri) {
|
|
2826
2937
|
if (!uri) return "(no source)";
|
|
2827
2938
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2909,9 +3020,9 @@ function registerImagesTools(server, sync) {
|
|
|
2909
3020
|
{
|
|
2910
3021
|
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.",
|
|
2911
3022
|
inputSchema: {
|
|
2912
|
-
deviceId:
|
|
2913
|
-
limit:
|
|
2914
|
-
status:
|
|
3023
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3024
|
+
limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
3025
|
+
status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2915
3026
|
}
|
|
2916
3027
|
},
|
|
2917
3028
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2924,7 +3035,7 @@ function registerImagesTools(server, sync) {
|
|
|
2924
3035
|
{ limit: capped, status },
|
|
2925
3036
|
target
|
|
2926
3037
|
);
|
|
2927
|
-
if (!list) return
|
|
3038
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2928
3039
|
const md = formatListMarkdown(list, capped);
|
|
2929
3040
|
const slim = {
|
|
2930
3041
|
stats: list.stats,
|
|
@@ -2951,13 +3062,13 @@ function registerImagesTools(server, sync) {
|
|
|
2951
3062
|
layoutShifts: r.layoutShifts
|
|
2952
3063
|
}))
|
|
2953
3064
|
};
|
|
2954
|
-
return
|
|
3065
|
+
return text14(`${md}
|
|
2955
3066
|
|
|
2956
3067
|
\`\`\`json
|
|
2957
3068
|
${JSON.stringify(slim)}
|
|
2958
3069
|
\`\`\``);
|
|
2959
3070
|
} catch (e) {
|
|
2960
|
-
return
|
|
3071
|
+
return fail16(errMsg16(e));
|
|
2961
3072
|
}
|
|
2962
3073
|
}
|
|
2963
3074
|
);
|
|
@@ -2966,9 +3077,9 @@ ${JSON.stringify(slim)}
|
|
|
2966
3077
|
{
|
|
2967
3078
|
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.",
|
|
2968
3079
|
inputSchema: {
|
|
2969
|
-
deviceId:
|
|
2970
|
-
id:
|
|
2971
|
-
action:
|
|
3080
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3081
|
+
id: import_zod16.z.number().describe("Record id from get_images."),
|
|
3082
|
+
action: import_zod16.z.enum([
|
|
2972
3083
|
"hard_reload",
|
|
2973
3084
|
"retry",
|
|
2974
3085
|
"evict_disk",
|
|
@@ -2980,7 +3091,7 @@ ${JSON.stringify(slim)}
|
|
|
2980
3091
|
"swap_url",
|
|
2981
3092
|
"restore"
|
|
2982
3093
|
]).describe("What to do."),
|
|
2983
|
-
url:
|
|
3094
|
+
url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
2984
3095
|
}
|
|
2985
3096
|
},
|
|
2986
3097
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2988,8 +3099,8 @@ ${JSON.stringify(slim)}
|
|
|
2988
3099
|
const target = sync.requireProDevice(deviceId);
|
|
2989
3100
|
if (action === "prove_savings") {
|
|
2990
3101
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2991
|
-
if (!savings) return
|
|
2992
|
-
if (!savings.ok) return
|
|
3102
|
+
if (!savings) return fail16("Device returned nothing for proveSavings.");
|
|
3103
|
+
if (!savings.ok) return fail16(savings.message);
|
|
2993
3104
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2994
3105
|
if (savings.originalBytes != null)
|
|
2995
3106
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3001,11 +3112,11 @@ ${JSON.stringify(slim)}
|
|
|
3001
3112
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3002
3113
|
if (savings.optimizedUri)
|
|
3003
3114
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3004
|
-
return
|
|
3115
|
+
return text14(parts.join("\n"));
|
|
3005
3116
|
}
|
|
3006
3117
|
const entry = ACTION_MAP[action];
|
|
3007
|
-
if (!entry) return
|
|
3008
|
-
if (action === "swap_url" && !url) return
|
|
3118
|
+
if (!entry) return fail16(`Unknown action: ${action}`);
|
|
3119
|
+
if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
|
|
3009
3120
|
const result = await sync.callAction(
|
|
3010
3121
|
IMAGES_TOOL_ID,
|
|
3011
3122
|
entry.action,
|
|
@@ -3025,9 +3136,9 @@ ${JSON.stringify(slim)}
|
|
|
3025
3136
|
);
|
|
3026
3137
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3027
3138
|
}
|
|
3028
|
-
return
|
|
3139
|
+
return text14(lines.join("\n"));
|
|
3029
3140
|
} catch (e) {
|
|
3030
|
-
return
|
|
3141
|
+
return fail16(errMsg16(e));
|
|
3031
3142
|
}
|
|
3032
3143
|
}
|
|
3033
3144
|
);
|
|
@@ -3036,12 +3147,12 @@ ${JSON.stringify(slim)}
|
|
|
3036
3147
|
{
|
|
3037
3148
|
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.",
|
|
3038
3149
|
inputSchema: {
|
|
3039
|
-
deviceId:
|
|
3040
|
-
network:
|
|
3041
|
-
blankImages:
|
|
3042
|
-
massAction:
|
|
3043
|
-
clearRegistry:
|
|
3044
|
-
clearExpoCaches:
|
|
3150
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3151
|
+
network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3152
|
+
blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
|
|
3153
|
+
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."),
|
|
3154
|
+
clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
|
|
3155
|
+
clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3045
3156
|
}
|
|
3046
3157
|
},
|
|
3047
3158
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3068,22 +3179,22 @@ ${JSON.stringify(slim)}
|
|
|
3068
3179
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3069
3180
|
done.push(r?.message ?? "expo caches cleared");
|
|
3070
3181
|
}
|
|
3071
|
-
if (done.length === 0) return
|
|
3072
|
-
return
|
|
3182
|
+
if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3183
|
+
return text14(`Applied: ${done.join(" \xB7 ")}`);
|
|
3073
3184
|
} catch (e) {
|
|
3074
|
-
return
|
|
3185
|
+
return fail16(errMsg16(e));
|
|
3075
3186
|
}
|
|
3076
3187
|
}
|
|
3077
3188
|
);
|
|
3078
3189
|
}
|
|
3079
3190
|
|
|
3080
3191
|
// src/tools/packs/assets.ts
|
|
3081
|
-
var
|
|
3192
|
+
var import_zod17 = require("zod");
|
|
3082
3193
|
var ASSETS_TOOL_ID = "assets";
|
|
3083
|
-
var
|
|
3084
|
-
var
|
|
3085
|
-
var
|
|
3086
|
-
function
|
|
3194
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3195
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3196
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3197
|
+
function kb2(bytes) {
|
|
3087
3198
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3088
3199
|
if (bytes < 1024) return `${bytes}B`;
|
|
3089
3200
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
@@ -3091,7 +3202,7 @@ function kb(bytes) {
|
|
|
3091
3202
|
}
|
|
3092
3203
|
function sizeLabel(r) {
|
|
3093
3204
|
if (r.sizeBytes == null) return "\u2013";
|
|
3094
|
-
return r.sizeSource === "measured" ?
|
|
3205
|
+
return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
|
|
3095
3206
|
}
|
|
3096
3207
|
function scalesLabel(scales) {
|
|
3097
3208
|
if (!scales.length) return "\u2013";
|
|
@@ -3114,9 +3225,9 @@ function formatListMarkdown2(list, limit) {
|
|
|
3114
3225
|
const ss = list.scanStatus;
|
|
3115
3226
|
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3116
3227
|
lines.push("");
|
|
3117
|
-
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${
|
|
3228
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
|
|
3118
3229
|
lines.push(
|
|
3119
|
-
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${
|
|
3230
|
+
`${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 ")}`
|
|
3120
3231
|
);
|
|
3121
3232
|
if (ss.fontFamilies.length > 0) {
|
|
3122
3233
|
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
@@ -3132,7 +3243,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3132
3243
|
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3133
3244
|
if (ins.duplicates.length) {
|
|
3134
3245
|
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3135
|
-
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${
|
|
3246
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
|
|
3136
3247
|
}
|
|
3137
3248
|
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3138
3249
|
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
@@ -3146,7 +3257,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3146
3257
|
}
|
|
3147
3258
|
const diff = list.diff;
|
|
3148
3259
|
if (diff) {
|
|
3149
|
-
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${
|
|
3260
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
|
|
3150
3261
|
lines.push("");
|
|
3151
3262
|
lines.push(
|
|
3152
3263
|
`**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" : ""}` : "")
|
|
@@ -3173,10 +3284,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3173
3284
|
{
|
|
3174
3285
|
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.",
|
|
3175
3286
|
inputSchema: {
|
|
3176
|
-
deviceId:
|
|
3177
|
-
limit:
|
|
3178
|
-
kind:
|
|
3179
|
-
unusedOnly:
|
|
3287
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3288
|
+
limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3289
|
+
kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3290
|
+
unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3180
3291
|
}
|
|
3181
3292
|
},
|
|
3182
3293
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3189,7 +3300,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3189
3300
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3190
3301
|
target
|
|
3191
3302
|
);
|
|
3192
|
-
if (!list) return
|
|
3303
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3193
3304
|
if (unusedOnly) {
|
|
3194
3305
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3195
3306
|
}
|
|
@@ -3213,13 +3324,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3213
3324
|
sizeSource: r.sizeSource
|
|
3214
3325
|
}))
|
|
3215
3326
|
};
|
|
3216
|
-
return
|
|
3327
|
+
return text15(`${md}
|
|
3217
3328
|
|
|
3218
3329
|
\`\`\`json
|
|
3219
3330
|
${JSON.stringify(slim)}
|
|
3220
3331
|
\`\`\``);
|
|
3221
3332
|
} catch (e) {
|
|
3222
|
-
return
|
|
3333
|
+
return fail17(errMsg17(e));
|
|
3223
3334
|
}
|
|
3224
3335
|
}
|
|
3225
3336
|
);
|
|
@@ -3228,9 +3339,9 @@ ${JSON.stringify(slim)}
|
|
|
3228
3339
|
{
|
|
3229
3340
|
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).",
|
|
3230
3341
|
inputSchema: {
|
|
3231
|
-
deviceId:
|
|
3232
|
-
action:
|
|
3233
|
-
id:
|
|
3342
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3343
|
+
action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3344
|
+
id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3234
3345
|
}
|
|
3235
3346
|
},
|
|
3236
3347
|
async ({ deviceId, action, id }) => {
|
|
@@ -3240,19 +3351,19 @@ ${JSON.stringify(slim)}
|
|
|
3240
3351
|
case "rescan": {
|
|
3241
3352
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3242
3353
|
const ss = result?.scanStatus;
|
|
3243
|
-
if (!ss) return
|
|
3244
|
-
return
|
|
3354
|
+
if (!ss) return fail17("Device returned nothing for rescan.");
|
|
3355
|
+
return text15(
|
|
3245
3356
|
`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" : "")
|
|
3246
3357
|
);
|
|
3247
3358
|
}
|
|
3248
3359
|
case "measure_sizes": {
|
|
3249
3360
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3250
|
-
return
|
|
3361
|
+
return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3251
3362
|
}
|
|
3252
3363
|
case "detail": {
|
|
3253
|
-
if (id == null) return
|
|
3364
|
+
if (id == null) return fail17("detail requires the `id` param (from get_assets).");
|
|
3254
3365
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3255
|
-
if (!detail) return
|
|
3366
|
+
if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3256
3367
|
const lines = [
|
|
3257
3368
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3258
3369
|
"",
|
|
@@ -3261,50 +3372,50 @@ ${JSON.stringify(slim)}
|
|
|
3261
3372
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3262
3373
|
if (detail.sizesByScale) {
|
|
3263
3374
|
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3264
|
-
lines.push(`- Size @${scale}x: ${
|
|
3375
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
|
|
3265
3376
|
}
|
|
3266
3377
|
}
|
|
3267
3378
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3268
|
-
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${
|
|
3379
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
|
|
3269
3380
|
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3270
3381
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3271
3382
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3272
3383
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3273
|
-
return
|
|
3384
|
+
return text15(lines.join("\n"));
|
|
3274
3385
|
}
|
|
3275
3386
|
case "save_baseline": {
|
|
3276
3387
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3277
|
-
return
|
|
3388
|
+
return text15(
|
|
3278
3389
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3279
3390
|
);
|
|
3280
3391
|
}
|
|
3281
3392
|
case "clear_baseline": {
|
|
3282
3393
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3283
|
-
return
|
|
3394
|
+
return text15("Baseline cleared.");
|
|
3284
3395
|
}
|
|
3285
3396
|
case "clear_records": {
|
|
3286
3397
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3287
|
-
return
|
|
3398
|
+
return text15(result?.message ?? "Inventory cleared.");
|
|
3288
3399
|
}
|
|
3289
3400
|
}
|
|
3290
3401
|
} catch (e) {
|
|
3291
|
-
return
|
|
3402
|
+
return fail17(errMsg17(e));
|
|
3292
3403
|
}
|
|
3293
3404
|
}
|
|
3294
3405
|
);
|
|
3295
3406
|
}
|
|
3296
3407
|
|
|
3297
3408
|
// src/tools/packs/reload.ts
|
|
3298
|
-
var
|
|
3409
|
+
var import_zod18 = require("zod");
|
|
3299
3410
|
var APP_TOOL_ID = "app";
|
|
3300
3411
|
var RELOAD_ACTION = "reloadApp";
|
|
3301
3412
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3302
3413
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3303
3414
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3304
3415
|
var PROBE_INTERVAL_MS = 400;
|
|
3305
|
-
var
|
|
3306
|
-
var
|
|
3307
|
-
var
|
|
3416
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3417
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3418
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3308
3419
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3309
3420
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3310
3421
|
try {
|
|
@@ -3339,12 +3450,12 @@ function registerReloadTools(server, sync) {
|
|
|
3339
3450
|
{
|
|
3340
3451
|
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.",
|
|
3341
3452
|
inputSchema: {
|
|
3342
|
-
strategy:
|
|
3453
|
+
strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3343
3454
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3344
3455
|
),
|
|
3345
|
-
wait:
|
|
3346
|
-
timeoutMs:
|
|
3347
|
-
deviceId:
|
|
3456
|
+
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."),
|
|
3457
|
+
timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3458
|
+
deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3348
3459
|
}
|
|
3349
3460
|
},
|
|
3350
3461
|
async (args) => {
|
|
@@ -3360,19 +3471,19 @@ function registerReloadTools(server, sync) {
|
|
|
3360
3471
|
);
|
|
3361
3472
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3362
3473
|
if (args.wait === false) {
|
|
3363
|
-
return
|
|
3474
|
+
return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3364
3475
|
}
|
|
3365
3476
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3366
3477
|
if (elapsed == null) {
|
|
3367
|
-
return
|
|
3478
|
+
return fail18(
|
|
3368
3479
|
`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.`
|
|
3369
3480
|
);
|
|
3370
3481
|
}
|
|
3371
|
-
return
|
|
3482
|
+
return text16(
|
|
3372
3483
|
`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.`
|
|
3373
3484
|
);
|
|
3374
3485
|
} catch (e) {
|
|
3375
|
-
return
|
|
3486
|
+
return fail18(errMsg18(e));
|
|
3376
3487
|
}
|
|
3377
3488
|
}
|
|
3378
3489
|
);
|
|
@@ -3427,6 +3538,14 @@ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 De
|
|
|
3427
3538
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
3428
3539
|
re-read anything you cached.
|
|
3429
3540
|
|
|
3541
|
+
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
3542
|
+
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
3543
|
+
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
3544
|
+
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
3545
|
+
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
3546
|
+
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
3547
|
+
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
3548
|
+
|
|
3430
3549
|
For everything else:
|
|
3431
3550
|
- \`list_tool_actions\` to see what a tool can do,
|
|
3432
3551
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -3454,6 +3573,7 @@ function createMcpServer(sync) {
|
|
|
3454
3573
|
registerStorageTools(server, sync);
|
|
3455
3574
|
registerRoutesTools(server, sync);
|
|
3456
3575
|
registerConsoleTools(server, sync);
|
|
3576
|
+
registerTimeMachineTools(server, sync);
|
|
3457
3577
|
registerScreenshotTools(server, sync);
|
|
3458
3578
|
registerInteractTools(server, sync);
|
|
3459
3579
|
registerJsThreadTools(server, sync);
|
|
@@ -3463,7 +3583,7 @@ function createMcpServer(sync) {
|
|
|
3463
3583
|
return server;
|
|
3464
3584
|
}
|
|
3465
3585
|
var deviceIdField = {
|
|
3466
|
-
deviceId:
|
|
3586
|
+
deviceId: import_zod19.z.string().optional().describe(
|
|
3467
3587
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3468
3588
|
)
|
|
3469
3589
|
};
|