@buoy-gg/mcp 7.0.15 → 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.15" ? "7.0.15" : "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";
@@ -3010,17 +3010,234 @@ ${JSON.stringify(slim)}
3010
3010
  );
3011
3011
  }
3012
3012
 
3013
- // src/tools/packs/reload.ts
3013
+ // src/tools/packs/assets.ts
3014
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";
3015
3232
  var APP_TOOL_ID = "app";
3016
3233
  var RELOAD_ACTION = "reloadApp";
3017
3234
  var DEFAULT_WAIT_MS = 3e4;
3018
3235
  var TEARDOWN_GRACE_MS = 1200;
3019
3236
  var PROBE_TIMEOUT_MS = 1500;
3020
3237
  var PROBE_INTERVAL_MS = 400;
3021
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3022
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3023
- 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);
3024
3241
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3025
3242
  async function ping(sync, deviceId, timeoutMs) {
3026
3243
  try {
@@ -3055,12 +3272,12 @@ function registerReloadTools(server, sync) {
3055
3272
  {
3056
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.",
3057
3274
  inputSchema: {
3058
- strategy: z16.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3275
+ strategy: z17.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3059
3276
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3060
3277
  ),
3061
- wait: z16.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3062
- timeoutMs: z16.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3063
- 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.')
3064
3281
  }
3065
3282
  },
3066
3283
  async (args) => {
@@ -3076,19 +3293,19 @@ function registerReloadTools(server, sync) {
3076
3293
  );
3077
3294
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3078
3295
  if (args.wait === false) {
3079
- 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.`);
3080
3297
  }
3081
3298
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3082
3299
  if (elapsed == null) {
3083
- return fail16(
3300
+ return fail17(
3084
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.`
3085
3302
  );
3086
3303
  }
3087
- return text14(
3304
+ return text15(
3088
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.`
3089
3306
  );
3090
3307
  } catch (e) {
3091
- return fail16(errMsg16(e));
3308
+ return fail17(errMsg17(e));
3092
3309
  }
3093
3310
  }
3094
3311
  );
@@ -3132,6 +3349,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3132
3349
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3133
3350
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3134
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
+
3135
3358
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3136
3359
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3137
3360
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3168,11 +3391,12 @@ function createMcpServer(sync) {
3168
3391
  registerInteractTools(server, sync);
3169
3392
  registerJsThreadTools(server, sync);
3170
3393
  registerImagesTools(server, sync);
3394
+ registerAssetsTools(server, sync);
3171
3395
  registerReloadTools(server, sync);
3172
3396
  return server;
3173
3397
  }
3174
3398
  var deviceIdField = {
3175
- deviceId: z17.string().optional().describe(
3399
+ deviceId: z18.string().optional().describe(
3176
3400
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3177
3401
  )
3178
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.15" ? "7.0.15" : "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
@@ -3042,17 +3042,234 @@ ${JSON.stringify(slim)}
3042
3042
  );
3043
3043
  }
3044
3044
 
3045
- // src/tools/packs/reload.ts
3045
+ // src/tools/packs/assets.ts
3046
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");
3047
3264
  var APP_TOOL_ID = "app";
3048
3265
  var RELOAD_ACTION = "reloadApp";
3049
3266
  var DEFAULT_WAIT_MS = 3e4;
3050
3267
  var TEARDOWN_GRACE_MS = 1200;
3051
3268
  var PROBE_TIMEOUT_MS = 1500;
3052
3269
  var PROBE_INTERVAL_MS = 400;
3053
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3054
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3055
- 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);
3056
3273
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3057
3274
  async function ping(sync, deviceId, timeoutMs) {
3058
3275
  try {
@@ -3087,12 +3304,12 @@ function registerReloadTools(server, sync) {
3087
3304
  {
3088
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.",
3089
3306
  inputSchema: {
3090
- 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(
3091
3308
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3092
3309
  ),
3093
- 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."),
3094
- timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3095
- 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.')
3096
3313
  }
3097
3314
  },
3098
3315
  async (args) => {
@@ -3108,19 +3325,19 @@ function registerReloadTools(server, sync) {
3108
3325
  );
3109
3326
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3110
3327
  if (args.wait === false) {
3111
- 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.`);
3112
3329
  }
3113
3330
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3114
3331
  if (elapsed == null) {
3115
- return fail16(
3332
+ return fail17(
3116
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.`
3117
3334
  );
3118
3335
  }
3119
- return text14(
3336
+ return text15(
3120
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.`
3121
3338
  );
3122
3339
  } catch (e) {
3123
- return fail16(errMsg16(e));
3340
+ return fail17(errMsg17(e));
3124
3341
  }
3125
3342
  }
3126
3343
  );
@@ -3164,6 +3381,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3164
3381
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3165
3382
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3166
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
+
3167
3390
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3168
3391
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3169
3392
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3200,11 +3423,12 @@ function createMcpServer(sync) {
3200
3423
  registerInteractTools(server, sync);
3201
3424
  registerJsThreadTools(server, sync);
3202
3425
  registerImagesTools(server, sync);
3426
+ registerAssetsTools(server, sync);
3203
3427
  registerReloadTools(server, sync);
3204
3428
  return server;
3205
3429
  }
3206
3430
  var deviceIdField = {
3207
- deviceId: import_zod17.z.string().optional().describe(
3431
+ deviceId: import_zod18.z.string().optional().describe(
3208
3432
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3209
3433
  )
3210
3434
  };
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-I3Z3IWDH.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.15" ? "7.0.15" : "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
@@ -3077,17 +3077,234 @@ ${JSON.stringify(slim)}
3077
3077
  );
3078
3078
  }
3079
3079
 
3080
- // src/tools/packs/reload.ts
3080
+ // src/tools/packs/assets.ts
3081
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");
3082
3299
  var APP_TOOL_ID = "app";
3083
3300
  var RELOAD_ACTION = "reloadApp";
3084
3301
  var DEFAULT_WAIT_MS = 3e4;
3085
3302
  var TEARDOWN_GRACE_MS = 1200;
3086
3303
  var PROBE_TIMEOUT_MS = 1500;
3087
3304
  var PROBE_INTERVAL_MS = 400;
3088
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3089
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3090
- 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);
3091
3308
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3092
3309
  async function ping(sync, deviceId, timeoutMs) {
3093
3310
  try {
@@ -3122,12 +3339,12 @@ function registerReloadTools(server, sync) {
3122
3339
  {
3123
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.",
3124
3341
  inputSchema: {
3125
- 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(
3126
3343
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3127
3344
  ),
3128
- 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."),
3129
- timeoutMs: import_zod16.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3130
- 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.')
3131
3348
  }
3132
3349
  },
3133
3350
  async (args) => {
@@ -3143,19 +3360,19 @@ function registerReloadTools(server, sync) {
3143
3360
  );
3144
3361
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3145
3362
  if (args.wait === false) {
3146
- 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.`);
3147
3364
  }
3148
3365
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3149
3366
  if (elapsed == null) {
3150
- return fail16(
3367
+ return fail17(
3151
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.`
3152
3369
  );
3153
3370
  }
3154
- return text14(
3371
+ return text15(
3155
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.`
3156
3373
  );
3157
3374
  } catch (e) {
3158
- return fail16(errMsg16(e));
3375
+ return fail17(errMsg17(e));
3159
3376
  }
3160
3377
  }
3161
3378
  );
@@ -3199,6 +3416,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
3199
3416
  expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
3200
3417
  simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
3201
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
+
3202
3425
  Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
3203
3426
  measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
3204
3427
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
@@ -3235,11 +3458,12 @@ function createMcpServer(sync) {
3235
3458
  registerInteractTools(server, sync);
3236
3459
  registerJsThreadTools(server, sync);
3237
3460
  registerImagesTools(server, sync);
3461
+ registerAssetsTools(server, sync);
3238
3462
  registerReloadTools(server, sync);
3239
3463
  return server;
3240
3464
  }
3241
3465
  var deviceIdField = {
3242
- deviceId: import_zod17.z.string().optional().describe(
3466
+ deviceId: import_zod18.z.string().optional().describe(
3243
3467
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3244
3468
  )
3245
3469
  };
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  registerStorageTools,
28
28
  registerZustandTools,
29
29
  runInit
30
- } from "./chunk-I3Z3IWDH.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.15",
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.15"
34
+ "@buoy-gg/sync-broker": "^7.0.16"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",