@buoy-gg/mcp 7.0.14 → 7.0.16

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.
@@ -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.14" ? "7.0.14" : "0.0.0-dev";
304
+ var VERSION = "7.0.16" ? "7.0.16" : "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 z17 } from "zod";
2028
+ import { z as z18 } from "zod";
2029
2029
 
2030
2030
  // src/tools/packs/network.ts
2031
2031
  import { z as z12 } from "zod";
@@ -2879,7 +2879,9 @@ function registerImagesTools(server, sync) {
2879
2879
  error: r.error,
2880
2880
  errorCode: r.errorCode,
2881
2881
  overrideLabel: r.overrideLabel,
2882
- loadCount: r.loadCount
2882
+ loadCount: r.loadCount,
2883
+ hasAltText: r.hasAltText,
2884
+ layoutShifts: r.layoutShifts
2883
2885
  }))
2884
2886
  };
2885
2887
  return text13(`${md}
@@ -3008,17 +3010,234 @@ ${JSON.stringify(slim)}
3008
3010
  );
3009
3011
  }
3010
3012
 
3011
- // src/tools/packs/reload.ts
3013
+ // src/tools/packs/assets.ts
3012
3014
  import { z as z16 } from "zod";
3015
+ var ASSETS_TOOL_ID = "assets";
3016
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3017
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3018
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3019
+ function kb(bytes) {
3020
+ if (bytes == null || bytes <= 0) return "\u2013";
3021
+ if (bytes < 1024) return `${bytes}B`;
3022
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3023
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
3024
+ }
3025
+ function sizeLabel(r) {
3026
+ if (r.sizeBytes == null) return "\u2013";
3027
+ return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
3028
+ }
3029
+ function scalesLabel(scales) {
3030
+ if (!scales.length) return "\u2013";
3031
+ return scales.map((s) => `@${s}x`).join(" ");
3032
+ }
3033
+ function flagsLabel(r, ins) {
3034
+ const flags = [];
3035
+ if (!r.loaded) flags.push("UNUSED");
3036
+ if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
3037
+ if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
3038
+ if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
3039
+ if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
3040
+ else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
3041
+ if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
3042
+ return flags.length ? flags.join(" ") : "\u2013";
3043
+ }
3044
+ function formatListMarkdown2(list, limit) {
3045
+ const lines = [];
3046
+ const s = list.stats;
3047
+ const ss = list.scanStatus;
3048
+ lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
3049
+ lines.push("");
3050
+ const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
3051
+ lines.push(
3052
+ `${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
3053
+ );
3054
+ if (ss.fontFamilies.length > 0) {
3055
+ lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
3056
+ }
3057
+ if (!ss.graphSupported || ss.graphCount == null) {
3058
+ lines.push(
3059
+ "\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})` : "")
3060
+ );
3061
+ }
3062
+ if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
3063
+ const ins = list.insights;
3064
+ const chips = [];
3065
+ if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
3066
+ if (ins.duplicates.length) {
3067
+ const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
3068
+ chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
3069
+ }
3070
+ if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
3071
+ if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
3072
+ if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
3073
+ if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
3074
+ if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
3075
+ if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
3076
+ if (chips.length) {
3077
+ lines.push("");
3078
+ lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
3079
+ }
3080
+ const diff = list.diff;
3081
+ if (diff) {
3082
+ const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
3083
+ lines.push("");
3084
+ lines.push(
3085
+ `**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" : ""}` : "")
3086
+ );
3087
+ }
3088
+ lines.push("");
3089
+ lines.push("| id | asset | kind | dims | scales | size | flags |");
3090
+ lines.push("|---|---|---|---|---|---|---|");
3091
+ for (const r of list.records) {
3092
+ const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
3093
+ lines.push(
3094
+ `| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
3095
+ );
3096
+ }
3097
+ lines.push("");
3098
+ lines.push(
3099
+ "_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
3100
+ );
3101
+ return lines.join("\n");
3102
+ }
3103
+ function registerAssetsTools(server, sync) {
3104
+ server.registerTool(
3105
+ "get_assets",
3106
+ {
3107
+ 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.",
3108
+ inputSchema: {
3109
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3110
+ limit: z16.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3111
+ kind: z16.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3112
+ unusedOnly: z16.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3113
+ }
3114
+ },
3115
+ async ({ deviceId, limit, kind, unusedOnly }) => {
3116
+ try {
3117
+ const target = sync.requireProDevice(deviceId);
3118
+ const capped = Math.min(Math.max(limit ?? 40, 1), 500);
3119
+ const list = await sync.callAction(
3120
+ ASSETS_TOOL_ID,
3121
+ "list",
3122
+ { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3123
+ target
3124
+ );
3125
+ if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3126
+ if (unusedOnly) {
3127
+ list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3128
+ }
3129
+ const md = formatListMarkdown2(list, capped);
3130
+ const slim = {
3131
+ stats: list.stats,
3132
+ scanStatus: list.scanStatus,
3133
+ insights: list.insights,
3134
+ diff: list.diff,
3135
+ records: list.records.map((r) => ({
3136
+ id: r.id,
3137
+ name: r.name,
3138
+ type: r.type,
3139
+ kind: r.kind,
3140
+ hash: r.hash,
3141
+ width: r.width,
3142
+ height: r.height,
3143
+ scales: r.scales,
3144
+ loaded: r.loaded,
3145
+ sizeBytes: r.sizeBytes,
3146
+ sizeSource: r.sizeSource
3147
+ }))
3148
+ };
3149
+ return text14(`${md}
3150
+
3151
+ \`\`\`json
3152
+ ${JSON.stringify(slim)}
3153
+ \`\`\``);
3154
+ } catch (e) {
3155
+ return fail16(errMsg16(e));
3156
+ }
3157
+ }
3158
+ );
3159
+ server.registerTool(
3160
+ "asset_action",
3161
+ {
3162
+ 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).",
3163
+ inputSchema: {
3164
+ deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
3165
+ action: z16.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3166
+ id: z16.number().optional().describe("Asset id from get_assets (detail only).")
3167
+ }
3168
+ },
3169
+ async ({ deviceId, action, id }) => {
3170
+ try {
3171
+ const target = sync.requireProDevice(deviceId);
3172
+ switch (action) {
3173
+ case "rescan": {
3174
+ const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3175
+ const ss = result?.scanStatus;
3176
+ if (!ss) return fail16("Device returned nothing for rescan.");
3177
+ return text14(
3178
+ `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" : "")
3179
+ );
3180
+ }
3181
+ case "measure_sizes": {
3182
+ const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3183
+ return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3184
+ }
3185
+ case "detail": {
3186
+ if (id == null) return fail16("detail requires the `id` param (from get_assets).");
3187
+ const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3188
+ if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3189
+ const lines = [
3190
+ `## ${detail.name}.${detail.type} (#${detail.id})`,
3191
+ "",
3192
+ `- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
3193
+ ];
3194
+ if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
3195
+ if (detail.sizesByScale) {
3196
+ for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
3197
+ lines.push(`- Size @${scale}x: ${kb(bytes)}`);
3198
+ }
3199
+ }
3200
+ if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
3201
+ if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
3202
+ if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
3203
+ if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3204
+ if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3205
+ if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3206
+ return text14(lines.join("\n"));
3207
+ }
3208
+ case "save_baseline": {
3209
+ const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3210
+ return text14(
3211
+ `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3212
+ );
3213
+ }
3214
+ case "clear_baseline": {
3215
+ await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3216
+ return text14("Baseline cleared.");
3217
+ }
3218
+ case "clear_records": {
3219
+ const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3220
+ return text14(result?.message ?? "Inventory cleared.");
3221
+ }
3222
+ }
3223
+ } catch (e) {
3224
+ return fail16(errMsg16(e));
3225
+ }
3226
+ }
3227
+ );
3228
+ }
3229
+
3230
+ // src/tools/packs/reload.ts
3231
+ import { z as z17 } from "zod";
3013
3232
  var APP_TOOL_ID = "app";
3014
3233
  var RELOAD_ACTION = "reloadApp";
3015
3234
  var DEFAULT_WAIT_MS = 3e4;
3016
3235
  var TEARDOWN_GRACE_MS = 1200;
3017
3236
  var PROBE_TIMEOUT_MS = 1500;
3018
3237
  var PROBE_INTERVAL_MS = 400;
3019
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3020
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3021
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3238
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3239
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3240
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3022
3241
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3023
3242
  async function ping(sync, deviceId, timeoutMs) {
3024
3243
  try {
@@ -3053,12 +3272,12 @@ function registerReloadTools(server, sync) {
3053
3272
  {
3054
3273
  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.",
3055
3274
  inputSchema: {
3056
- strategy: z16.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3275
+ strategy: z17.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3057
3276
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3058
3277
  ),
3059
- wait: z16.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3060
- timeoutMs: z16.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3061
- deviceId: z16.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3278
+ wait: z17.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3279
+ timeoutMs: z17.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3280
+ deviceId: z17.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3062
3281
  }
3063
3282
  },
3064
3283
  async (args) => {
@@ -3074,19 +3293,19 @@ function registerReloadTools(server, sync) {
3074
3293
  );
3075
3294
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3076
3295
  if (args.wait === false) {
3077
- return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3296
+ return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3078
3297
  }
3079
3298
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3080
3299
  if (elapsed == null) {
3081
- return fail16(
3300
+ return fail17(
3082
3301
  `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.`
3083
3302
  );
3084
3303
  }
3085
- return text14(
3304
+ return text15(
3086
3305
  `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.`
3087
3306
  );
3088
3307
  } catch (e) {
3089
- return fail16(errMsg16(e));
3308
+ return fail17(errMsg17(e));
3090
3309
  }
3091
3310
  }
3092
3311
  );
@@ -3130,6 +3349,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3130
3349
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3131
3350
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3132
3351
 
3352
+ Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
3353
+ fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
3354
+ WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
3355
+ the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
3356
+ get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
3357
+
3133
3358
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3134
3359
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3135
3360
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3166,11 +3391,12 @@ function createMcpServer(sync) {
3166
3391
  registerInteractTools(server, sync);
3167
3392
  registerJsThreadTools(server, sync);
3168
3393
  registerImagesTools(server, sync);
3394
+ registerAssetsTools(server, sync);
3169
3395
  registerReloadTools(server, sync);
3170
3396
  return server;
3171
3397
  }
3172
3398
  var deviceIdField = {
3173
- deviceId: z17.string().optional().describe(
3399
+ deviceId: z18.string().optional().describe(
3174
3400
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3175
3401
  )
3176
3402
  };
package/dist/cli.js CHANGED
@@ -330,7 +330,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
330
330
 
331
331
  // src/server.ts
332
332
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
333
- var import_zod17 = require("zod");
333
+ var import_zod18 = require("zod");
334
334
 
335
335
  // src/tools/generic.ts
336
336
  var import_zod = require("zod");
@@ -341,7 +341,7 @@ var import_node_os = __toESM(require("os"));
341
341
  var import_node_path = __toESM(require("path"));
342
342
 
343
343
  // src/version.ts
344
- var VERSION = "7.0.14" ? "7.0.14" : "0.0.0-dev";
344
+ var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
345
345
  var PACKAGE_NAME = "@buoy-gg/mcp";
346
346
 
347
347
  // src/update/checkForUpdate.ts
@@ -2911,7 +2911,9 @@ function registerImagesTools(server, sync) {
2911
2911
  error: r.error,
2912
2912
  errorCode: r.errorCode,
2913
2913
  overrideLabel: r.overrideLabel,
2914
- loadCount: r.loadCount
2914
+ loadCount: r.loadCount,
2915
+ hasAltText: r.hasAltText,
2916
+ layoutShifts: r.layoutShifts
2915
2917
  }))
2916
2918
  };
2917
2919
  return text13(`${md}
@@ -3040,17 +3042,234 @@ ${JSON.stringify(slim)}
3040
3042
  );
3041
3043
  }
3042
3044
 
3043
- // src/tools/packs/reload.ts
3045
+ // src/tools/packs/assets.ts
3044
3046
  var import_zod16 = require("zod");
3047
+ var ASSETS_TOOL_ID = "assets";
3048
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3049
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3050
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3051
+ function kb(bytes) {
3052
+ if (bytes == null || bytes <= 0) return "\u2013";
3053
+ if (bytes < 1024) return `${bytes}B`;
3054
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3055
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
3056
+ }
3057
+ function sizeLabel(r) {
3058
+ if (r.sizeBytes == null) return "\u2013";
3059
+ return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
3060
+ }
3061
+ function scalesLabel(scales) {
3062
+ if (!scales.length) return "\u2013";
3063
+ return scales.map((s) => `@${s}x`).join(" ");
3064
+ }
3065
+ function flagsLabel(r, ins) {
3066
+ const flags = [];
3067
+ if (!r.loaded) flags.push("UNUSED");
3068
+ if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
3069
+ if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
3070
+ if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
3071
+ if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
3072
+ else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
3073
+ if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
3074
+ return flags.length ? flags.join(" ") : "\u2013";
3075
+ }
3076
+ function formatListMarkdown2(list, limit) {
3077
+ const lines = [];
3078
+ const s = list.stats;
3079
+ const ss = list.scanStatus;
3080
+ lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
3081
+ lines.push("");
3082
+ const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
3083
+ lines.push(
3084
+ `${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
3085
+ );
3086
+ if (ss.fontFamilies.length > 0) {
3087
+ lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
3088
+ }
3089
+ if (!ss.graphSupported || ss.graphCount == null) {
3090
+ lines.push(
3091
+ "\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})` : "")
3092
+ );
3093
+ }
3094
+ if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
3095
+ const ins = list.insights;
3096
+ const chips = [];
3097
+ if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
3098
+ if (ins.duplicates.length) {
3099
+ const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
3100
+ chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
3101
+ }
3102
+ if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
3103
+ if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
3104
+ if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
3105
+ if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
3106
+ if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
3107
+ if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
3108
+ if (chips.length) {
3109
+ lines.push("");
3110
+ lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
3111
+ }
3112
+ const diff = list.diff;
3113
+ if (diff) {
3114
+ const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
3115
+ lines.push("");
3116
+ lines.push(
3117
+ `**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" : ""}` : "")
3118
+ );
3119
+ }
3120
+ lines.push("");
3121
+ lines.push("| id | asset | kind | dims | scales | size | flags |");
3122
+ lines.push("|---|---|---|---|---|---|---|");
3123
+ for (const r of list.records) {
3124
+ const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
3125
+ lines.push(
3126
+ `| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
3127
+ );
3128
+ }
3129
+ lines.push("");
3130
+ lines.push(
3131
+ "_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
3132
+ );
3133
+ return lines.join("\n");
3134
+ }
3135
+ function registerAssetsTools(server, sync) {
3136
+ server.registerTool(
3137
+ "get_assets",
3138
+ {
3139
+ description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
3140
+ inputSchema: {
3141
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3142
+ limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3143
+ kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3144
+ unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3145
+ }
3146
+ },
3147
+ async ({ deviceId, limit, kind, unusedOnly }) => {
3148
+ try {
3149
+ const target = sync.requireProDevice(deviceId);
3150
+ const capped = Math.min(Math.max(limit ?? 40, 1), 500);
3151
+ const list = await sync.callAction(
3152
+ ASSETS_TOOL_ID,
3153
+ "list",
3154
+ { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3155
+ target
3156
+ );
3157
+ if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3158
+ if (unusedOnly) {
3159
+ list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3160
+ }
3161
+ const md = formatListMarkdown2(list, capped);
3162
+ const slim = {
3163
+ stats: list.stats,
3164
+ scanStatus: list.scanStatus,
3165
+ insights: list.insights,
3166
+ diff: list.diff,
3167
+ records: list.records.map((r) => ({
3168
+ id: r.id,
3169
+ name: r.name,
3170
+ type: r.type,
3171
+ kind: r.kind,
3172
+ hash: r.hash,
3173
+ width: r.width,
3174
+ height: r.height,
3175
+ scales: r.scales,
3176
+ loaded: r.loaded,
3177
+ sizeBytes: r.sizeBytes,
3178
+ sizeSource: r.sizeSource
3179
+ }))
3180
+ };
3181
+ return text14(`${md}
3182
+
3183
+ \`\`\`json
3184
+ ${JSON.stringify(slim)}
3185
+ \`\`\``);
3186
+ } catch (e) {
3187
+ return fail16(errMsg16(e));
3188
+ }
3189
+ }
3190
+ );
3191
+ server.registerTool(
3192
+ "asset_action",
3193
+ {
3194
+ description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
3195
+ inputSchema: {
3196
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3197
+ action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3198
+ id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
3199
+ }
3200
+ },
3201
+ async ({ deviceId, action, id }) => {
3202
+ try {
3203
+ const target = sync.requireProDevice(deviceId);
3204
+ switch (action) {
3205
+ case "rescan": {
3206
+ const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3207
+ const ss = result?.scanStatus;
3208
+ if (!ss) return fail16("Device returned nothing for rescan.");
3209
+ return text14(
3210
+ `Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
3211
+ );
3212
+ }
3213
+ case "measure_sizes": {
3214
+ const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3215
+ return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3216
+ }
3217
+ case "detail": {
3218
+ if (id == null) return fail16("detail requires the `id` param (from get_assets).");
3219
+ const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3220
+ if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3221
+ const lines = [
3222
+ `## ${detail.name}.${detail.type} (#${detail.id})`,
3223
+ "",
3224
+ `- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
3225
+ ];
3226
+ if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
3227
+ if (detail.sizesByScale) {
3228
+ for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
3229
+ lines.push(`- Size @${scale}x: ${kb(bytes)}`);
3230
+ }
3231
+ }
3232
+ if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
3233
+ if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
3234
+ if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
3235
+ if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3236
+ if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3237
+ if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3238
+ return text14(lines.join("\n"));
3239
+ }
3240
+ case "save_baseline": {
3241
+ const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3242
+ return text14(
3243
+ `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3244
+ );
3245
+ }
3246
+ case "clear_baseline": {
3247
+ await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3248
+ return text14("Baseline cleared.");
3249
+ }
3250
+ case "clear_records": {
3251
+ const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3252
+ return text14(result?.message ?? "Inventory cleared.");
3253
+ }
3254
+ }
3255
+ } catch (e) {
3256
+ return fail16(errMsg16(e));
3257
+ }
3258
+ }
3259
+ );
3260
+ }
3261
+
3262
+ // src/tools/packs/reload.ts
3263
+ var import_zod17 = require("zod");
3045
3264
  var APP_TOOL_ID = "app";
3046
3265
  var RELOAD_ACTION = "reloadApp";
3047
3266
  var DEFAULT_WAIT_MS = 3e4;
3048
3267
  var TEARDOWN_GRACE_MS = 1200;
3049
3268
  var PROBE_TIMEOUT_MS = 1500;
3050
3269
  var PROBE_INTERVAL_MS = 400;
3051
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3052
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3053
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3270
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3271
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3272
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3054
3273
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3055
3274
  async function ping(sync, deviceId, timeoutMs) {
3056
3275
  try {
@@ -3085,12 +3304,12 @@ function registerReloadTools(server, sync) {
3085
3304
  {
3086
3305
  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.",
3087
3306
  inputSchema: {
3088
- strategy: import_zod16.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3307
+ strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3089
3308
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3090
3309
  ),
3091
- wait: import_zod16.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3092
- timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3093
- deviceId: import_zod16.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3310
+ wait: import_zod17.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3311
+ timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3312
+ deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3094
3313
  }
3095
3314
  },
3096
3315
  async (args) => {
@@ -3106,19 +3325,19 @@ function registerReloadTools(server, sync) {
3106
3325
  );
3107
3326
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3108
3327
  if (args.wait === false) {
3109
- return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3328
+ return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3110
3329
  }
3111
3330
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3112
3331
  if (elapsed == null) {
3113
- return fail16(
3332
+ return fail17(
3114
3333
  `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.`
3115
3334
  );
3116
3335
  }
3117
- return text14(
3336
+ return text15(
3118
3337
  `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.`
3119
3338
  );
3120
3339
  } catch (e) {
3121
- return fail16(errMsg16(e));
3340
+ return fail17(errMsg17(e));
3122
3341
  }
3123
3342
  }
3124
3343
  );
@@ -3162,6 +3381,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3162
3381
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3163
3382
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3164
3383
 
3384
+ Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
3385
+ fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
3386
+ WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
3387
+ the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
3388
+ get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
3389
+
3165
3390
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3166
3391
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3167
3392
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3198,11 +3423,12 @@ function createMcpServer(sync) {
3198
3423
  registerInteractTools(server, sync);
3199
3424
  registerJsThreadTools(server, sync);
3200
3425
  registerImagesTools(server, sync);
3426
+ registerAssetsTools(server, sync);
3201
3427
  registerReloadTools(server, sync);
3202
3428
  return server;
3203
3429
  }
3204
3430
  var deviceIdField = {
3205
- deviceId: import_zod17.z.string().optional().describe(
3431
+ deviceId: import_zod18.z.string().optional().describe(
3206
3432
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3207
3433
  )
3208
3434
  };
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-ZEHIU6OG.mjs";
6
+ } from "./chunk-P2OALRPK.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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 import_zod17 = require("zod");
368
+ var import_zod18 = 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.14" ? "7.0.14" : "0.0.0-dev";
379
+ var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
380
380
  var PACKAGE_NAME = "@buoy-gg/mcp";
381
381
 
382
382
  // src/update/checkForUpdate.ts
@@ -2946,7 +2946,9 @@ function registerImagesTools(server, sync) {
2946
2946
  error: r.error,
2947
2947
  errorCode: r.errorCode,
2948
2948
  overrideLabel: r.overrideLabel,
2949
- loadCount: r.loadCount
2949
+ loadCount: r.loadCount,
2950
+ hasAltText: r.hasAltText,
2951
+ layoutShifts: r.layoutShifts
2950
2952
  }))
2951
2953
  };
2952
2954
  return text13(`${md}
@@ -3075,17 +3077,234 @@ ${JSON.stringify(slim)}
3075
3077
  );
3076
3078
  }
3077
3079
 
3078
- // src/tools/packs/reload.ts
3080
+ // src/tools/packs/assets.ts
3079
3081
  var import_zod16 = require("zod");
3082
+ var ASSETS_TOOL_ID = "assets";
3083
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3084
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3085
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3086
+ function kb(bytes) {
3087
+ if (bytes == null || bytes <= 0) return "\u2013";
3088
+ if (bytes < 1024) return `${bytes}B`;
3089
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
3090
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
3091
+ }
3092
+ function sizeLabel(r) {
3093
+ if (r.sizeBytes == null) return "\u2013";
3094
+ return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
3095
+ }
3096
+ function scalesLabel(scales) {
3097
+ if (!scales.length) return "\u2013";
3098
+ return scales.map((s) => `@${s}x`).join(" ");
3099
+ }
3100
+ function flagsLabel(r, ins) {
3101
+ const flags = [];
3102
+ if (!r.loaded) flags.push("UNUSED");
3103
+ if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
3104
+ if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
3105
+ if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
3106
+ if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
3107
+ else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
3108
+ if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
3109
+ return flags.length ? flags.join(" ") : "\u2013";
3110
+ }
3111
+ function formatListMarkdown2(list, limit) {
3112
+ const lines = [];
3113
+ const s = list.stats;
3114
+ const ss = list.scanStatus;
3115
+ lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
3116
+ lines.push("");
3117
+ const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
3118
+ lines.push(
3119
+ `${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
3120
+ );
3121
+ if (ss.fontFamilies.length > 0) {
3122
+ lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
3123
+ }
3124
+ if (!ss.graphSupported || ss.graphCount == null) {
3125
+ lines.push(
3126
+ "\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})` : "")
3127
+ );
3128
+ }
3129
+ if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
3130
+ const ins = list.insights;
3131
+ const chips = [];
3132
+ if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
3133
+ if (ins.duplicates.length) {
3134
+ const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
3135
+ chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
3136
+ }
3137
+ if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
3138
+ if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
3139
+ if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
3140
+ if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
3141
+ if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
3142
+ if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
3143
+ if (chips.length) {
3144
+ lines.push("");
3145
+ lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
3146
+ }
3147
+ const diff = list.diff;
3148
+ if (diff) {
3149
+ const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
3150
+ lines.push("");
3151
+ lines.push(
3152
+ `**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" : ""}` : "")
3153
+ );
3154
+ }
3155
+ lines.push("");
3156
+ lines.push("| id | asset | kind | dims | scales | size | flags |");
3157
+ lines.push("|---|---|---|---|---|---|---|");
3158
+ for (const r of list.records) {
3159
+ const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
3160
+ lines.push(
3161
+ `| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
3162
+ );
3163
+ }
3164
+ lines.push("");
3165
+ lines.push(
3166
+ "_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
3167
+ );
3168
+ return lines.join("\n");
3169
+ }
3170
+ function registerAssetsTools(server, sync) {
3171
+ server.registerTool(
3172
+ "get_assets",
3173
+ {
3174
+ 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
+ inputSchema: {
3176
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3177
+ limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3178
+ kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3179
+ unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3180
+ }
3181
+ },
3182
+ async ({ deviceId, limit, kind, unusedOnly }) => {
3183
+ try {
3184
+ const target = sync.requireProDevice(deviceId);
3185
+ const capped = Math.min(Math.max(limit ?? 40, 1), 500);
3186
+ const list = await sync.callAction(
3187
+ ASSETS_TOOL_ID,
3188
+ "list",
3189
+ { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3190
+ target
3191
+ );
3192
+ if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3193
+ if (unusedOnly) {
3194
+ list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3195
+ }
3196
+ const md = formatListMarkdown2(list, capped);
3197
+ const slim = {
3198
+ stats: list.stats,
3199
+ scanStatus: list.scanStatus,
3200
+ insights: list.insights,
3201
+ diff: list.diff,
3202
+ records: list.records.map((r) => ({
3203
+ id: r.id,
3204
+ name: r.name,
3205
+ type: r.type,
3206
+ kind: r.kind,
3207
+ hash: r.hash,
3208
+ width: r.width,
3209
+ height: r.height,
3210
+ scales: r.scales,
3211
+ loaded: r.loaded,
3212
+ sizeBytes: r.sizeBytes,
3213
+ sizeSource: r.sizeSource
3214
+ }))
3215
+ };
3216
+ return text14(`${md}
3217
+
3218
+ \`\`\`json
3219
+ ${JSON.stringify(slim)}
3220
+ \`\`\``);
3221
+ } catch (e) {
3222
+ return fail16(errMsg16(e));
3223
+ }
3224
+ }
3225
+ );
3226
+ server.registerTool(
3227
+ "asset_action",
3228
+ {
3229
+ 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
+ inputSchema: {
3231
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3232
+ action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3233
+ id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
3234
+ }
3235
+ },
3236
+ async ({ deviceId, action, id }) => {
3237
+ try {
3238
+ const target = sync.requireProDevice(deviceId);
3239
+ switch (action) {
3240
+ case "rescan": {
3241
+ const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3242
+ const ss = result?.scanStatus;
3243
+ if (!ss) return fail16("Device returned nothing for rescan.");
3244
+ return text14(
3245
+ `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
+ );
3247
+ }
3248
+ case "measure_sizes": {
3249
+ const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3250
+ return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3251
+ }
3252
+ case "detail": {
3253
+ if (id == null) return fail16("detail requires the `id` param (from get_assets).");
3254
+ const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3255
+ if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3256
+ const lines = [
3257
+ `## ${detail.name}.${detail.type} (#${detail.id})`,
3258
+ "",
3259
+ `- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
3260
+ ];
3261
+ if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
3262
+ if (detail.sizesByScale) {
3263
+ for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
3264
+ lines.push(`- Size @${scale}x: ${kb(bytes)}`);
3265
+ }
3266
+ }
3267
+ if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
3268
+ if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
3269
+ if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
3270
+ if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3271
+ if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3272
+ if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3273
+ return text14(lines.join("\n"));
3274
+ }
3275
+ case "save_baseline": {
3276
+ const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3277
+ return text14(
3278
+ `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3279
+ );
3280
+ }
3281
+ case "clear_baseline": {
3282
+ await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3283
+ return text14("Baseline cleared.");
3284
+ }
3285
+ case "clear_records": {
3286
+ const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3287
+ return text14(result?.message ?? "Inventory cleared.");
3288
+ }
3289
+ }
3290
+ } catch (e) {
3291
+ return fail16(errMsg16(e));
3292
+ }
3293
+ }
3294
+ );
3295
+ }
3296
+
3297
+ // src/tools/packs/reload.ts
3298
+ var import_zod17 = require("zod");
3080
3299
  var APP_TOOL_ID = "app";
3081
3300
  var RELOAD_ACTION = "reloadApp";
3082
3301
  var DEFAULT_WAIT_MS = 3e4;
3083
3302
  var TEARDOWN_GRACE_MS = 1200;
3084
3303
  var PROBE_TIMEOUT_MS = 1500;
3085
3304
  var PROBE_INTERVAL_MS = 400;
3086
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3087
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3088
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3305
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3306
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3307
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3089
3308
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3090
3309
  async function ping(sync, deviceId, timeoutMs) {
3091
3310
  try {
@@ -3120,12 +3339,12 @@ function registerReloadTools(server, sync) {
3120
3339
  {
3121
3340
  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.",
3122
3341
  inputSchema: {
3123
- strategy: import_zod16.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3342
+ strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3124
3343
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3125
3344
  ),
3126
- wait: import_zod16.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3127
- timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3128
- deviceId: import_zod16.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3345
+ wait: import_zod17.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3346
+ timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3347
+ deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3129
3348
  }
3130
3349
  },
3131
3350
  async (args) => {
@@ -3141,19 +3360,19 @@ function registerReloadTools(server, sync) {
3141
3360
  );
3142
3361
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3143
3362
  if (args.wait === false) {
3144
- return text14(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3363
+ return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3145
3364
  }
3146
3365
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3147
3366
  if (elapsed == null) {
3148
- return fail16(
3367
+ return fail17(
3149
3368
  `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.`
3150
3369
  );
3151
3370
  }
3152
- return text14(
3371
+ return text15(
3153
3372
  `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.`
3154
3373
  );
3155
3374
  } catch (e) {
3156
- return fail16(errMsg16(e));
3375
+ return fail17(errMsg17(e));
3157
3376
  }
3158
3377
  }
3159
3378
  );
@@ -3197,6 +3416,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3197
3416
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3198
3417
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3199
3418
 
3419
+ Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
3420
+ fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
3421
+ WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
3422
+ the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
3423
+ get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
3424
+
3200
3425
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3201
3426
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3202
3427
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3233,11 +3458,12 @@ function createMcpServer(sync) {
3233
3458
  registerInteractTools(server, sync);
3234
3459
  registerJsThreadTools(server, sync);
3235
3460
  registerImagesTools(server, sync);
3461
+ registerAssetsTools(server, sync);
3236
3462
  registerReloadTools(server, sync);
3237
3463
  return server;
3238
3464
  }
3239
3465
  var deviceIdField = {
3240
- deviceId: import_zod17.z.string().optional().describe(
3466
+ deviceId: import_zod18.z.string().optional().describe(
3241
3467
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3242
3468
  )
3243
3469
  };
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  registerStorageTools,
28
28
  registerZustandTools,
29
29
  runInit
30
- } from "./chunk-ZEHIU6OG.mjs";
30
+ } from "./chunk-P2OALRPK.mjs";
31
31
  export {
32
32
  ALL_DEVICES,
33
33
  BROKER_PORT,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/mcp",
3
3
  "mcpName": "io.github.buoy-gg/mcp",
4
- "version": "7.0.14",
4
+ "version": "7.0.16",
5
5
  "description": "MCP server for Buoy devtools — lets any LLM copilot drive Buoy tools (perf-monitor benchmarks, etc.) over the devtool sync protocol",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
32
  "socket.io-client": "^4.8.1",
33
33
  "zod": "^3.25.0",
34
- "@buoy-gg/sync-broker": "^7.0.14"
34
+ "@buoy-gg/sync-broker": "^7.0.16"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",