@buoy-gg/mcp 7.0.2 → 7.0.3

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/cli.js CHANGED
@@ -325,7 +325,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
325
325
 
326
326
  // src/server.ts
327
327
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
328
- var import_zod13 = require("zod");
328
+ var import_zod15 = require("zod");
329
329
 
330
330
  // src/tools/generic.ts
331
331
  var import_zod = require("zod");
@@ -336,7 +336,7 @@ var import_node_os = __toESM(require("os"));
336
336
  var import_node_path = __toESM(require("path"));
337
337
 
338
338
  // src/version.ts
339
- var VERSION = "7.0.2" ? "7.0.2" : "0.0.0-dev";
339
+ var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
340
340
  var PACKAGE_NAME = "@buoy-gg/mcp";
341
341
 
342
342
  // src/update/checkForUpdate.ts
@@ -2144,6 +2144,439 @@ ${elements.map(formatElement).join("\n")}`);
2144
2144
  );
2145
2145
  }
2146
2146
 
2147
+ // src/tools/packs/jsThread.ts
2148
+ var import_zod13 = require("zod");
2149
+ var JS_TOP_TOOL_ID = "js-top";
2150
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2151
+ var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2152
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2153
+ var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2154
+ function sparkline(values) {
2155
+ const max = Math.max(1, ...values);
2156
+ return values.map((v) => SPARK_CHARS[Math.min(7, Math.floor(v / max * 7.999))]).join("");
2157
+ }
2158
+ function formatTopMarkdown(snap, durationMs) {
2159
+ const lines = [];
2160
+ lines.push(`## JS thread top (${(durationMs / 1e3).toFixed(1)}s sample)`);
2161
+ lines.push("");
2162
+ lines.push(`Thread busy: **${snap.busyPct.toFixed(1)}%** of the trailing 5s window.`);
2163
+ if (snap.freezeSummary.count > 0) {
2164
+ lines.push(
2165
+ `Freezes (\u226550ms single-task blocks, last 60s): **${snap.freezeSummary.count}**, worst **${snap.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`
2166
+ );
2167
+ }
2168
+ if (!snap.tiers.bridge) {
2169
+ lines.push(
2170
+ "_New Architecture device: touch/React commit work appears as \u201Cunattributed\u201D; timers, rAF and Promise chains are fully attributed._"
2171
+ );
2172
+ }
2173
+ lines.push("");
2174
+ lines.push("| # | task origin | api | 5s ms | % busy | calls | avg ms | max ms | freezes |");
2175
+ lines.push("|---|---|---|---|---|---|---|---|---|");
2176
+ snap.rows.slice(0, 15).forEach((r, i) => {
2177
+ const freezeCell = r.freezes > 0 ? `\u26A0 ${r.freezes} (worst ${r.worstFreezeMs}ms)` : "\u2013";
2178
+ lines.push(
2179
+ `| ${i + 1} | ${r.label} | ${r.api} | ${r.windowMs.toFixed(1)} | ${r.pctOfBusy.toFixed(1)} | ${r.calls} | ${r.avgMs.toFixed(2)} | ${r.maxMs.toFixed(1)} | ${freezeCell} |`
2180
+ );
2181
+ });
2182
+ lines.push("");
2183
+ lines.push(
2184
+ "_Drill into any row with `get_js_thread_origin_detail` using its `key` from the JSON below._"
2185
+ );
2186
+ return lines.join("\n");
2187
+ }
2188
+ function formatDetailMarkdown(d) {
2189
+ const lines = [];
2190
+ lines.push(`## Origin detail \u2014 ${d.label}`);
2191
+ lines.push("");
2192
+ lines.push(`API: \`${d.api}\` \xB7 key: \`${d.key}\``);
2193
+ lines.push("");
2194
+ lines.push(`Activity last 5s (250ms buckets): \`${sparkline(d.buckets)}\` \u2014 ${d.windowMs.toFixed(1)}ms in window, ${d.pctOfBusy.toFixed(1)}% of busy time.`);
2195
+ lines.push("");
2196
+ lines.push("| runs | scheduled | avg/run | max/run | total | last run | freezes (60s) | worst freeze |");
2197
+ lines.push("|---|---|---|---|---|---|---|---|");
2198
+ lines.push(
2199
+ `| ${d.calls} | ${d.scheduleCount} | ${d.avgMs.toFixed(2)}ms | ${d.maxMs.toFixed(1)}ms | ${d.totalMs.toFixed(1)}ms | ${d.lastSeenAgoMs > 0 ? `${Math.round(d.lastSeenAgoMs / 1e3)}s ago` : "\u2014"} | ${d.freezes} | ${d.worstFreezeMs > 0 ? `${d.worstFreezeMs}ms` : "\u2014"} |`
2200
+ );
2201
+ if (d.caller || d.file) {
2202
+ lines.push("");
2203
+ lines.push(`Scheduled from: **${d.caller ? `${d.caller}()` : "unknown caller"}**${d.file ? ` \u2014 ${d.file}${d.line != null ? `:${d.line}` : ""}` : ""}`);
2204
+ if (d.frames?.length) {
2205
+ lines.push(
2206
+ `Raw frames (Hermes bundle offsets): ${d.frames.map((f) => `${f.file}:${f.line}:${f.col}`).join(" \xB7 ")}`
2207
+ );
2208
+ }
2209
+ } else if (d.api !== "unattributed" && d.api !== "other") {
2210
+ lines.push("");
2211
+ lines.push(
2212
+ d.promoted ? "_Scheduling stack not captured yet \u2014 the next schedule from this site records it._" : "_Not hot enough for stack capture yet (threshold: 25 schedules or 50ms total)._"
2213
+ );
2214
+ }
2215
+ return lines.join("\n");
2216
+ }
2217
+ function registerJsThreadTools(server, sync) {
2218
+ server.registerTool(
2219
+ "get_js_thread_top",
2220
+ {
2221
+ 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.",
2222
+ inputSchema: {
2223
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2224
+ durationMs: import_zod13.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2225
+ clearFirst: import_zod13.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2226
+ }
2227
+ },
2228
+ async ({ deviceId, durationMs, clearFirst }) => {
2229
+ try {
2230
+ const target = sync.requireProDevice(deviceId);
2231
+ const duration = Math.min(Math.max(durationMs ?? 3e3, 250), 3e4);
2232
+ const snap = await sync.callAction(
2233
+ JS_TOP_TOOL_ID,
2234
+ "sample",
2235
+ { durationMs: duration, clearFirst: clearFirst === true },
2236
+ target
2237
+ );
2238
+ if (!snap) return fail13("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2239
+ const md = formatTopMarkdown(snap, duration);
2240
+ const slim = {
2241
+ busyPct: Math.round(snap.busyPct * 10) / 10,
2242
+ freezeSummary: snap.freezeSummary,
2243
+ tiers: snap.tiers,
2244
+ rows: snap.rows.slice(0, 15).map((r) => ({
2245
+ key: r.key,
2246
+ api: r.api,
2247
+ windowMs: Math.round(r.windowMs * 10) / 10,
2248
+ pctOfBusy: Math.round(r.pctOfBusy * 10) / 10,
2249
+ calls: r.calls,
2250
+ avgMs: Math.round(r.avgMs * 100) / 100,
2251
+ maxMs: Math.round(r.maxMs * 10) / 10,
2252
+ totalMs: Math.round(r.totalMs),
2253
+ freezes: r.freezes,
2254
+ worstFreezeMs: r.worstFreezeMs
2255
+ }))
2256
+ };
2257
+ return text11(`${md}
2258
+
2259
+ \`\`\`json
2260
+ ${JSON.stringify(slim)}
2261
+ \`\`\``);
2262
+ } catch (e) {
2263
+ return fail13(errMsg13(e));
2264
+ }
2265
+ }
2266
+ );
2267
+ server.registerTool(
2268
+ "get_js_thread_origin_detail",
2269
+ {
2270
+ 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).',
2271
+ inputSchema: {
2272
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2273
+ key: import_zod13.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2274
+ }
2275
+ },
2276
+ async ({ deviceId, key }) => {
2277
+ try {
2278
+ const target = sync.requireProDevice(deviceId);
2279
+ const detail = await sync.callAction(
2280
+ JS_TOP_TOOL_ID,
2281
+ "getOriginDetail",
2282
+ { key },
2283
+ target
2284
+ );
2285
+ if (!detail) {
2286
+ return fail13(
2287
+ `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2288
+ );
2289
+ }
2290
+ const md = formatDetailMarkdown(detail);
2291
+ return text11(`${md}
2292
+
2293
+ \`\`\`json
2294
+ ${JSON.stringify(detail)}
2295
+ \`\`\``);
2296
+ } catch (e) {
2297
+ return fail13(errMsg13(e));
2298
+ }
2299
+ }
2300
+ );
2301
+ server.registerTool(
2302
+ "clear_js_thread_stats",
2303
+ {
2304
+ description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2305
+ inputSchema: {
2306
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2307
+ }
2308
+ },
2309
+ async ({ deviceId }) => {
2310
+ try {
2311
+ const target = sync.requireProDevice(deviceId);
2312
+ await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2313
+ return text11("Cleared js-top stats.");
2314
+ } catch (e) {
2315
+ return fail13(errMsg13(e));
2316
+ }
2317
+ }
2318
+ );
2319
+ }
2320
+
2321
+ // src/tools/packs/images.ts
2322
+ var import_zod14 = require("zod");
2323
+ var IMAGES_TOOL_ID = "images";
2324
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2325
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2326
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2327
+ function uriTail(uri) {
2328
+ if (!uri) return "(no source)";
2329
+ const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
2330
+ const effective = unstable ? decodeURIComponent(unstable[1]) : uri;
2331
+ const noQuery = effective.split("?")[0] ?? effective;
2332
+ const segs = noQuery.split("/").filter(Boolean);
2333
+ return segs[segs.length - 1] ?? effective;
2334
+ }
2335
+ function verdictLabel(r) {
2336
+ if (r.oversizeFactor == null) return "\u2013";
2337
+ if (r.oversizeFactor < 0.9) return `${r.oversizeFactor.toFixed(1)}\xD7 upscaled`;
2338
+ if (r.oversizeFactor <= 1.1) return "right-sized";
2339
+ return `${r.oversizeFactor.toFixed(1)}\xD7 oversized (${r.wastedKB}KB wasted)`;
2340
+ }
2341
+ function cacheLabel(r) {
2342
+ if (r.cache) return r.cache;
2343
+ if (r.progressSeen) return "network";
2344
+ return "?";
2345
+ }
2346
+ function mb(bytes) {
2347
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2348
+ }
2349
+ function formatListMarkdown(list, limit) {
2350
+ const lines = [];
2351
+ const s = list.stats;
2352
+ lines.push(`## Images registry (${list.total} records, showing ${Math.min(limit, list.records.length)})`);
2353
+ lines.push("");
2354
+ lines.push(
2355
+ `${s.loaded} loaded \xB7 ${s.errors} failed \xB7 ${s.loading} in flight \xB7 \u2248${mb(s.estDecodedBytes)} decoded` + (s.estWastedBytes > 0 ? ` \xB7 **${mb(s.estWastedBytes)} wasted on oversized sources**` : "")
2356
+ );
2357
+ const ins = list.insights;
2358
+ if (ins) {
2359
+ const chips = [];
2360
+ if (ins.duplicates.length > 0)
2361
+ chips.push(`${ins.duplicates.length} duplicate URL(s): ${ins.duplicates.slice(0, 3).map((d) => `${uriTail(d.uri)}\xD7${d.count}`).join(", ")}`);
2362
+ if (ins.retryStorms.length > 0)
2363
+ chips.push(`${ins.retryStorms.length} retry storm(s): ${ins.retryStorms.slice(0, 3).map((r) => `#${r.id}(${r.loadCount} loads)`).join(", ")}`);
2364
+ if (ins.queueSaturated) chips.push("iOS image queue saturated (>4 concurrent loads)");
2365
+ if (ins.missingAlt > 0) chips.push(`${ins.missingAlt} image(s) missing alt/accessibilityLabel`);
2366
+ if (ins.layoutShifters.length > 0)
2367
+ chips.push(`${ins.layoutShifters.length} layout-shifting image(s): ${ins.layoutShifters.slice(0, 3).map((l) => `#${l.id}`).join(", ")} \u2014 reserve explicit dimensions`);
2368
+ if (chips.length > 0) {
2369
+ lines.push("");
2370
+ lines.push(`**Insights:** ${chips.join(" \xB7 ")}`);
2371
+ }
2372
+ }
2373
+ const cs = list.captureStatus;
2374
+ if (cs.rnDecoratorTooLate) {
2375
+ lines.push("");
2376
+ lines.push('\u26A0 RN <Image> capture missed the module-load race \u2014 add `import "@buoy-gg/images/register"` as the FIRST import of the app entry file.');
2377
+ }
2378
+ if (list.globalModes.network !== "normal" || list.globalModes.blank) {
2379
+ lines.push("");
2380
+ lines.push(`\u26A0 Simulation active: network=${list.globalModes.network}${list.globalModes.blank ? ", blank images ON" : ""} (set_image_simulation to reset).`);
2381
+ }
2382
+ lines.push("");
2383
+ lines.push("| id | image | lib | status | cache | ms | decoded px | shown dp | verdict | sim |");
2384
+ lines.push("|---|---|---|---|---|---|---|---|---|---|");
2385
+ for (const r of list.records) {
2386
+ const dims = r.intrinsic ? `${r.intrinsic.width}\xD7${r.intrinsic.height}` : "\u2013";
2387
+ const layout = r.layout ? `${r.layout.width}\xD7${r.layout.height}` : "\u2013";
2388
+ const status = r.status === "error" ? `**error**${r.errorCode ? ` (${r.errorCode})` : ""}` : r.status;
2389
+ lines.push(
2390
+ `| ${r.id} | ${uriTail(r.uri)} | ${r.lib} | ${status}${r.mounted ? "" : " (unmounted)"} | ${cacheLabel(r)} | ${r.ms ?? "\u2013"} | ${dims} | ${layout} | ${verdictLabel(r)} | ${r.overrideLabel ?? "\u2013"} |`
2391
+ );
2392
+ }
2393
+ lines.push("");
2394
+ lines.push("_Act on a row with `image_action` using its `id` (hard_reload / retry / force_error / force_loading / force_blank / swap_url / flash / restore / evict_disk)._");
2395
+ return lines.join("\n");
2396
+ }
2397
+ var ACTION_MAP = {
2398
+ hard_reload: { action: "hardReload" },
2399
+ retry: { action: "retry" },
2400
+ evict_disk: { action: "evictDisk" },
2401
+ flash: { action: "flash" },
2402
+ restore: { action: "clearOverride" },
2403
+ force_error: { action: "setOverride", params: () => ({ kind: "error" }) },
2404
+ force_loading: { action: "setOverride", params: () => ({ kind: "hang" }) },
2405
+ force_blank: { action: "setOverride", params: () => ({ kind: "blank" }) },
2406
+ swap_url: { action: "setOverride", params: (url) => ({ kind: "url", uri: url }) }
2407
+ };
2408
+ function registerImagesTools(server, sync) {
2409
+ server.registerTool(
2410
+ "get_images",
2411
+ {
2412
+ 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.",
2413
+ inputSchema: {
2414
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2415
+ limit: import_zod14.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2416
+ status: import_zod14.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2417
+ }
2418
+ },
2419
+ async ({ deviceId, limit, status }) => {
2420
+ try {
2421
+ const target = sync.requireProDevice(deviceId);
2422
+ const capped = Math.min(Math.max(limit ?? 30, 1), 200);
2423
+ const list = await sync.callAction(
2424
+ IMAGES_TOOL_ID,
2425
+ "list",
2426
+ { limit: capped, status },
2427
+ target
2428
+ );
2429
+ if (!list) return fail14("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2430
+ const md = formatListMarkdown(list, capped);
2431
+ const slim = {
2432
+ stats: list.stats,
2433
+ insights: list.insights,
2434
+ captureStatus: list.captureStatus,
2435
+ globalModes: list.globalModes,
2436
+ records: list.records.map((r) => ({
2437
+ id: r.id,
2438
+ uri: r.uri,
2439
+ lib: r.lib,
2440
+ status: r.status,
2441
+ mounted: r.mounted,
2442
+ cache: r.cache,
2443
+ ms: r.ms,
2444
+ intrinsic: r.intrinsic,
2445
+ layout: r.layout,
2446
+ oversizeFactor: r.oversizeFactor,
2447
+ wastedKB: r.wastedKB,
2448
+ error: r.error,
2449
+ errorCode: r.errorCode,
2450
+ overrideLabel: r.overrideLabel,
2451
+ loadCount: r.loadCount
2452
+ }))
2453
+ };
2454
+ return text12(`${md}
2455
+
2456
+ \`\`\`json
2457
+ ${JSON.stringify(slim)}
2458
+ \`\`\``);
2459
+ } catch (e) {
2460
+ return fail14(errMsg14(e));
2461
+ }
2462
+ }
2463
+ );
2464
+ server.registerTool(
2465
+ "image_action",
2466
+ {
2467
+ 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.",
2468
+ inputSchema: {
2469
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2470
+ id: import_zod14.z.number().describe("Record id from get_images."),
2471
+ action: import_zod14.z.enum([
2472
+ "hard_reload",
2473
+ "retry",
2474
+ "evict_disk",
2475
+ "flash",
2476
+ "prove_savings",
2477
+ "force_error",
2478
+ "force_loading",
2479
+ "force_blank",
2480
+ "swap_url",
2481
+ "restore"
2482
+ ]).describe("What to do."),
2483
+ url: import_zod14.z.string().optional().describe("Replacement URL (swap_url only).")
2484
+ }
2485
+ },
2486
+ async ({ deviceId, id, action, url }) => {
2487
+ try {
2488
+ const target = sync.requireProDevice(deviceId);
2489
+ if (action === "prove_savings") {
2490
+ const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2491
+ if (!savings) return fail14("Device returned nothing for proveSavings.");
2492
+ if (!savings.ok) return fail14(savings.message);
2493
+ const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2494
+ if (savings.originalBytes != null)
2495
+ parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
2496
+ if (savings.optimizedBytes != null)
2497
+ parts.push(
2498
+ `Optimized: ${Math.round(savings.optimizedBytes / 1024)}KB \u2014 ${savings.optimizedDims?.width}\xD7${savings.optimizedDims?.height} WebP`
2499
+ );
2500
+ if (savings.savedBytes != null)
2501
+ parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
2502
+ if (savings.optimizedUri)
2503
+ parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
2504
+ return text12(parts.join("\n"));
2505
+ }
2506
+ const entry = ACTION_MAP[action];
2507
+ if (!entry) return fail14(`Unknown action: ${action}`);
2508
+ if (action === "swap_url" && !url) return fail14("swap_url requires the `url` param.");
2509
+ const result = await sync.callAction(
2510
+ IMAGES_TOOL_ID,
2511
+ entry.action,
2512
+ { id, ...entry.params?.(url) ?? {} },
2513
+ target
2514
+ );
2515
+ const settleMs = action === "flash" ? 0 : 1500;
2516
+ if (settleMs > 0) await new Promise((r) => setTimeout(r, settleMs));
2517
+ const detail = await sync.callAction(IMAGES_TOOL_ID, "getDetail", { id }, target).catch(() => null);
2518
+ const lines = [
2519
+ `**${action}** on #${id}: ${result?.message ?? (result?.ok === false ? "failed" : "done")}`
2520
+ ];
2521
+ if (detail) {
2522
+ lines.push("");
2523
+ lines.push(
2524
+ `Now: status=${detail.status}${detail.errorCode ? ` (${detail.errorCode})` : ""} \xB7 cache=${cacheLabel(detail)} \xB7 ms=${detail.ms ?? "\u2013"} \xB7 loads=${detail.loadCount}${detail.overrideLabel ? ` \xB7 sim="${detail.overrideLabel}"` : ""}${detail.mounted ? "" : " \xB7 UNMOUNTED"}`
2525
+ );
2526
+ if (detail.error) lines.push(`Error: ${detail.error}`);
2527
+ }
2528
+ return text12(lines.join("\n"));
2529
+ } catch (e) {
2530
+ return fail14(errMsg14(e));
2531
+ }
2532
+ }
2533
+ );
2534
+ server.registerTool(
2535
+ "set_image_simulation",
2536
+ {
2537
+ 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.",
2538
+ inputSchema: {
2539
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2540
+ network: import_zod14.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2541
+ blankImages: import_zod14.z.boolean().optional().describe("Render all images without a source."),
2542
+ massAction: import_zod14.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."),
2543
+ clearRegistry: import_zod14.z.boolean().optional().describe("Clear the captured image log."),
2544
+ clearExpoCaches: import_zod14.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
2545
+ }
2546
+ },
2547
+ async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
2548
+ try {
2549
+ const target = sync.requireProDevice(deviceId);
2550
+ const done = [];
2551
+ if (massAction) {
2552
+ const r = await sync.callAction(IMAGES_TOOL_ID, "massAction", { kind: massAction }, target);
2553
+ done.push(r?.message ?? `mass ${massAction} applied`);
2554
+ }
2555
+ if (network) {
2556
+ await sync.callAction(IMAGES_TOOL_ID, "setNetworkMode", { mode: network }, target);
2557
+ done.push(`network=${network}`);
2558
+ }
2559
+ if (blankImages != null) {
2560
+ await sync.callAction(IMAGES_TOOL_ID, "setBlankImages", { enabled: blankImages }, target);
2561
+ done.push(`blankImages=${blankImages}`);
2562
+ }
2563
+ if (clearRegistry) {
2564
+ await sync.callAction(IMAGES_TOOL_ID, "clearRecords", {}, target);
2565
+ done.push("registry cleared");
2566
+ }
2567
+ if (clearExpoCaches) {
2568
+ const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
2569
+ done.push(r?.message ?? "expo caches cleared");
2570
+ }
2571
+ if (done.length === 0) return fail14("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2572
+ return text12(`Applied: ${done.join(" \xB7 ")}`);
2573
+ } catch (e) {
2574
+ return fail14(errMsg14(e));
2575
+ }
2576
+ }
2577
+ );
2578
+ }
2579
+
2147
2580
  // src/server.ts
2148
2581
  var SERVER_INSTRUCTIONS = `Buoy MCP \u2014 drive Buoy dev tools running in a React Native app from your editor.
2149
2582
 
@@ -2166,6 +2599,15 @@ elements (accessibility-style tree with tap points + testIDs, no screenshot), th
2166
2599
  ({ testID | query | nativeTag }) to tap one \u2014 it invokes the element's onPress directly in JS, so
2167
2600
  it's fast and works on physical devices too. Re-run \`describe_screen\` after the screen changes.
2168
2601
 
2602
+ JS FPS low / app hitching and you need to know WHAT is eating the JS thread? Use
2603
+ \`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
2604
+ freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
2605
+ Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
2606
+
2607
+ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
2608
+ expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
2609
+ simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
2610
+
2169
2611
  For everything else:
2170
2612
  - \`list_tool_actions\` to see what a tool can do,
2171
2613
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -2194,10 +2636,12 @@ function createMcpServer(sync) {
2194
2636
  registerConsoleTools(server, sync);
2195
2637
  registerScreenshotTools(server, sync);
2196
2638
  registerInteractTools(server, sync);
2639
+ registerJsThreadTools(server, sync);
2640
+ registerImagesTools(server, sync);
2197
2641
  return server;
2198
2642
  }
2199
2643
  var deviceIdField = {
2200
- deviceId: import_zod13.z.string().optional().describe(
2644
+ deviceId: import_zod15.z.string().optional().describe(
2201
2645
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
2202
2646
  )
2203
2647
  };
@@ -2284,6 +2728,9 @@ function rel(file, cwd) {
2284
2728
  }
2285
2729
 
2286
2730
  // src/cli.ts
2731
+ console.log = (...args) => console.error(...args);
2732
+ console.info = (...args) => console.error(...args);
2733
+ console.debug = (...args) => console.error(...args);
2287
2734
  async function main() {
2288
2735
  const portEnv = process.env.BUOY_BROKER_PORT;
2289
2736
  const sync = new SyncClient({
package/dist/cli.mjs CHANGED
@@ -3,10 +3,13 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-XIN7JPXX.mjs";
6
+ } from "./chunk-MGZN6MJE.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ console.log = (...args) => console.error(...args);
11
+ console.info = (...args) => console.error(...args);
12
+ console.debug = (...args) => console.error(...args);
10
13
  async function main() {
11
14
  const portEnv = process.env.BUOY_BROKER_PORT;
12
15
  const sync = new SyncClient({