@buoy-gg/mcp 7.0.30 → 7.0.31
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-POUWWZXB.mjs → chunk-HUL2D27L.mjs} +458 -161
- package/dist/cli.js +471 -174
- package/dist/cli.mjs +1 -1
- package/dist/index.js +471 -174
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -613,7 +613,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
613
613
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
614
614
|
|
|
615
615
|
// src/version.ts
|
|
616
|
-
var VERSION = "7.0.
|
|
616
|
+
var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
|
|
617
617
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
618
618
|
|
|
619
619
|
// src/update/checkForUpdate.ts
|
|
@@ -2462,7 +2462,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2462
2462
|
|
|
2463
2463
|
// src/server.ts
|
|
2464
2464
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2465
|
-
import { z as
|
|
2465
|
+
import { z as z23 } from "zod";
|
|
2466
2466
|
|
|
2467
2467
|
// src/tools/packs/network.ts
|
|
2468
2468
|
import { z as z12 } from "zod";
|
|
@@ -3111,15 +3111,310 @@ ${lines.join("\n")}`);
|
|
|
3111
3111
|
);
|
|
3112
3112
|
}
|
|
3113
3113
|
|
|
3114
|
-
// src/tools/packs/
|
|
3114
|
+
// src/tools/packs/scenarios.ts
|
|
3115
3115
|
import { z as z14 } from "zod";
|
|
3116
|
-
var
|
|
3116
|
+
var SCENARIOS_TOOL_ID = "scenarios";
|
|
3117
3117
|
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3118
3118
|
var fail14 = (message) => ({
|
|
3119
3119
|
isError: true,
|
|
3120
3120
|
content: [{ type: "text", text: message }]
|
|
3121
3121
|
});
|
|
3122
3122
|
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
3123
|
+
function varSpec(v) {
|
|
3124
|
+
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
3125
|
+
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
3126
|
+
return `${v.key}: ${v.type}${opts}${def}`;
|
|
3127
|
+
}
|
|
3128
|
+
function summarize(s) {
|
|
3129
|
+
const vars = s.vars?.length ? `
|
|
3130
|
+
variables: ${s.vars.map(varSpec).join("; ")}` : "";
|
|
3131
|
+
const steps = s.stepLines?.length ? `
|
|
3132
|
+
${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
|
|
3133
|
+
const meta = [
|
|
3134
|
+
`v${s.version}`,
|
|
3135
|
+
s.author,
|
|
3136
|
+
s.usage?.runs ? `${s.usage.runs} run(s)` : null,
|
|
3137
|
+
s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
|
|
3138
|
+
s.unsavedToRepo ? "unsaved to repo" : null
|
|
3139
|
+
].filter(Boolean).join(" \xB7 ");
|
|
3140
|
+
const outcome = s.expectedOutcome ? `
|
|
3141
|
+
expected: ${s.expectedOutcome}` : "";
|
|
3142
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
|
|
3143
|
+
}
|
|
3144
|
+
function renderSection(title, list, folders) {
|
|
3145
|
+
if (!list.length) return `**${title} (0):**
|
|
3146
|
+
_none_`;
|
|
3147
|
+
if (!folders.length) {
|
|
3148
|
+
return `**${title} (${list.length}):**
|
|
3149
|
+
${list.map(summarize).join("\n")}`;
|
|
3150
|
+
}
|
|
3151
|
+
const parts = [`**${title} (${list.length}):**`];
|
|
3152
|
+
for (const folder of folders) {
|
|
3153
|
+
const inIt = list.filter(
|
|
3154
|
+
(s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
|
|
3155
|
+
);
|
|
3156
|
+
if (!inIt.length) continue;
|
|
3157
|
+
parts.push(`_${folder} (${inIt.length})_
|
|
3158
|
+
${inIt.map(summarize).join("\n")}`);
|
|
3159
|
+
}
|
|
3160
|
+
const ungrouped = list.filter((s) => !s.folder);
|
|
3161
|
+
if (ungrouped.length) {
|
|
3162
|
+
parts.push(
|
|
3163
|
+
`_Ungrouped (${ungrouped.length})_
|
|
3164
|
+
${ungrouped.map(summarize).join("\n")}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
return parts.join("\n");
|
|
3168
|
+
}
|
|
3169
|
+
function formatActive(a) {
|
|
3170
|
+
const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
|
|
3171
|
+
return [
|
|
3172
|
+
`**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
|
|
3173
|
+
vars ? `variables: ${vars}` : null,
|
|
3174
|
+
`effects:
|
|
3175
|
+
${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
|
|
3176
|
+
].filter(Boolean).join("\n");
|
|
3177
|
+
}
|
|
3178
|
+
function registerScenariosTools(server, sync) {
|
|
3179
|
+
server.registerTool(
|
|
3180
|
+
"get_scenarios",
|
|
3181
|
+
{
|
|
3182
|
+
description: "List the app's saved Scenarios \u2014 named, parameterized app states that can be applied with one call (override a response, write storage, impersonate a user, navigate). Shows what each one WILL do in plain English with a durability label per step, plus which scenario is currently active and what it changed. Use scenario_action to run one as a deterministic fixture BEFORE driving a flow, so the flow starts from a known state instead of whatever the app happened to be in. If a scenario is active, whatever you observe on the device is partly simulated \u2014 check here first when the app's data looks wrong.",
|
|
3183
|
+
inputSchema: {
|
|
3184
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3185
|
+
}
|
|
3186
|
+
},
|
|
3187
|
+
async (args) => {
|
|
3188
|
+
try {
|
|
3189
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3190
|
+
const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
|
|
3191
|
+
const p = payload ?? {};
|
|
3192
|
+
const code = p.code ?? [];
|
|
3193
|
+
const device = p.device ?? [];
|
|
3194
|
+
const drafts = p.drafts ?? [];
|
|
3195
|
+
const folders = p.folders ?? [];
|
|
3196
|
+
const parts = [];
|
|
3197
|
+
parts.push(
|
|
3198
|
+
p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
|
|
3199
|
+
${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
|
|
3200
|
+
);
|
|
3201
|
+
if (folders.length) {
|
|
3202
|
+
parts.push(
|
|
3203
|
+
`**Folders:** ${folders.join(" \xB7 ")} \u2014 the flows this team organizes by. Scenarios below are grouped under them; file new ones into an existing folder with \`scenario_action setFolder\` rather than inventing a name.`
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3206
|
+
parts.push(renderSection("Shipped with this app", code, folders));
|
|
3207
|
+
parts.push(renderSection("Saved on this device", device, folders));
|
|
3208
|
+
if (drafts.length) {
|
|
3209
|
+
parts.push(
|
|
3210
|
+
`**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
|
|
3211
|
+
${drafts.map(summarize).join("\n")}`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
return text12(parts.join("\n\n"));
|
|
3215
|
+
} catch (e) {
|
|
3216
|
+
return fail14(errMsg14(e));
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
);
|
|
3220
|
+
server.registerTool(
|
|
3221
|
+
"scenario_action",
|
|
3222
|
+
{
|
|
3223
|
+
description: `Drive Buoy Scenarios: 'preview' dry-runs one and reports exactly what it WOULD change (plus any conflict with the active scenario) without touching the app \u2014 do this before running an unfamiliar scenario; 'run' applies it and resolves only once every step has landed, which makes it a deterministic fixture to call before run_flow; 'deactivate' reverses it and reports what it could NOT restore (storage writes have no automatic inverse); 'save' writes a new scenario onto the device \u2014 it lands as a DRAFT that a human must accept on the device before it can run, so tell the user to go accept it; 'setFolder' files a saved scenario under a flow ("Checkout", "Login") or clears its folder, which is how a QA library stays navigable \u2014 check get_scenarios for the folders already in use and reuse one instead of inventing a near-duplicate; 'delete' removes a device scenario. Prefer a network override step over a query-cache poke: the override survives refetch and restart, the poke dies on the next fetch.`,
|
|
3224
|
+
inputSchema: {
|
|
3225
|
+
action: z14.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3226
|
+
folder: z14.string().optional().describe(
|
|
3227
|
+
"Folder name for action 'setFolder' \u2014 omit to un-file the scenario. Matching an existing folder case-insensitively files it there rather than creating a second one."
|
|
3228
|
+
),
|
|
3229
|
+
id: z14.string().optional().describe(
|
|
3230
|
+
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3231
|
+
),
|
|
3232
|
+
params: z14.record(z14.union([z14.string(), z14.number(), z14.boolean()])).optional().describe(
|
|
3233
|
+
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3234
|
+
),
|
|
3235
|
+
scenario: z14.object({
|
|
3236
|
+
id: z14.string().describe("Stable kebab-case id."),
|
|
3237
|
+
name: z14.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3238
|
+
description: z14.string().optional(),
|
|
3239
|
+
expectedOutcome: z14.string().optional().describe(
|
|
3240
|
+
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3241
|
+
),
|
|
3242
|
+
author: z14.string().optional(),
|
|
3243
|
+
authoredAgainst: z14.string().optional().describe("App version this was written against."),
|
|
3244
|
+
folder: z14.string().optional().describe(
|
|
3245
|
+
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3246
|
+
),
|
|
3247
|
+
tags: z14.array(z14.string()).optional(),
|
|
3248
|
+
vars: z14.array(
|
|
3249
|
+
z14.object({
|
|
3250
|
+
key: z14.string(),
|
|
3251
|
+
label: z14.string().optional(),
|
|
3252
|
+
type: z14.enum(["string", "number", "boolean", "enum"]),
|
|
3253
|
+
options: z14.array(z14.union([z14.string(), z14.number(), z14.boolean()])).optional(),
|
|
3254
|
+
default: z14.union([z14.string(), z14.number(), z14.boolean()]).optional()
|
|
3255
|
+
})
|
|
3256
|
+
).optional().describe(
|
|
3257
|
+
"Declare a variable for every value a tester might want to change (a store id, a user, a status code) instead of freezing it into the steps."
|
|
3258
|
+
),
|
|
3259
|
+
steps: z14.array(
|
|
3260
|
+
z14.object({
|
|
3261
|
+
id: z14.string().optional(),
|
|
3262
|
+
tool: z14.string().describe(
|
|
3263
|
+
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3264
|
+
),
|
|
3265
|
+
action: z14.string().describe(
|
|
3266
|
+
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3267
|
+
),
|
|
3268
|
+
params: z14.unknown().optional(),
|
|
3269
|
+
label: z14.string().optional()
|
|
3270
|
+
})
|
|
3271
|
+
).describe("Steps, in order."),
|
|
3272
|
+
undoSteps: z14.array(
|
|
3273
|
+
z14.object({
|
|
3274
|
+
id: z14.string().optional(),
|
|
3275
|
+
tool: z14.string(),
|
|
3276
|
+
action: z14.string(),
|
|
3277
|
+
params: z14.unknown().optional(),
|
|
3278
|
+
label: z14.string().optional()
|
|
3279
|
+
})
|
|
3280
|
+
).optional().describe(
|
|
3281
|
+
"How to reverse effects that have no automatic inverse. Supply these whenever a step writes or removes storage, or deactivation will honestly report the key as unrestored."
|
|
3282
|
+
)
|
|
3283
|
+
}).optional().describe("The scenario to save (action 'save')."),
|
|
3284
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3285
|
+
}
|
|
3286
|
+
},
|
|
3287
|
+
async (args) => {
|
|
3288
|
+
try {
|
|
3289
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3290
|
+
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3291
|
+
switch (args.action) {
|
|
3292
|
+
case "preview": {
|
|
3293
|
+
if (!args.id) return fail14("preview requires `id` (from get_scenarios).");
|
|
3294
|
+
const r = await call("preview", {
|
|
3295
|
+
id: args.id,
|
|
3296
|
+
params: args.params ?? {}
|
|
3297
|
+
});
|
|
3298
|
+
const lines = [
|
|
3299
|
+
`**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
|
|
3300
|
+
`variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
|
|
3301
|
+
`**Would do:**
|
|
3302
|
+
${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
3303
|
+
];
|
|
3304
|
+
if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
|
|
3305
|
+
if (r.conflict) {
|
|
3306
|
+
lines.push(
|
|
3307
|
+
`\u26A0\uFE0F **Conflict:** "${r.conflict.activeName}" is active and touches the same target(s): ${r.conflict.overlaps.join(", ")}. Running will SWAP \u2014 it deactivates first.`
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3311
|
+
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3312
|
+
lines.push(
|
|
3313
|
+
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3314
|
+
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3315
|
+
);
|
|
3316
|
+
return text12(lines.join("\n\n"));
|
|
3317
|
+
}
|
|
3318
|
+
case "run": {
|
|
3319
|
+
if (!args.id) return fail14("run requires `id` (from get_scenarios).");
|
|
3320
|
+
const r = await call("run", {
|
|
3321
|
+
id: args.id,
|
|
3322
|
+
params: args.params ?? {}
|
|
3323
|
+
});
|
|
3324
|
+
if (r.preflightErrors?.length) {
|
|
3325
|
+
return fail14(
|
|
3326
|
+
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3327
|
+
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
const steps = (r.steps ?? []).map((s) => {
|
|
3331
|
+
const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
|
|
3332
|
+
const err = s.error ? `
|
|
3333
|
+
${s.error}` : "";
|
|
3334
|
+
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3335
|
+
}).join("\n");
|
|
3336
|
+
if (!r.ok) {
|
|
3337
|
+
return fail14(
|
|
3338
|
+
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3339
|
+
${steps}
|
|
3340
|
+
|
|
3341
|
+
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
return text12(
|
|
3345
|
+
[
|
|
3346
|
+
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3347
|
+
steps,
|
|
3348
|
+
`**Now bent:**
|
|
3349
|
+
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3350
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
|
|
3351
|
+
].join("\n\n")
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
case "deactivate": {
|
|
3355
|
+
const r = await call("deactivate", {});
|
|
3356
|
+
const rep = r.report;
|
|
3357
|
+
const lines = [
|
|
3358
|
+
`Removed ${rep.removedRules} override rule(s)`,
|
|
3359
|
+
rep.stoppedImpersonation ? "Ended impersonation" : null,
|
|
3360
|
+
rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
|
|
3361
|
+
].filter(Boolean);
|
|
3362
|
+
const warn = rep.unrestoredStorageKeys.length ? `
|
|
3363
|
+
|
|
3364
|
+
\u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
|
|
3365
|
+
const errs = rep.errors.length ? `
|
|
3366
|
+
|
|
3367
|
+
Errors: ${rep.errors.join("; ")}` : "";
|
|
3368
|
+
return text12(`**Deactivated.**
|
|
3369
|
+
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3370
|
+
}
|
|
3371
|
+
case "save": {
|
|
3372
|
+
if (!args.scenario) return fail14("save requires `scenario`.");
|
|
3373
|
+
const r = await call("save", { scenario: args.scenario });
|
|
3374
|
+
if (!r.ok) {
|
|
3375
|
+
return fail14(
|
|
3376
|
+
`Save rejected:
|
|
3377
|
+
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
return text12(
|
|
3381
|
+
`Saved \`${r.id}\` **as a draft**.
|
|
3382
|
+
|
|
3383
|
+
It is INERT and cannot run yet. Ask the user to open Scenarios on the device, review what it will do, and tap **Accept to library** \u2014 a scenario never becomes runnable on someone's device without them seeing it first.`
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
case "setFolder": {
|
|
3387
|
+
if (!args.id) return fail14("setFolder requires `id` (from get_scenarios).");
|
|
3388
|
+
const r = await call("setFolder", {
|
|
3389
|
+
id: args.id,
|
|
3390
|
+
folder: args.folder
|
|
3391
|
+
});
|
|
3392
|
+
return text12(
|
|
3393
|
+
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
case "delete": {
|
|
3397
|
+
if (!args.id) return fail14("delete requires `id`.");
|
|
3398
|
+
await call("delete", { id: args.id });
|
|
3399
|
+
return text12(`Deleted \`${args.id}\` from the device library.`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
} catch (e) {
|
|
3403
|
+
return fail14(errMsg14(e));
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/tools/packs/interact.ts
|
|
3410
|
+
import { z as z15 } from "zod";
|
|
3411
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3412
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3413
|
+
var fail15 = (message) => ({
|
|
3414
|
+
isError: true,
|
|
3415
|
+
content: [{ type: "text", text: message }]
|
|
3416
|
+
});
|
|
3417
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3123
3418
|
function formatElement(e) {
|
|
3124
3419
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3125
3420
|
line += e.name;
|
|
@@ -3142,8 +3437,8 @@ function registerInteractTools(server, sync) {
|
|
|
3142
3437
|
{
|
|
3143
3438
|
description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. If you already know the sequence you want to drive, `run_flow` does the whole thing (taps, waits, assertions) in one call instead. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
|
|
3144
3439
|
inputSchema: {
|
|
3145
|
-
interactiveOnly:
|
|
3146
|
-
deviceId:
|
|
3440
|
+
interactiveOnly: z15.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3441
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3147
3442
|
}
|
|
3148
3443
|
},
|
|
3149
3444
|
async (args) => {
|
|
@@ -3153,16 +3448,16 @@ function registerInteractTools(server, sync) {
|
|
|
3153
3448
|
const all = res?.elements ?? [];
|
|
3154
3449
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3155
3450
|
if (elements.length === 0) {
|
|
3156
|
-
return
|
|
3451
|
+
return text13(
|
|
3157
3452
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3158
3453
|
);
|
|
3159
3454
|
}
|
|
3160
3455
|
const header = `Screen ${res.screen.width}\xD7${res.screen.height} \u2014 ${elements.length} element${elements.length === 1 ? "" : "s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).`;
|
|
3161
|
-
return
|
|
3456
|
+
return text13(`${header}
|
|
3162
3457
|
|
|
3163
3458
|
${elements.map(formatElement).join("\n")}`);
|
|
3164
3459
|
} catch (e) {
|
|
3165
|
-
return
|
|
3460
|
+
return fail15(errMsg15(e));
|
|
3166
3461
|
}
|
|
3167
3462
|
}
|
|
3168
3463
|
);
|
|
@@ -3171,20 +3466,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3171
3466
|
{
|
|
3172
3467
|
description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Driving MORE THAN ONE step (reproducing a bug, verifying a fix)? Use `run_flow` instead \u2014 it runs the whole sequence in one call. Requires Buoy Pro.",
|
|
3173
3468
|
inputSchema: {
|
|
3174
|
-
nativeTag:
|
|
3175
|
-
testID:
|
|
3176
|
-
query:
|
|
3177
|
-
value:
|
|
3178
|
-
text:
|
|
3179
|
-
longPress:
|
|
3180
|
-
scrollIntoView:
|
|
3181
|
-
deviceId:
|
|
3469
|
+
nativeTag: z15.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3470
|
+
testID: z15.string().optional().describe("Exact testID of the element."),
|
|
3471
|
+
query: z15.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3472
|
+
value: z15.union([z15.number(), z15.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
3473
|
+
text: z15.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3474
|
+
longPress: z15.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3475
|
+
scrollIntoView: z15.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3476
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3182
3477
|
}
|
|
3183
3478
|
},
|
|
3184
3479
|
async (args) => {
|
|
3185
3480
|
try {
|
|
3186
3481
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3187
|
-
return
|
|
3482
|
+
return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3188
3483
|
}
|
|
3189
3484
|
const target = sync.requireProDevice(args.deviceId);
|
|
3190
3485
|
const res = await sync.callAction(
|
|
@@ -3209,26 +3504,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3209
3504
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3210
3505
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3211
3506
|
const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : m.via === "onLongPress" ? `Long-pressed "${label}"${id}` : `Tapped "${label}"${id}`;
|
|
3212
|
-
return
|
|
3507
|
+
return text13(`${verb}${scrolledNote}.${flowTip}`);
|
|
3213
3508
|
}
|
|
3214
3509
|
let msg = res?.reason ?? "Tap failed.";
|
|
3215
3510
|
if (res?.candidates?.length) {
|
|
3216
3511
|
msg += "\n\nTappable candidates:\n" + res.candidates.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n");
|
|
3217
3512
|
}
|
|
3218
|
-
return
|
|
3513
|
+
return fail15(msg);
|
|
3219
3514
|
} catch (e) {
|
|
3220
|
-
return
|
|
3515
|
+
return fail15(errMsg15(e));
|
|
3221
3516
|
}
|
|
3222
3517
|
}
|
|
3223
3518
|
);
|
|
3224
3519
|
}
|
|
3225
3520
|
|
|
3226
3521
|
// src/tools/packs/jsThread.ts
|
|
3227
|
-
import { z as
|
|
3522
|
+
import { z as z16 } from "zod";
|
|
3228
3523
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3229
|
-
var
|
|
3230
|
-
var
|
|
3231
|
-
var
|
|
3524
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3525
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3526
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3232
3527
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3233
3528
|
function sparkline(values) {
|
|
3234
3529
|
const max = Math.max(1, ...values);
|
|
@@ -3299,9 +3594,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3299
3594
|
{
|
|
3300
3595
|
description: 'Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. NOT the tool for React components re-rendering too often \u2014 this measures TASKS on the thread, not renders; for "the page re-renders constantly / typing lags / tapping one button re-renders everything" use measure_renders. Requires @buoy-gg/js-top on the device.',
|
|
3301
3596
|
inputSchema: {
|
|
3302
|
-
deviceId:
|
|
3303
|
-
durationMs:
|
|
3304
|
-
clearFirst:
|
|
3597
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3598
|
+
durationMs: z16.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
3599
|
+
clearFirst: z16.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3305
3600
|
}
|
|
3306
3601
|
},
|
|
3307
3602
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3314,7 +3609,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3314
3609
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3315
3610
|
target
|
|
3316
3611
|
);
|
|
3317
|
-
if (!snap) return
|
|
3612
|
+
if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3318
3613
|
const md = formatTopMarkdown(snap, duration);
|
|
3319
3614
|
const slim = {
|
|
3320
3615
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3333,13 +3628,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3333
3628
|
worstFreezeMs: r.worstFreezeMs
|
|
3334
3629
|
}))
|
|
3335
3630
|
};
|
|
3336
|
-
return
|
|
3631
|
+
return text14(`${md}
|
|
3337
3632
|
|
|
3338
3633
|
\`\`\`json
|
|
3339
3634
|
${JSON.stringify(slim)}
|
|
3340
3635
|
\`\`\``);
|
|
3341
3636
|
} catch (e) {
|
|
3342
|
-
return
|
|
3637
|
+
return fail16(errMsg16(e));
|
|
3343
3638
|
}
|
|
3344
3639
|
}
|
|
3345
3640
|
);
|
|
@@ -3348,8 +3643,8 @@ ${JSON.stringify(slim)}
|
|
|
3348
3643
|
{
|
|
3349
3644
|
description: 'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',
|
|
3350
3645
|
inputSchema: {
|
|
3351
|
-
deviceId:
|
|
3352
|
-
key:
|
|
3646
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3647
|
+
key: z16.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3353
3648
|
}
|
|
3354
3649
|
},
|
|
3355
3650
|
async ({ deviceId, key }) => {
|
|
@@ -3362,18 +3657,18 @@ ${JSON.stringify(slim)}
|
|
|
3362
3657
|
target
|
|
3363
3658
|
);
|
|
3364
3659
|
if (!detail) {
|
|
3365
|
-
return
|
|
3660
|
+
return fail16(
|
|
3366
3661
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3367
3662
|
);
|
|
3368
3663
|
}
|
|
3369
3664
|
const md = formatDetailMarkdown(detail);
|
|
3370
|
-
return
|
|
3665
|
+
return text14(`${md}
|
|
3371
3666
|
|
|
3372
3667
|
\`\`\`json
|
|
3373
3668
|
${JSON.stringify(detail)}
|
|
3374
3669
|
\`\`\``);
|
|
3375
3670
|
} catch (e) {
|
|
3376
|
-
return
|
|
3671
|
+
return fail16(errMsg16(e));
|
|
3377
3672
|
}
|
|
3378
3673
|
}
|
|
3379
3674
|
);
|
|
@@ -3382,27 +3677,27 @@ ${JSON.stringify(detail)}
|
|
|
3382
3677
|
{
|
|
3383
3678
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3384
3679
|
inputSchema: {
|
|
3385
|
-
deviceId:
|
|
3680
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3386
3681
|
}
|
|
3387
3682
|
},
|
|
3388
3683
|
async ({ deviceId }) => {
|
|
3389
3684
|
try {
|
|
3390
3685
|
const target = sync.requireProDevice(deviceId);
|
|
3391
3686
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3392
|
-
return
|
|
3687
|
+
return text14("Cleared js-top stats.");
|
|
3393
3688
|
} catch (e) {
|
|
3394
|
-
return
|
|
3689
|
+
return fail16(errMsg16(e));
|
|
3395
3690
|
}
|
|
3396
3691
|
}
|
|
3397
3692
|
);
|
|
3398
3693
|
}
|
|
3399
3694
|
|
|
3400
3695
|
// src/tools/packs/images.ts
|
|
3401
|
-
import { z as
|
|
3696
|
+
import { z as z17 } from "zod";
|
|
3402
3697
|
var IMAGES_TOOL_ID = "images";
|
|
3403
|
-
var
|
|
3404
|
-
var
|
|
3405
|
-
var
|
|
3698
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3699
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3700
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3406
3701
|
function uriTail(uri) {
|
|
3407
3702
|
if (!uri) return "(no source)";
|
|
3408
3703
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -3490,9 +3785,9 @@ function registerImagesTools(server, sync) {
|
|
|
3490
3785
|
{
|
|
3491
3786
|
description: "Live image registry from the app: every RN <Image> and expo-image load with cache verdict (memory/disk/network), load ms, decoded vs displayed size with an oversize/upscale verdict and wasted-KB estimate, error details, and any active simulation overrides. Image HTTP never hits the JS network stack, so this is the only visibility into image loading. Requires @buoy-gg/images on the device.",
|
|
3492
3787
|
inputSchema: {
|
|
3493
|
-
deviceId:
|
|
3494
|
-
limit:
|
|
3495
|
-
status:
|
|
3788
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3789
|
+
limit: z17.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
3790
|
+
status: z17.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3496
3791
|
}
|
|
3497
3792
|
},
|
|
3498
3793
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3505,7 +3800,7 @@ function registerImagesTools(server, sync) {
|
|
|
3505
3800
|
{ limit: capped, status },
|
|
3506
3801
|
target
|
|
3507
3802
|
);
|
|
3508
|
-
if (!list) return
|
|
3803
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3509
3804
|
const md = formatListMarkdown(list, capped);
|
|
3510
3805
|
const slim = {
|
|
3511
3806
|
stats: list.stats,
|
|
@@ -3532,13 +3827,13 @@ function registerImagesTools(server, sync) {
|
|
|
3532
3827
|
layoutShifts: r.layoutShifts
|
|
3533
3828
|
}))
|
|
3534
3829
|
};
|
|
3535
|
-
return
|
|
3830
|
+
return text15(`${md}
|
|
3536
3831
|
|
|
3537
3832
|
\`\`\`json
|
|
3538
3833
|
${JSON.stringify(slim)}
|
|
3539
3834
|
\`\`\``);
|
|
3540
3835
|
} catch (e) {
|
|
3541
|
-
return
|
|
3836
|
+
return fail17(errMsg17(e));
|
|
3542
3837
|
}
|
|
3543
3838
|
}
|
|
3544
3839
|
);
|
|
@@ -3547,9 +3842,9 @@ ${JSON.stringify(slim)}
|
|
|
3547
3842
|
{
|
|
3548
3843
|
description: "Act on one image from get_images (by id): hard_reload (bypass caches + refetch), retry, evict_disk (expo disk entry), flash (red border to locate it on screen), prove_savings (re-encode at displayed size as WebP ON DEVICE and return real byte savings + a previewable file:// uri), or simulation overrides \u2014 force_error (instant offline-safe failure), force_loading (request hangs forever), force_blank, swap_url (requires url), restore (remove override). Returns the record's state after the action so the effect is visible.",
|
|
3549
3844
|
inputSchema: {
|
|
3550
|
-
deviceId:
|
|
3551
|
-
id:
|
|
3552
|
-
action:
|
|
3845
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3846
|
+
id: z17.number().describe("Record id from get_images."),
|
|
3847
|
+
action: z17.enum([
|
|
3553
3848
|
"hard_reload",
|
|
3554
3849
|
"retry",
|
|
3555
3850
|
"evict_disk",
|
|
@@ -3561,7 +3856,7 @@ ${JSON.stringify(slim)}
|
|
|
3561
3856
|
"swap_url",
|
|
3562
3857
|
"restore"
|
|
3563
3858
|
]).describe("What to do."),
|
|
3564
|
-
url:
|
|
3859
|
+
url: z17.string().optional().describe("Replacement URL (swap_url only).")
|
|
3565
3860
|
}
|
|
3566
3861
|
},
|
|
3567
3862
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3569,8 +3864,8 @@ ${JSON.stringify(slim)}
|
|
|
3569
3864
|
const target = sync.requireProDevice(deviceId);
|
|
3570
3865
|
if (action === "prove_savings") {
|
|
3571
3866
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3572
|
-
if (!savings) return
|
|
3573
|
-
if (!savings.ok) return
|
|
3867
|
+
if (!savings) return fail17("Device returned nothing for proveSavings.");
|
|
3868
|
+
if (!savings.ok) return fail17(savings.message);
|
|
3574
3869
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3575
3870
|
if (savings.originalBytes != null)
|
|
3576
3871
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3582,11 +3877,11 @@ ${JSON.stringify(slim)}
|
|
|
3582
3877
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3583
3878
|
if (savings.optimizedUri)
|
|
3584
3879
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3585
|
-
return
|
|
3880
|
+
return text15(parts.join("\n"));
|
|
3586
3881
|
}
|
|
3587
3882
|
const entry = ACTION_MAP[action];
|
|
3588
|
-
if (!entry) return
|
|
3589
|
-
if (action === "swap_url" && !url) return
|
|
3883
|
+
if (!entry) return fail17(`Unknown action: ${action}`);
|
|
3884
|
+
if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
|
|
3590
3885
|
const result = await sync.callAction(
|
|
3591
3886
|
IMAGES_TOOL_ID,
|
|
3592
3887
|
entry.action,
|
|
@@ -3606,9 +3901,9 @@ ${JSON.stringify(slim)}
|
|
|
3606
3901
|
);
|
|
3607
3902
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3608
3903
|
}
|
|
3609
|
-
return
|
|
3904
|
+
return text15(lines.join("\n"));
|
|
3610
3905
|
} catch (e) {
|
|
3611
|
-
return
|
|
3906
|
+
return fail17(errMsg17(e));
|
|
3612
3907
|
}
|
|
3613
3908
|
}
|
|
3614
3909
|
);
|
|
@@ -3617,12 +3912,12 @@ ${JSON.stringify(slim)}
|
|
|
3617
3912
|
{
|
|
3618
3913
|
description: "App-wide image simulation modes: network 'offline' (every NETWORK image load fails immediately, both libs \u2014 bundled assets still load; reload/remount on-screen images to see it), 'cold' (every image bypasses caches \u2014 first-launch behavior, nothing cleared), 'normal' to reset. blankImages renders every image without a source. massAction applies per-image simulations to everything mounted. Also clears the registry or expo caches when asked.",
|
|
3619
3914
|
inputSchema: {
|
|
3620
|
-
deviceId:
|
|
3621
|
-
network:
|
|
3622
|
-
blankImages:
|
|
3623
|
-
massAction:
|
|
3624
|
-
clearRegistry:
|
|
3625
|
-
clearExpoCaches:
|
|
3915
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3916
|
+
network: z17.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3917
|
+
blankImages: z17.boolean().optional().describe("Render all images without a source."),
|
|
3918
|
+
massAction: z17.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
3919
|
+
clearRegistry: z17.boolean().optional().describe("Clear the captured image log."),
|
|
3920
|
+
clearExpoCaches: z17.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3626
3921
|
}
|
|
3627
3922
|
},
|
|
3628
3923
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3649,17 +3944,17 @@ ${JSON.stringify(slim)}
|
|
|
3649
3944
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3650
3945
|
done.push(r?.message ?? "expo caches cleared");
|
|
3651
3946
|
}
|
|
3652
|
-
if (done.length === 0) return
|
|
3653
|
-
return
|
|
3947
|
+
if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3948
|
+
return text15(`Applied: ${done.join(" \xB7 ")}`);
|
|
3654
3949
|
} catch (e) {
|
|
3655
|
-
return
|
|
3950
|
+
return fail17(errMsg17(e));
|
|
3656
3951
|
}
|
|
3657
3952
|
}
|
|
3658
3953
|
);
|
|
3659
3954
|
}
|
|
3660
3955
|
|
|
3661
3956
|
// src/tools/packs/assets.ts
|
|
3662
|
-
import { z as
|
|
3957
|
+
import { z as z18 } from "zod";
|
|
3663
3958
|
|
|
3664
3959
|
// src/tools/packs/assetsGraphNote.ts
|
|
3665
3960
|
function graphUnavailableNote(ss) {
|
|
@@ -3674,9 +3969,9 @@ function graphUnavailableNote(ss) {
|
|
|
3674
3969
|
|
|
3675
3970
|
// src/tools/packs/assets.ts
|
|
3676
3971
|
var ASSETS_TOOL_ID = "assets";
|
|
3677
|
-
var
|
|
3678
|
-
var
|
|
3679
|
-
var
|
|
3972
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3973
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3974
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3680
3975
|
function kb2(bytes) {
|
|
3681
3976
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3682
3977
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -3764,10 +4059,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3764
4059
|
{
|
|
3765
4060
|
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.",
|
|
3766
4061
|
inputSchema: {
|
|
3767
|
-
deviceId:
|
|
3768
|
-
limit:
|
|
3769
|
-
kind:
|
|
3770
|
-
unusedOnly:
|
|
4062
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4063
|
+
limit: z18.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4064
|
+
kind: z18.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4065
|
+
unusedOnly: z18.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3771
4066
|
}
|
|
3772
4067
|
},
|
|
3773
4068
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3780,7 +4075,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3780
4075
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3781
4076
|
target
|
|
3782
4077
|
);
|
|
3783
|
-
if (!list) return
|
|
4078
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3784
4079
|
if (unusedOnly) {
|
|
3785
4080
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3786
4081
|
}
|
|
@@ -3804,13 +4099,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3804
4099
|
sizeSource: r.sizeSource
|
|
3805
4100
|
}))
|
|
3806
4101
|
};
|
|
3807
|
-
return
|
|
4102
|
+
return text16(`${md}
|
|
3808
4103
|
|
|
3809
4104
|
\`\`\`json
|
|
3810
4105
|
${JSON.stringify(slim)}
|
|
3811
4106
|
\`\`\``);
|
|
3812
4107
|
} catch (e) {
|
|
3813
|
-
return
|
|
4108
|
+
return fail18(errMsg18(e));
|
|
3814
4109
|
}
|
|
3815
4110
|
}
|
|
3816
4111
|
);
|
|
@@ -3819,9 +4114,9 @@ ${JSON.stringify(slim)}
|
|
|
3819
4114
|
{
|
|
3820
4115
|
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).",
|
|
3821
4116
|
inputSchema: {
|
|
3822
|
-
deviceId:
|
|
3823
|
-
action:
|
|
3824
|
-
id:
|
|
4117
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4118
|
+
action: z18.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4119
|
+
id: z18.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3825
4120
|
}
|
|
3826
4121
|
},
|
|
3827
4122
|
async ({ deviceId, action, id }) => {
|
|
@@ -3831,19 +4126,19 @@ ${JSON.stringify(slim)}
|
|
|
3831
4126
|
case "rescan": {
|
|
3832
4127
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3833
4128
|
const ss = result?.scanStatus;
|
|
3834
|
-
if (!ss) return
|
|
3835
|
-
return
|
|
4129
|
+
if (!ss) return fail18("Device returned nothing for rescan.");
|
|
4130
|
+
return text16(
|
|
3836
4131
|
`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" : "")
|
|
3837
4132
|
);
|
|
3838
4133
|
}
|
|
3839
4134
|
case "measure_sizes": {
|
|
3840
4135
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3841
|
-
return
|
|
4136
|
+
return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3842
4137
|
}
|
|
3843
4138
|
case "detail": {
|
|
3844
|
-
if (id == null) return
|
|
4139
|
+
if (id == null) return fail18("detail requires the `id` param (from get_assets).");
|
|
3845
4140
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3846
|
-
if (!detail) return
|
|
4141
|
+
if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3847
4142
|
const lines = [
|
|
3848
4143
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3849
4144
|
"",
|
|
@@ -3861,32 +4156,32 @@ ${JSON.stringify(slim)}
|
|
|
3861
4156
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3862
4157
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3863
4158
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3864
|
-
return
|
|
4159
|
+
return text16(lines.join("\n"));
|
|
3865
4160
|
}
|
|
3866
4161
|
case "save_baseline": {
|
|
3867
4162
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3868
|
-
return
|
|
4163
|
+
return text16(
|
|
3869
4164
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3870
4165
|
);
|
|
3871
4166
|
}
|
|
3872
4167
|
case "clear_baseline": {
|
|
3873
4168
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3874
|
-
return
|
|
4169
|
+
return text16("Baseline cleared.");
|
|
3875
4170
|
}
|
|
3876
4171
|
case "clear_records": {
|
|
3877
4172
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3878
|
-
return
|
|
4173
|
+
return text16(result?.message ?? "Inventory cleared.");
|
|
3879
4174
|
}
|
|
3880
4175
|
}
|
|
3881
4176
|
} catch (e) {
|
|
3882
|
-
return
|
|
4177
|
+
return fail18(errMsg18(e));
|
|
3883
4178
|
}
|
|
3884
4179
|
}
|
|
3885
4180
|
);
|
|
3886
4181
|
}
|
|
3887
4182
|
|
|
3888
4183
|
// src/tools/packs/reload.ts
|
|
3889
|
-
import { z as
|
|
4184
|
+
import { z as z19 } from "zod";
|
|
3890
4185
|
|
|
3891
4186
|
// src/capture/devServerReload.ts
|
|
3892
4187
|
import { WebSocket } from "ws";
|
|
@@ -3949,9 +4244,9 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
3949
4244
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3950
4245
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3951
4246
|
var PROBE_INTERVAL_MS = 400;
|
|
3952
|
-
var
|
|
3953
|
-
var
|
|
3954
|
-
var
|
|
4247
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4248
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4249
|
+
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
3955
4250
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3956
4251
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3957
4252
|
try {
|
|
@@ -3990,12 +4285,12 @@ function registerReloadTools(server, sync) {
|
|
|
3990
4285
|
{
|
|
3991
4286
|
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, to re-run app startup, or to RECOVER A CRASHED APP: after a fatal error the other tools stop answering, and reloading is how you get them back (it falls through to the dev server when the app is too broken to reload itself). 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 \u2014 read anything you need from get_console/get_storage BEFORE reloading. 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.",
|
|
3992
4287
|
inputSchema: {
|
|
3993
|
-
strategy:
|
|
4288
|
+
strategy: z19.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3994
4289
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3995
4290
|
),
|
|
3996
|
-
wait:
|
|
3997
|
-
timeoutMs:
|
|
3998
|
-
deviceId:
|
|
4291
|
+
wait: z19.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4292
|
+
timeoutMs: z19.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4293
|
+
deviceId: z19.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3999
4294
|
}
|
|
4000
4295
|
},
|
|
4001
4296
|
async (args) => {
|
|
@@ -4037,32 +4332,32 @@ function registerReloadTools(server, sync) {
|
|
|
4037
4332
|
}
|
|
4038
4333
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4039
4334
|
if (args.wait === false) {
|
|
4040
|
-
return
|
|
4335
|
+
return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4041
4336
|
}
|
|
4042
4337
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4043
4338
|
if (elapsed == null) {
|
|
4044
|
-
return
|
|
4339
|
+
return fail19(
|
|
4045
4340
|
`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.`
|
|
4046
4341
|
);
|
|
4047
4342
|
}
|
|
4048
|
-
return
|
|
4343
|
+
return text17(
|
|
4049
4344
|
`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.`
|
|
4050
4345
|
);
|
|
4051
4346
|
} catch (e) {
|
|
4052
|
-
return
|
|
4347
|
+
return fail19(errMsg19(e));
|
|
4053
4348
|
}
|
|
4054
4349
|
}
|
|
4055
4350
|
);
|
|
4056
4351
|
}
|
|
4057
4352
|
|
|
4058
4353
|
// src/tools/packs/triage.ts
|
|
4059
|
-
import { z as
|
|
4060
|
-
var
|
|
4061
|
-
var
|
|
4354
|
+
import { z as z20 } from "zod";
|
|
4355
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4356
|
+
var fail20 = (message) => ({
|
|
4062
4357
|
isError: true,
|
|
4063
4358
|
content: [{ type: "text", text: message }]
|
|
4064
4359
|
});
|
|
4065
|
-
var
|
|
4360
|
+
var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
|
|
4066
4361
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4067
4362
|
var MAX_STORAGE_KEYS = 20;
|
|
4068
4363
|
var MAX_STORAGE_VALUE = 400;
|
|
@@ -4099,7 +4394,7 @@ async function section(title, run) {
|
|
|
4099
4394
|
return body ? `${title}
|
|
4100
4395
|
${body}` : null;
|
|
4101
4396
|
} catch (e) {
|
|
4102
|
-
const msg =
|
|
4397
|
+
const msg = errMsg20(e);
|
|
4103
4398
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4104
4399
|
return `${title}
|
|
4105
4400
|
(no answer \u2014 see the note above)`;
|
|
@@ -4114,7 +4409,7 @@ function registerTriageTools(server, sync) {
|
|
|
4114
4409
|
{
|
|
4115
4410
|
description: "START HERE when debugging a running app. One call = the app's current runtime state: connected device, recent console errors (including [FATAL] crashes), recent network requests with statuses, persisted storage values, and what's on screen right now. Use it as the first step for ANY 'why is the app doing X' question \u2014 it is far cheaper than calling get_console + get_network_requests + get_storage + describe_screen separately, and it tells you which of those to drill into next. If the app has crashed it says so and shows the crash. Requires Buoy Pro.",
|
|
4116
4411
|
inputSchema: {
|
|
4117
|
-
deviceId:
|
|
4412
|
+
deviceId: z20.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4118
4413
|
}
|
|
4119
4414
|
},
|
|
4120
4415
|
async (args) => {
|
|
@@ -4221,7 +4516,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4221
4516
|
|
|
4222
4517
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4223
4518
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4224
|
-
return
|
|
4519
|
+
return text18(
|
|
4225
4520
|
`${head}${banner}
|
|
4226
4521
|
|
|
4227
4522
|
${body}
|
|
@@ -4229,22 +4524,22 @@ ${body}
|
|
|
4229
4524
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4230
4525
|
);
|
|
4231
4526
|
} catch (e) {
|
|
4232
|
-
return
|
|
4527
|
+
return fail20(errMsg20(e));
|
|
4233
4528
|
}
|
|
4234
4529
|
}
|
|
4235
4530
|
);
|
|
4236
4531
|
}
|
|
4237
4532
|
|
|
4238
4533
|
// src/tools/packs/flow.ts
|
|
4239
|
-
import { z as
|
|
4534
|
+
import { z as z21 } from "zod";
|
|
4240
4535
|
var UI_TOOL_ID = "highlight-updates";
|
|
4241
4536
|
var APP_TOOL_ID2 = "app";
|
|
4242
|
-
var
|
|
4243
|
-
var
|
|
4537
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4538
|
+
var fail21 = (message) => ({
|
|
4244
4539
|
isError: true,
|
|
4245
4540
|
content: [{ type: "text", text: message }]
|
|
4246
4541
|
});
|
|
4247
|
-
var
|
|
4542
|
+
var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
|
|
4248
4543
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4249
4544
|
var POLL_MS = 250;
|
|
4250
4545
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4261,19 +4556,19 @@ function formatElement2(e) {
|
|
|
4261
4556
|
const display = e.text ?? e.label;
|
|
4262
4557
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4263
4558
|
}
|
|
4264
|
-
var stepSchema =
|
|
4265
|
-
tap:
|
|
4266
|
-
longPress:
|
|
4267
|
-
type:
|
|
4268
|
-
into:
|
|
4269
|
-
set:
|
|
4270
|
-
reload:
|
|
4271
|
-
waitMs:
|
|
4272
|
-
waitFor:
|
|
4273
|
-
waitGone:
|
|
4274
|
-
expect:
|
|
4275
|
-
expectGone:
|
|
4276
|
-
timeoutMs:
|
|
4559
|
+
var stepSchema = z21.object({
|
|
4560
|
+
tap: z21.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4561
|
+
longPress: z21.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4562
|
+
type: z21.string().optional().describe("Text to type (needs `into`)."),
|
|
4563
|
+
into: z21.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4564
|
+
set: z21.union([z21.number(), z21.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4565
|
+
reload: z21.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4566
|
+
waitMs: z21.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4567
|
+
waitFor: z21.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4568
|
+
waitGone: z21.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4569
|
+
expect: z21.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
4570
|
+
expectGone: z21.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
4571
|
+
timeoutMs: z21.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4277
4572
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4278
4573
|
function registerFlowTools(server, sync) {
|
|
4279
4574
|
server.registerTool(
|
|
@@ -4281,10 +4576,10 @@ function registerFlowTools(server, sync) {
|
|
|
4281
4576
|
{
|
|
4282
4577
|
description: "Run a scripted sequence of UI interactions in ONE call and report what happened: per-step results, the final screen, plus the network requests and console errors the flow produced. Use this instead of alternating describe_screen/tap_element \u2014 reproducing a bug ('open cart, checkout, submit') or verifying a fix after an edit is one call, not a dozen. Steps run in order and stop at the first failure, returning the screen at that point so you can see WHERE it broke. Each step can tap, type, set a toggle/slider, reload, wait for an element, and assert. Requires Buoy Pro.",
|
|
4283
4578
|
inputSchema: {
|
|
4284
|
-
steps:
|
|
4285
|
-
include:
|
|
4286
|
-
continueOnFailure:
|
|
4287
|
-
deviceId:
|
|
4579
|
+
steps: z21.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4580
|
+
include: z21.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final screen + network/console produced by the flow; 'screen' = final screen only; 'none' = step results only."),
|
|
4581
|
+
continueOnFailure: z21.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4582
|
+
deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4288
4583
|
}
|
|
4289
4584
|
},
|
|
4290
4585
|
async (args) => {
|
|
@@ -4456,7 +4751,7 @@ function registerFlowTools(server, sync) {
|
|
|
4456
4751
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
4457
4752
|
}
|
|
4458
4753
|
} catch (e) {
|
|
4459
|
-
stepErrors.push(
|
|
4754
|
+
stepErrors.push(errMsg21(e));
|
|
4460
4755
|
}
|
|
4461
4756
|
if (stepErrors.length) {
|
|
4462
4757
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -4479,7 +4774,7 @@ function registerFlowTools(server, sync) {
|
|
|
4479
4774
|
${shown.map(formatElement2).join("\n")}`
|
|
4480
4775
|
);
|
|
4481
4776
|
} catch (e) {
|
|
4482
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
4777
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
|
|
4483
4778
|
}
|
|
4484
4779
|
}
|
|
4485
4780
|
if (include === "all") {
|
|
@@ -4508,9 +4803,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4508
4803
|
}
|
|
4509
4804
|
}
|
|
4510
4805
|
}
|
|
4511
|
-
return
|
|
4806
|
+
return text19(parts.join("\n\n"));
|
|
4512
4807
|
} catch (e) {
|
|
4513
|
-
return
|
|
4808
|
+
return fail21(errMsg21(e));
|
|
4514
4809
|
}
|
|
4515
4810
|
}
|
|
4516
4811
|
);
|
|
@@ -4531,25 +4826,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4531
4826
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4532
4827
|
|
|
4533
4828
|
// src/tools/packs/renders.ts
|
|
4534
|
-
import { z as
|
|
4829
|
+
import { z as z22 } from "zod";
|
|
4535
4830
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4536
|
-
var
|
|
4537
|
-
var
|
|
4831
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4832
|
+
var fail22 = (message) => ({
|
|
4538
4833
|
isError: true,
|
|
4539
4834
|
content: [{ type: "text", text: message }]
|
|
4540
4835
|
});
|
|
4541
|
-
var
|
|
4836
|
+
var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
|
|
4542
4837
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4543
4838
|
var DEFAULT_IDLE_MS = 3e3;
|
|
4544
4839
|
var SETTLE_MS = 400;
|
|
4545
4840
|
var DEFAULT_TOP = 12;
|
|
4546
4841
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4547
|
-
var stepSchema2 =
|
|
4548
|
-
tap:
|
|
4549
|
-
type:
|
|
4550
|
-
into:
|
|
4551
|
-
waitMs:
|
|
4552
|
-
repeat:
|
|
4842
|
+
var stepSchema2 = z22.object({
|
|
4843
|
+
tap: z22.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4844
|
+
type: z22.string().optional().describe("Text to type (needs `into`)."),
|
|
4845
|
+
into: z22.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4846
|
+
waitMs: z22.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
4847
|
+
repeat: z22.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4553
4848
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4554
4849
|
function selector2(needle) {
|
|
4555
4850
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4642,14 +4937,14 @@ function registerRenderTools(server, sync) {
|
|
|
4642
4937
|
{
|
|
4643
4938
|
description: "Find out WHAT re-rendered on the current screen and WHY, in one call \u2014 no test route, no scaffolding. THE tool for 'this page is slow/laggy/janky', 'typing lags', 'the whole page re-renders when I tap one button'. Give it `steps` to measure a specific interaction (tap a button, type in a field) \u2014 measuring an idle window instead tells you nothing about what taps cost, which is how re-render fixes usually go wrong. Returns per-component render COST in ms (sorted by it \u2014 a component rendering 40x for 0.4ms is noise, one rendering 4x for 38ms is the answer), the CAUSE of each (parent cascade / own state / changed props), which parent dragged it along, and which props changed. `wasted` counts renders that produced nothing new \u2014 that's the removable part. AFTER YOU EDIT, call it again with the same steps and compareToPrevious:true to prove the fix landed and didn't move the cost somewhere else. (For choosing between several implementations of a screen, use run_benchmark_batch instead \u2014 that ranks variants; this explains one screen.) Requires Buoy Pro and @buoy-gg/highlight-updates.",
|
|
4644
4939
|
inputSchema: {
|
|
4645
|
-
steps:
|
|
4940
|
+
steps: z22.array(stepSchema2).max(20).optional().describe(
|
|
4646
4941
|
"The interaction to measure, e.g. [{tap:'favorite-deal-2', repeat:5}] or [{type:'bench', into:'deals-search'}]. Omit to sample the screen idle \u2014 only useful for churn that happens with nobody touching it."
|
|
4647
4942
|
),
|
|
4648
|
-
idleMs:
|
|
4649
|
-
label:
|
|
4650
|
-
compareToPrevious:
|
|
4651
|
-
top:
|
|
4652
|
-
deviceId:
|
|
4943
|
+
idleMs: z22.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
4944
|
+
label: z22.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
4945
|
+
compareToPrevious: z22.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
4946
|
+
top: z22.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
4947
|
+
deviceId: z22.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4653
4948
|
}
|
|
4654
4949
|
},
|
|
4655
4950
|
async (args) => {
|
|
@@ -4658,18 +4953,18 @@ function registerRenderTools(server, sync) {
|
|
|
4658
4953
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4659
4954
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4660
4955
|
if (!uiTool) {
|
|
4661
|
-
return
|
|
4956
|
+
return fail22(
|
|
4662
4957
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4663
4958
|
);
|
|
4664
4959
|
}
|
|
4665
4960
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4666
|
-
return
|
|
4961
|
+
return fail22(
|
|
4667
4962
|
"This app's @buoy-gg/highlight-updates is too old to measure renders (needs the version with beginMeasurement). Upgrade it, then try again. Meanwhile run_benchmark_batch with captureRenders can still give counts if you have a test route."
|
|
4668
4963
|
);
|
|
4669
4964
|
}
|
|
4670
4965
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4671
4966
|
if (began && began.ok === false) {
|
|
4672
|
-
return
|
|
4967
|
+
return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4673
4968
|
}
|
|
4674
4969
|
await delay5(SETTLE_MS);
|
|
4675
4970
|
const startedAt = Date.now();
|
|
@@ -4714,7 +5009,7 @@ function registerRenderTools(server, sync) {
|
|
|
4714
5009
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
4715
5010
|
const summary = ended?.summary;
|
|
4716
5011
|
if (!summary) {
|
|
4717
|
-
return
|
|
5012
|
+
return fail22(
|
|
4718
5013
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
4719
5014
|
);
|
|
4720
5015
|
}
|
|
@@ -4739,9 +5034,9 @@ function registerRenderTools(server, sync) {
|
|
|
4739
5034
|
} else if (!args.compareToPrevious) {
|
|
4740
5035
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
4741
5036
|
}
|
|
4742
|
-
return
|
|
5037
|
+
return text20(parts.join("\n"));
|
|
4743
5038
|
} catch (e) {
|
|
4744
|
-
return
|
|
5039
|
+
return fail22(errMsg22(e));
|
|
4745
5040
|
}
|
|
4746
5041
|
}
|
|
4747
5042
|
);
|
|
@@ -4962,6 +5257,7 @@ require the connected app to have an active Buoy Pro license. If they error with
|
|
|
4962
5257
|
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
4963
5258
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
4964
5259
|
"measure_renders",
|
|
5260
|
+
"scenario_action",
|
|
4965
5261
|
"run_flow",
|
|
4966
5262
|
"run_benchmark_batch",
|
|
4967
5263
|
"get_triage",
|
|
@@ -5012,6 +5308,7 @@ function createMcpServer(sync) {
|
|
|
5012
5308
|
registerRoutesTools(server, sync);
|
|
5013
5309
|
registerConsoleTools(server, sync);
|
|
5014
5310
|
registerTimeMachineTools(server, sync);
|
|
5311
|
+
registerScenariosTools(server, sync);
|
|
5015
5312
|
registerScreenshotTools(server, sync);
|
|
5016
5313
|
registerInteractTools(server, sync);
|
|
5017
5314
|
registerJsThreadTools(server, sync);
|
|
@@ -5021,7 +5318,7 @@ function createMcpServer(sync) {
|
|
|
5021
5318
|
return server;
|
|
5022
5319
|
}
|
|
5023
5320
|
var deviceIdField = {
|
|
5024
|
-
deviceId:
|
|
5321
|
+
deviceId: z23.string().optional().describe(
|
|
5025
5322
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5026
5323
|
)
|
|
5027
5324
|
};
|