@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.
- package/dist/{chunk-I3Z3IWDH.mjs → chunk-P2OALRPK.mjs} +239 -15
- package/dist/cli.js +239 -15
- package/dist/cli.mjs +1 -1
- package/dist/index.js +239 -15
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -301,7 +301,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
301
301
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
302
302
|
|
|
303
303
|
// src/version.ts
|
|
304
|
-
var VERSION = "7.0.
|
|
304
|
+
var VERSION = "7.0.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
|
|
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/
|
|
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
|
|
3022
|
-
var
|
|
3023
|
-
var
|
|
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:
|
|
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:
|
|
3062
|
-
timeoutMs:
|
|
3063
|
-
deviceId:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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/
|
|
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
|
|
3054
|
-
var
|
|
3055
|
-
var
|
|
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:
|
|
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:
|
|
3094
|
-
timeoutMs:
|
|
3095
|
-
deviceId:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
package/dist/index.js
CHANGED
|
@@ -365,7 +365,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
365
365
|
|
|
366
366
|
// src/server.ts
|
|
367
367
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
368
|
-
var
|
|
368
|
+
var import_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.
|
|
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/
|
|
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
|
|
3089
|
-
var
|
|
3090
|
-
var
|
|
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:
|
|
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:
|
|
3129
|
-
timeoutMs:
|
|
3130
|
-
deviceId:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
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.
|
|
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.
|
|
34
|
+
"@buoy-gg/sync-broker": "^7.0.16"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|