@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
package/dist/index.js
CHANGED
|
@@ -675,7 +675,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
675
675
|
|
|
676
676
|
// src/server.ts
|
|
677
677
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
678
|
-
var
|
|
678
|
+
var import_zod23 = require("zod");
|
|
679
679
|
|
|
680
680
|
// src/tools/generic.ts
|
|
681
681
|
var import_zod = require("zod");
|
|
@@ -686,7 +686,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
686
686
|
var import_node_path = __toESM(require("path"));
|
|
687
687
|
|
|
688
688
|
// src/version.ts
|
|
689
|
-
var VERSION = "7.0.
|
|
689
|
+
var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
|
|
690
690
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
691
691
|
|
|
692
692
|
// src/update/checkForUpdate.ts
|
|
@@ -2946,8 +2946,303 @@ ${lines.join("\n")}`);
|
|
|
2946
2946
|
);
|
|
2947
2947
|
}
|
|
2948
2948
|
|
|
2949
|
-
// src/tools/packs/
|
|
2949
|
+
// src/tools/packs/scenarios.ts
|
|
2950
2950
|
var import_zod13 = require("zod");
|
|
2951
|
+
var SCENARIOS_TOOL_ID = "scenarios";
|
|
2952
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2953
|
+
var fail13 = (message) => ({
|
|
2954
|
+
isError: true,
|
|
2955
|
+
content: [{ type: "text", text: message }]
|
|
2956
|
+
});
|
|
2957
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2958
|
+
function varSpec(v) {
|
|
2959
|
+
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
2960
|
+
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
2961
|
+
return `${v.key}: ${v.type}${opts}${def}`;
|
|
2962
|
+
}
|
|
2963
|
+
function summarize(s) {
|
|
2964
|
+
const vars = s.vars?.length ? `
|
|
2965
|
+
variables: ${s.vars.map(varSpec).join("; ")}` : "";
|
|
2966
|
+
const steps = s.stepLines?.length ? `
|
|
2967
|
+
${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
|
|
2968
|
+
const meta = [
|
|
2969
|
+
`v${s.version}`,
|
|
2970
|
+
s.author,
|
|
2971
|
+
s.usage?.runs ? `${s.usage.runs} run(s)` : null,
|
|
2972
|
+
s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
|
|
2973
|
+
s.unsavedToRepo ? "unsaved to repo" : null
|
|
2974
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2975
|
+
const outcome = s.expectedOutcome ? `
|
|
2976
|
+
expected: ${s.expectedOutcome}` : "";
|
|
2977
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
|
|
2978
|
+
}
|
|
2979
|
+
function renderSection(title, list, folders) {
|
|
2980
|
+
if (!list.length) return `**${title} (0):**
|
|
2981
|
+
_none_`;
|
|
2982
|
+
if (!folders.length) {
|
|
2983
|
+
return `**${title} (${list.length}):**
|
|
2984
|
+
${list.map(summarize).join("\n")}`;
|
|
2985
|
+
}
|
|
2986
|
+
const parts = [`**${title} (${list.length}):**`];
|
|
2987
|
+
for (const folder of folders) {
|
|
2988
|
+
const inIt = list.filter(
|
|
2989
|
+
(s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
|
|
2990
|
+
);
|
|
2991
|
+
if (!inIt.length) continue;
|
|
2992
|
+
parts.push(`_${folder} (${inIt.length})_
|
|
2993
|
+
${inIt.map(summarize).join("\n")}`);
|
|
2994
|
+
}
|
|
2995
|
+
const ungrouped = list.filter((s) => !s.folder);
|
|
2996
|
+
if (ungrouped.length) {
|
|
2997
|
+
parts.push(
|
|
2998
|
+
`_Ungrouped (${ungrouped.length})_
|
|
2999
|
+
${ungrouped.map(summarize).join("\n")}`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
return parts.join("\n");
|
|
3003
|
+
}
|
|
3004
|
+
function formatActive(a) {
|
|
3005
|
+
const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
|
|
3006
|
+
return [
|
|
3007
|
+
`**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
|
|
3008
|
+
vars ? `variables: ${vars}` : null,
|
|
3009
|
+
`effects:
|
|
3010
|
+
${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
|
|
3011
|
+
].filter(Boolean).join("\n");
|
|
3012
|
+
}
|
|
3013
|
+
function registerScenariosTools(server, sync) {
|
|
3014
|
+
server.registerTool(
|
|
3015
|
+
"get_scenarios",
|
|
3016
|
+
{
|
|
3017
|
+
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.",
|
|
3018
|
+
inputSchema: {
|
|
3019
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3020
|
+
}
|
|
3021
|
+
},
|
|
3022
|
+
async (args) => {
|
|
3023
|
+
try {
|
|
3024
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3025
|
+
const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
|
|
3026
|
+
const p = payload ?? {};
|
|
3027
|
+
const code = p.code ?? [];
|
|
3028
|
+
const device = p.device ?? [];
|
|
3029
|
+
const drafts = p.drafts ?? [];
|
|
3030
|
+
const folders = p.folders ?? [];
|
|
3031
|
+
const parts = [];
|
|
3032
|
+
parts.push(
|
|
3033
|
+
p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
|
|
3034
|
+
${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
|
|
3035
|
+
);
|
|
3036
|
+
if (folders.length) {
|
|
3037
|
+
parts.push(
|
|
3038
|
+
`**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.`
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
parts.push(renderSection("Shipped with this app", code, folders));
|
|
3042
|
+
parts.push(renderSection("Saved on this device", device, folders));
|
|
3043
|
+
if (drafts.length) {
|
|
3044
|
+
parts.push(
|
|
3045
|
+
`**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
|
|
3046
|
+
${drafts.map(summarize).join("\n")}`
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
return text12(parts.join("\n\n"));
|
|
3050
|
+
} catch (e) {
|
|
3051
|
+
return fail13(errMsg13(e));
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
);
|
|
3055
|
+
server.registerTool(
|
|
3056
|
+
"scenario_action",
|
|
3057
|
+
{
|
|
3058
|
+
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.`,
|
|
3059
|
+
inputSchema: {
|
|
3060
|
+
action: import_zod13.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3061
|
+
folder: import_zod13.z.string().optional().describe(
|
|
3062
|
+
"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."
|
|
3063
|
+
),
|
|
3064
|
+
id: import_zod13.z.string().optional().describe(
|
|
3065
|
+
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3066
|
+
),
|
|
3067
|
+
params: import_zod13.z.record(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional().describe(
|
|
3068
|
+
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3069
|
+
),
|
|
3070
|
+
scenario: import_zod13.z.object({
|
|
3071
|
+
id: import_zod13.z.string().describe("Stable kebab-case id."),
|
|
3072
|
+
name: import_zod13.z.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3073
|
+
description: import_zod13.z.string().optional(),
|
|
3074
|
+
expectedOutcome: import_zod13.z.string().optional().describe(
|
|
3075
|
+
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3076
|
+
),
|
|
3077
|
+
author: import_zod13.z.string().optional(),
|
|
3078
|
+
authoredAgainst: import_zod13.z.string().optional().describe("App version this was written against."),
|
|
3079
|
+
folder: import_zod13.z.string().optional().describe(
|
|
3080
|
+
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3081
|
+
),
|
|
3082
|
+
tags: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
3083
|
+
vars: import_zod13.z.array(
|
|
3084
|
+
import_zod13.z.object({
|
|
3085
|
+
key: import_zod13.z.string(),
|
|
3086
|
+
label: import_zod13.z.string().optional(),
|
|
3087
|
+
type: import_zod13.z.enum(["string", "number", "boolean", "enum"]),
|
|
3088
|
+
options: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional(),
|
|
3089
|
+
default: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]).optional()
|
|
3090
|
+
})
|
|
3091
|
+
).optional().describe(
|
|
3092
|
+
"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."
|
|
3093
|
+
),
|
|
3094
|
+
steps: import_zod13.z.array(
|
|
3095
|
+
import_zod13.z.object({
|
|
3096
|
+
id: import_zod13.z.string().optional(),
|
|
3097
|
+
tool: import_zod13.z.string().describe(
|
|
3098
|
+
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3099
|
+
),
|
|
3100
|
+
action: import_zod13.z.string().describe(
|
|
3101
|
+
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3102
|
+
),
|
|
3103
|
+
params: import_zod13.z.unknown().optional(),
|
|
3104
|
+
label: import_zod13.z.string().optional()
|
|
3105
|
+
})
|
|
3106
|
+
).describe("Steps, in order."),
|
|
3107
|
+
undoSteps: import_zod13.z.array(
|
|
3108
|
+
import_zod13.z.object({
|
|
3109
|
+
id: import_zod13.z.string().optional(),
|
|
3110
|
+
tool: import_zod13.z.string(),
|
|
3111
|
+
action: import_zod13.z.string(),
|
|
3112
|
+
params: import_zod13.z.unknown().optional(),
|
|
3113
|
+
label: import_zod13.z.string().optional()
|
|
3114
|
+
})
|
|
3115
|
+
).optional().describe(
|
|
3116
|
+
"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."
|
|
3117
|
+
)
|
|
3118
|
+
}).optional().describe("The scenario to save (action 'save')."),
|
|
3119
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3120
|
+
}
|
|
3121
|
+
},
|
|
3122
|
+
async (args) => {
|
|
3123
|
+
try {
|
|
3124
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3125
|
+
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3126
|
+
switch (args.action) {
|
|
3127
|
+
case "preview": {
|
|
3128
|
+
if (!args.id) return fail13("preview requires `id` (from get_scenarios).");
|
|
3129
|
+
const r = await call("preview", {
|
|
3130
|
+
id: args.id,
|
|
3131
|
+
params: args.params ?? {}
|
|
3132
|
+
});
|
|
3133
|
+
const lines = [
|
|
3134
|
+
`**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
|
|
3135
|
+
`variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
|
|
3136
|
+
`**Would do:**
|
|
3137
|
+
${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
3138
|
+
];
|
|
3139
|
+
if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
|
|
3140
|
+
if (r.conflict) {
|
|
3141
|
+
lines.push(
|
|
3142
|
+
`\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.`
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3146
|
+
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3147
|
+
lines.push(
|
|
3148
|
+
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3149
|
+
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3150
|
+
);
|
|
3151
|
+
return text12(lines.join("\n\n"));
|
|
3152
|
+
}
|
|
3153
|
+
case "run": {
|
|
3154
|
+
if (!args.id) return fail13("run requires `id` (from get_scenarios).");
|
|
3155
|
+
const r = await call("run", {
|
|
3156
|
+
id: args.id,
|
|
3157
|
+
params: args.params ?? {}
|
|
3158
|
+
});
|
|
3159
|
+
if (r.preflightErrors?.length) {
|
|
3160
|
+
return fail13(
|
|
3161
|
+
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3162
|
+
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
const steps = (r.steps ?? []).map((s) => {
|
|
3166
|
+
const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
|
|
3167
|
+
const err = s.error ? `
|
|
3168
|
+
${s.error}` : "";
|
|
3169
|
+
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3170
|
+
}).join("\n");
|
|
3171
|
+
if (!r.ok) {
|
|
3172
|
+
return fail13(
|
|
3173
|
+
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3174
|
+
${steps}
|
|
3175
|
+
|
|
3176
|
+
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
return text12(
|
|
3180
|
+
[
|
|
3181
|
+
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3182
|
+
steps,
|
|
3183
|
+
`**Now bent:**
|
|
3184
|
+
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3185
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
|
|
3186
|
+
].join("\n\n")
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
case "deactivate": {
|
|
3190
|
+
const r = await call("deactivate", {});
|
|
3191
|
+
const rep = r.report;
|
|
3192
|
+
const lines = [
|
|
3193
|
+
`Removed ${rep.removedRules} override rule(s)`,
|
|
3194
|
+
rep.stoppedImpersonation ? "Ended impersonation" : null,
|
|
3195
|
+
rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
|
|
3196
|
+
].filter(Boolean);
|
|
3197
|
+
const warn = rep.unrestoredStorageKeys.length ? `
|
|
3198
|
+
|
|
3199
|
+
\u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
|
|
3200
|
+
const errs = rep.errors.length ? `
|
|
3201
|
+
|
|
3202
|
+
Errors: ${rep.errors.join("; ")}` : "";
|
|
3203
|
+
return text12(`**Deactivated.**
|
|
3204
|
+
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3205
|
+
}
|
|
3206
|
+
case "save": {
|
|
3207
|
+
if (!args.scenario) return fail13("save requires `scenario`.");
|
|
3208
|
+
const r = await call("save", { scenario: args.scenario });
|
|
3209
|
+
if (!r.ok) {
|
|
3210
|
+
return fail13(
|
|
3211
|
+
`Save rejected:
|
|
3212
|
+
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
return text12(
|
|
3216
|
+
`Saved \`${r.id}\` **as a draft**.
|
|
3217
|
+
|
|
3218
|
+
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.`
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
case "setFolder": {
|
|
3222
|
+
if (!args.id) return fail13("setFolder requires `id` (from get_scenarios).");
|
|
3223
|
+
const r = await call("setFolder", {
|
|
3224
|
+
id: args.id,
|
|
3225
|
+
folder: args.folder
|
|
3226
|
+
});
|
|
3227
|
+
return text12(
|
|
3228
|
+
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
case "delete": {
|
|
3232
|
+
if (!args.id) return fail13("delete requires `id`.");
|
|
3233
|
+
await call("delete", { id: args.id });
|
|
3234
|
+
return text12(`Deleted \`${args.id}\` from the device library.`);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
} catch (e) {
|
|
3238
|
+
return fail13(errMsg13(e));
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/tools/packs/screenshot.ts
|
|
3245
|
+
var import_zod14 = require("zod");
|
|
2951
3246
|
|
|
2952
3247
|
// src/capture/simctl.ts
|
|
2953
3248
|
var import_node_child_process = require("child_process");
|
|
@@ -3061,8 +3356,8 @@ function frameToRect(frame, screen) {
|
|
|
3061
3356
|
var TOOL_ID = "highlight-updates";
|
|
3062
3357
|
var MARGIN_PT = 12;
|
|
3063
3358
|
var CAPTURE_PADDING_PT = 6;
|
|
3064
|
-
var
|
|
3065
|
-
var
|
|
3359
|
+
var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3360
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
3066
3361
|
function reasonMessage(res, query) {
|
|
3067
3362
|
switch (res.reason) {
|
|
3068
3363
|
case "no-renders":
|
|
@@ -3092,10 +3387,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
3092
3387
|
{
|
|
3093
3388
|
description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. If highlight tracking hasn't seen a render yet, it falls back to a live fiber walk (same as describe_screen). iOS Simulator only, and the MCP server must run on the same Mac as the booted Simulator. Requires Buoy Pro.",
|
|
3094
3389
|
inputSchema: {
|
|
3095
|
-
query:
|
|
3096
|
-
nativeTag:
|
|
3097
|
-
scrollIntoView:
|
|
3098
|
-
deviceId:
|
|
3390
|
+
query: import_zod14.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3391
|
+
nativeTag: import_zod14.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3392
|
+
scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
3393
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3099
3394
|
}
|
|
3100
3395
|
},
|
|
3101
3396
|
async (args) => {
|
|
@@ -3137,11 +3432,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3137
3432
|
}
|
|
3138
3433
|
}
|
|
3139
3434
|
if (!located.matched || !located.rect || !located.screen) {
|
|
3140
|
-
return
|
|
3435
|
+
return fail14(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
3141
3436
|
}
|
|
3142
3437
|
const shot = await captureIosFramebuffer();
|
|
3143
3438
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
3144
|
-
return
|
|
3439
|
+
return fail14(shot.error ?? "Screenshot capture failed.");
|
|
3145
3440
|
}
|
|
3146
3441
|
const pxScale = shot.width / located.screen.width;
|
|
3147
3442
|
const r = located.rect;
|
|
@@ -3157,7 +3452,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
3157
3452
|
}
|
|
3158
3453
|
);
|
|
3159
3454
|
if (!crop.ok || !crop.buffer) {
|
|
3160
|
-
return
|
|
3455
|
+
return fail14(crop.error ?? "Crop failed.");
|
|
3161
3456
|
}
|
|
3162
3457
|
const label = located.componentName || "component";
|
|
3163
3458
|
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
@@ -3170,21 +3465,21 @@ function registerScreenshotTools(server, sync) {
|
|
|
3170
3465
|
]
|
|
3171
3466
|
};
|
|
3172
3467
|
} catch (e) {
|
|
3173
|
-
return
|
|
3468
|
+
return fail14(errMsg14(e));
|
|
3174
3469
|
}
|
|
3175
3470
|
}
|
|
3176
3471
|
);
|
|
3177
3472
|
}
|
|
3178
3473
|
|
|
3179
3474
|
// src/tools/packs/interact.ts
|
|
3180
|
-
var
|
|
3475
|
+
var import_zod15 = require("zod");
|
|
3181
3476
|
var TOOL_ID2 = "highlight-updates";
|
|
3182
|
-
var
|
|
3183
|
-
var
|
|
3477
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3478
|
+
var fail15 = (message) => ({
|
|
3184
3479
|
isError: true,
|
|
3185
3480
|
content: [{ type: "text", text: message }]
|
|
3186
3481
|
});
|
|
3187
|
-
var
|
|
3482
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3188
3483
|
function formatElement(e) {
|
|
3189
3484
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3190
3485
|
line += e.name;
|
|
@@ -3207,8 +3502,8 @@ function registerInteractTools(server, sync) {
|
|
|
3207
3502
|
{
|
|
3208
3503
|
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.",
|
|
3209
3504
|
inputSchema: {
|
|
3210
|
-
interactiveOnly:
|
|
3211
|
-
deviceId:
|
|
3505
|
+
interactiveOnly: import_zod15.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3506
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3212
3507
|
}
|
|
3213
3508
|
},
|
|
3214
3509
|
async (args) => {
|
|
@@ -3218,16 +3513,16 @@ function registerInteractTools(server, sync) {
|
|
|
3218
3513
|
const all = res?.elements ?? [];
|
|
3219
3514
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3220
3515
|
if (elements.length === 0) {
|
|
3221
|
-
return
|
|
3516
|
+
return text13(
|
|
3222
3517
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3223
3518
|
);
|
|
3224
3519
|
}
|
|
3225
3520
|
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 }).`;
|
|
3226
|
-
return
|
|
3521
|
+
return text13(`${header}
|
|
3227
3522
|
|
|
3228
3523
|
${elements.map(formatElement).join("\n")}`);
|
|
3229
3524
|
} catch (e) {
|
|
3230
|
-
return
|
|
3525
|
+
return fail15(errMsg15(e));
|
|
3231
3526
|
}
|
|
3232
3527
|
}
|
|
3233
3528
|
);
|
|
@@ -3236,20 +3531,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3236
3531
|
{
|
|
3237
3532
|
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.",
|
|
3238
3533
|
inputSchema: {
|
|
3239
|
-
nativeTag:
|
|
3240
|
-
testID:
|
|
3241
|
-
query:
|
|
3242
|
-
value:
|
|
3243
|
-
text:
|
|
3244
|
-
longPress:
|
|
3245
|
-
scrollIntoView:
|
|
3246
|
-
deviceId:
|
|
3534
|
+
nativeTag: import_zod15.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3535
|
+
testID: import_zod15.z.string().optional().describe("Exact testID of the element."),
|
|
3536
|
+
query: import_zod15.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3537
|
+
value: import_zod15.z.union([import_zod15.z.number(), import_zod15.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
3538
|
+
text: import_zod15.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3539
|
+
longPress: import_zod15.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3540
|
+
scrollIntoView: import_zod15.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3541
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3247
3542
|
}
|
|
3248
3543
|
},
|
|
3249
3544
|
async (args) => {
|
|
3250
3545
|
try {
|
|
3251
3546
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3252
|
-
return
|
|
3547
|
+
return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3253
3548
|
}
|
|
3254
3549
|
const target = sync.requireProDevice(args.deviceId);
|
|
3255
3550
|
const res = await sync.callAction(
|
|
@@ -3274,26 +3569,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3274
3569
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3275
3570
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3276
3571
|
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}`;
|
|
3277
|
-
return
|
|
3572
|
+
return text13(`${verb}${scrolledNote}.${flowTip}`);
|
|
3278
3573
|
}
|
|
3279
3574
|
let msg = res?.reason ?? "Tap failed.";
|
|
3280
3575
|
if (res?.candidates?.length) {
|
|
3281
3576
|
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");
|
|
3282
3577
|
}
|
|
3283
|
-
return
|
|
3578
|
+
return fail15(msg);
|
|
3284
3579
|
} catch (e) {
|
|
3285
|
-
return
|
|
3580
|
+
return fail15(errMsg15(e));
|
|
3286
3581
|
}
|
|
3287
3582
|
}
|
|
3288
3583
|
);
|
|
3289
3584
|
}
|
|
3290
3585
|
|
|
3291
3586
|
// src/tools/packs/jsThread.ts
|
|
3292
|
-
var
|
|
3587
|
+
var import_zod16 = require("zod");
|
|
3293
3588
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3294
|
-
var
|
|
3295
|
-
var
|
|
3296
|
-
var
|
|
3589
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3590
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3591
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3297
3592
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3298
3593
|
function sparkline(values) {
|
|
3299
3594
|
const max = Math.max(1, ...values);
|
|
@@ -3364,9 +3659,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3364
3659
|
{
|
|
3365
3660
|
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.',
|
|
3366
3661
|
inputSchema: {
|
|
3367
|
-
deviceId:
|
|
3368
|
-
durationMs:
|
|
3369
|
-
clearFirst:
|
|
3662
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3663
|
+
durationMs: import_zod16.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
3664
|
+
clearFirst: import_zod16.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3370
3665
|
}
|
|
3371
3666
|
},
|
|
3372
3667
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3379,7 +3674,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3379
3674
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3380
3675
|
target
|
|
3381
3676
|
);
|
|
3382
|
-
if (!snap) return
|
|
3677
|
+
if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3383
3678
|
const md = formatTopMarkdown(snap, duration);
|
|
3384
3679
|
const slim = {
|
|
3385
3680
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3398,13 +3693,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3398
3693
|
worstFreezeMs: r.worstFreezeMs
|
|
3399
3694
|
}))
|
|
3400
3695
|
};
|
|
3401
|
-
return
|
|
3696
|
+
return text14(`${md}
|
|
3402
3697
|
|
|
3403
3698
|
\`\`\`json
|
|
3404
3699
|
${JSON.stringify(slim)}
|
|
3405
3700
|
\`\`\``);
|
|
3406
3701
|
} catch (e) {
|
|
3407
|
-
return
|
|
3702
|
+
return fail16(errMsg16(e));
|
|
3408
3703
|
}
|
|
3409
3704
|
}
|
|
3410
3705
|
);
|
|
@@ -3413,8 +3708,8 @@ ${JSON.stringify(slim)}
|
|
|
3413
3708
|
{
|
|
3414
3709
|
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).',
|
|
3415
3710
|
inputSchema: {
|
|
3416
|
-
deviceId:
|
|
3417
|
-
key:
|
|
3711
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3712
|
+
key: import_zod16.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3418
3713
|
}
|
|
3419
3714
|
},
|
|
3420
3715
|
async ({ deviceId, key }) => {
|
|
@@ -3427,18 +3722,18 @@ ${JSON.stringify(slim)}
|
|
|
3427
3722
|
target
|
|
3428
3723
|
);
|
|
3429
3724
|
if (!detail) {
|
|
3430
|
-
return
|
|
3725
|
+
return fail16(
|
|
3431
3726
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3432
3727
|
);
|
|
3433
3728
|
}
|
|
3434
3729
|
const md = formatDetailMarkdown(detail);
|
|
3435
|
-
return
|
|
3730
|
+
return text14(`${md}
|
|
3436
3731
|
|
|
3437
3732
|
\`\`\`json
|
|
3438
3733
|
${JSON.stringify(detail)}
|
|
3439
3734
|
\`\`\``);
|
|
3440
3735
|
} catch (e) {
|
|
3441
|
-
return
|
|
3736
|
+
return fail16(errMsg16(e));
|
|
3442
3737
|
}
|
|
3443
3738
|
}
|
|
3444
3739
|
);
|
|
@@ -3447,27 +3742,27 @@ ${JSON.stringify(detail)}
|
|
|
3447
3742
|
{
|
|
3448
3743
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3449
3744
|
inputSchema: {
|
|
3450
|
-
deviceId:
|
|
3745
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3451
3746
|
}
|
|
3452
3747
|
},
|
|
3453
3748
|
async ({ deviceId }) => {
|
|
3454
3749
|
try {
|
|
3455
3750
|
const target = sync.requireProDevice(deviceId);
|
|
3456
3751
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3457
|
-
return
|
|
3752
|
+
return text14("Cleared js-top stats.");
|
|
3458
3753
|
} catch (e) {
|
|
3459
|
-
return
|
|
3754
|
+
return fail16(errMsg16(e));
|
|
3460
3755
|
}
|
|
3461
3756
|
}
|
|
3462
3757
|
);
|
|
3463
3758
|
}
|
|
3464
3759
|
|
|
3465
3760
|
// src/tools/packs/images.ts
|
|
3466
|
-
var
|
|
3761
|
+
var import_zod17 = require("zod");
|
|
3467
3762
|
var IMAGES_TOOL_ID = "images";
|
|
3468
|
-
var
|
|
3469
|
-
var
|
|
3470
|
-
var
|
|
3763
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3764
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3765
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3471
3766
|
function uriTail(uri) {
|
|
3472
3767
|
if (!uri) return "(no source)";
|
|
3473
3768
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -3555,9 +3850,9 @@ function registerImagesTools(server, sync) {
|
|
|
3555
3850
|
{
|
|
3556
3851
|
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.",
|
|
3557
3852
|
inputSchema: {
|
|
3558
|
-
deviceId:
|
|
3559
|
-
limit:
|
|
3560
|
-
status:
|
|
3853
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3854
|
+
limit: import_zod17.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
3855
|
+
status: import_zod17.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3561
3856
|
}
|
|
3562
3857
|
},
|
|
3563
3858
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3570,7 +3865,7 @@ function registerImagesTools(server, sync) {
|
|
|
3570
3865
|
{ limit: capped, status },
|
|
3571
3866
|
target
|
|
3572
3867
|
);
|
|
3573
|
-
if (!list) return
|
|
3868
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3574
3869
|
const md = formatListMarkdown(list, capped);
|
|
3575
3870
|
const slim = {
|
|
3576
3871
|
stats: list.stats,
|
|
@@ -3597,13 +3892,13 @@ function registerImagesTools(server, sync) {
|
|
|
3597
3892
|
layoutShifts: r.layoutShifts
|
|
3598
3893
|
}))
|
|
3599
3894
|
};
|
|
3600
|
-
return
|
|
3895
|
+
return text15(`${md}
|
|
3601
3896
|
|
|
3602
3897
|
\`\`\`json
|
|
3603
3898
|
${JSON.stringify(slim)}
|
|
3604
3899
|
\`\`\``);
|
|
3605
3900
|
} catch (e) {
|
|
3606
|
-
return
|
|
3901
|
+
return fail17(errMsg17(e));
|
|
3607
3902
|
}
|
|
3608
3903
|
}
|
|
3609
3904
|
);
|
|
@@ -3612,9 +3907,9 @@ ${JSON.stringify(slim)}
|
|
|
3612
3907
|
{
|
|
3613
3908
|
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.",
|
|
3614
3909
|
inputSchema: {
|
|
3615
|
-
deviceId:
|
|
3616
|
-
id:
|
|
3617
|
-
action:
|
|
3910
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3911
|
+
id: import_zod17.z.number().describe("Record id from get_images."),
|
|
3912
|
+
action: import_zod17.z.enum([
|
|
3618
3913
|
"hard_reload",
|
|
3619
3914
|
"retry",
|
|
3620
3915
|
"evict_disk",
|
|
@@ -3626,7 +3921,7 @@ ${JSON.stringify(slim)}
|
|
|
3626
3921
|
"swap_url",
|
|
3627
3922
|
"restore"
|
|
3628
3923
|
]).describe("What to do."),
|
|
3629
|
-
url:
|
|
3924
|
+
url: import_zod17.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3630
3925
|
}
|
|
3631
3926
|
},
|
|
3632
3927
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3634,8 +3929,8 @@ ${JSON.stringify(slim)}
|
|
|
3634
3929
|
const target = sync.requireProDevice(deviceId);
|
|
3635
3930
|
if (action === "prove_savings") {
|
|
3636
3931
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3637
|
-
if (!savings) return
|
|
3638
|
-
if (!savings.ok) return
|
|
3932
|
+
if (!savings) return fail17("Device returned nothing for proveSavings.");
|
|
3933
|
+
if (!savings.ok) return fail17(savings.message);
|
|
3639
3934
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3640
3935
|
if (savings.originalBytes != null)
|
|
3641
3936
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3647,11 +3942,11 @@ ${JSON.stringify(slim)}
|
|
|
3647
3942
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3648
3943
|
if (savings.optimizedUri)
|
|
3649
3944
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3650
|
-
return
|
|
3945
|
+
return text15(parts.join("\n"));
|
|
3651
3946
|
}
|
|
3652
3947
|
const entry = ACTION_MAP[action];
|
|
3653
|
-
if (!entry) return
|
|
3654
|
-
if (action === "swap_url" && !url) return
|
|
3948
|
+
if (!entry) return fail17(`Unknown action: ${action}`);
|
|
3949
|
+
if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
|
|
3655
3950
|
const result = await sync.callAction(
|
|
3656
3951
|
IMAGES_TOOL_ID,
|
|
3657
3952
|
entry.action,
|
|
@@ -3671,9 +3966,9 @@ ${JSON.stringify(slim)}
|
|
|
3671
3966
|
);
|
|
3672
3967
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3673
3968
|
}
|
|
3674
|
-
return
|
|
3969
|
+
return text15(lines.join("\n"));
|
|
3675
3970
|
} catch (e) {
|
|
3676
|
-
return
|
|
3971
|
+
return fail17(errMsg17(e));
|
|
3677
3972
|
}
|
|
3678
3973
|
}
|
|
3679
3974
|
);
|
|
@@ -3682,12 +3977,12 @@ ${JSON.stringify(slim)}
|
|
|
3682
3977
|
{
|
|
3683
3978
|
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.",
|
|
3684
3979
|
inputSchema: {
|
|
3685
|
-
deviceId:
|
|
3686
|
-
network:
|
|
3687
|
-
blankImages:
|
|
3688
|
-
massAction:
|
|
3689
|
-
clearRegistry:
|
|
3690
|
-
clearExpoCaches:
|
|
3980
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3981
|
+
network: import_zod17.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3982
|
+
blankImages: import_zod17.z.boolean().optional().describe("Render all images without a source."),
|
|
3983
|
+
massAction: import_zod17.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
3984
|
+
clearRegistry: import_zod17.z.boolean().optional().describe("Clear the captured image log."),
|
|
3985
|
+
clearExpoCaches: import_zod17.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3691
3986
|
}
|
|
3692
3987
|
},
|
|
3693
3988
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3714,17 +4009,17 @@ ${JSON.stringify(slim)}
|
|
|
3714
4009
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3715
4010
|
done.push(r?.message ?? "expo caches cleared");
|
|
3716
4011
|
}
|
|
3717
|
-
if (done.length === 0) return
|
|
3718
|
-
return
|
|
4012
|
+
if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4013
|
+
return text15(`Applied: ${done.join(" \xB7 ")}`);
|
|
3719
4014
|
} catch (e) {
|
|
3720
|
-
return
|
|
4015
|
+
return fail17(errMsg17(e));
|
|
3721
4016
|
}
|
|
3722
4017
|
}
|
|
3723
4018
|
);
|
|
3724
4019
|
}
|
|
3725
4020
|
|
|
3726
4021
|
// src/tools/packs/assets.ts
|
|
3727
|
-
var
|
|
4022
|
+
var import_zod18 = require("zod");
|
|
3728
4023
|
|
|
3729
4024
|
// src/tools/packs/assetsGraphNote.ts
|
|
3730
4025
|
function graphUnavailableNote(ss) {
|
|
@@ -3739,9 +4034,9 @@ function graphUnavailableNote(ss) {
|
|
|
3739
4034
|
|
|
3740
4035
|
// src/tools/packs/assets.ts
|
|
3741
4036
|
var ASSETS_TOOL_ID = "assets";
|
|
3742
|
-
var
|
|
3743
|
-
var
|
|
3744
|
-
var
|
|
4037
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4038
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4039
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3745
4040
|
function kb2(bytes) {
|
|
3746
4041
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3747
4042
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -3829,10 +4124,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3829
4124
|
{
|
|
3830
4125
|
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.",
|
|
3831
4126
|
inputSchema: {
|
|
3832
|
-
deviceId:
|
|
3833
|
-
limit:
|
|
3834
|
-
kind:
|
|
3835
|
-
unusedOnly:
|
|
4127
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4128
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4129
|
+
kind: import_zod18.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4130
|
+
unusedOnly: import_zod18.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3836
4131
|
}
|
|
3837
4132
|
},
|
|
3838
4133
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3845,7 +4140,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3845
4140
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3846
4141
|
target
|
|
3847
4142
|
);
|
|
3848
|
-
if (!list) return
|
|
4143
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3849
4144
|
if (unusedOnly) {
|
|
3850
4145
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3851
4146
|
}
|
|
@@ -3869,13 +4164,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3869
4164
|
sizeSource: r.sizeSource
|
|
3870
4165
|
}))
|
|
3871
4166
|
};
|
|
3872
|
-
return
|
|
4167
|
+
return text16(`${md}
|
|
3873
4168
|
|
|
3874
4169
|
\`\`\`json
|
|
3875
4170
|
${JSON.stringify(slim)}
|
|
3876
4171
|
\`\`\``);
|
|
3877
4172
|
} catch (e) {
|
|
3878
|
-
return
|
|
4173
|
+
return fail18(errMsg18(e));
|
|
3879
4174
|
}
|
|
3880
4175
|
}
|
|
3881
4176
|
);
|
|
@@ -3884,9 +4179,9 @@ ${JSON.stringify(slim)}
|
|
|
3884
4179
|
{
|
|
3885
4180
|
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).",
|
|
3886
4181
|
inputSchema: {
|
|
3887
|
-
deviceId:
|
|
3888
|
-
action:
|
|
3889
|
-
id:
|
|
4182
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4183
|
+
action: import_zod18.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4184
|
+
id: import_zod18.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3890
4185
|
}
|
|
3891
4186
|
},
|
|
3892
4187
|
async ({ deviceId, action, id }) => {
|
|
@@ -3896,19 +4191,19 @@ ${JSON.stringify(slim)}
|
|
|
3896
4191
|
case "rescan": {
|
|
3897
4192
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3898
4193
|
const ss = result?.scanStatus;
|
|
3899
|
-
if (!ss) return
|
|
3900
|
-
return
|
|
4194
|
+
if (!ss) return fail18("Device returned nothing for rescan.");
|
|
4195
|
+
return text16(
|
|
3901
4196
|
`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" : "")
|
|
3902
4197
|
);
|
|
3903
4198
|
}
|
|
3904
4199
|
case "measure_sizes": {
|
|
3905
4200
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3906
|
-
return
|
|
4201
|
+
return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3907
4202
|
}
|
|
3908
4203
|
case "detail": {
|
|
3909
|
-
if (id == null) return
|
|
4204
|
+
if (id == null) return fail18("detail requires the `id` param (from get_assets).");
|
|
3910
4205
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3911
|
-
if (!detail) return
|
|
4206
|
+
if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3912
4207
|
const lines = [
|
|
3913
4208
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3914
4209
|
"",
|
|
@@ -3926,32 +4221,32 @@ ${JSON.stringify(slim)}
|
|
|
3926
4221
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3927
4222
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3928
4223
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3929
|
-
return
|
|
4224
|
+
return text16(lines.join("\n"));
|
|
3930
4225
|
}
|
|
3931
4226
|
case "save_baseline": {
|
|
3932
4227
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3933
|
-
return
|
|
4228
|
+
return text16(
|
|
3934
4229
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3935
4230
|
);
|
|
3936
4231
|
}
|
|
3937
4232
|
case "clear_baseline": {
|
|
3938
4233
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3939
|
-
return
|
|
4234
|
+
return text16("Baseline cleared.");
|
|
3940
4235
|
}
|
|
3941
4236
|
case "clear_records": {
|
|
3942
4237
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3943
|
-
return
|
|
4238
|
+
return text16(result?.message ?? "Inventory cleared.");
|
|
3944
4239
|
}
|
|
3945
4240
|
}
|
|
3946
4241
|
} catch (e) {
|
|
3947
|
-
return
|
|
4242
|
+
return fail18(errMsg18(e));
|
|
3948
4243
|
}
|
|
3949
4244
|
}
|
|
3950
4245
|
);
|
|
3951
4246
|
}
|
|
3952
4247
|
|
|
3953
4248
|
// src/tools/packs/reload.ts
|
|
3954
|
-
var
|
|
4249
|
+
var import_zod19 = require("zod");
|
|
3955
4250
|
|
|
3956
4251
|
// src/capture/devServerReload.ts
|
|
3957
4252
|
var import_ws = require("ws");
|
|
@@ -4014,9 +4309,9 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4014
4309
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4015
4310
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4016
4311
|
var PROBE_INTERVAL_MS = 400;
|
|
4017
|
-
var
|
|
4018
|
-
var
|
|
4019
|
-
var
|
|
4312
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4313
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4314
|
+
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4020
4315
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4021
4316
|
async function ping(sync, deviceId, timeoutMs) {
|
|
4022
4317
|
try {
|
|
@@ -4055,12 +4350,12 @@ function registerReloadTools(server, sync) {
|
|
|
4055
4350
|
{
|
|
4056
4351
|
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.",
|
|
4057
4352
|
inputSchema: {
|
|
4058
|
-
strategy:
|
|
4353
|
+
strategy: import_zod19.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4059
4354
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4060
4355
|
),
|
|
4061
|
-
wait:
|
|
4062
|
-
timeoutMs:
|
|
4063
|
-
deviceId:
|
|
4356
|
+
wait: import_zod19.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4357
|
+
timeoutMs: import_zod19.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4358
|
+
deviceId: import_zod19.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4064
4359
|
}
|
|
4065
4360
|
},
|
|
4066
4361
|
async (args) => {
|
|
@@ -4102,32 +4397,32 @@ function registerReloadTools(server, sync) {
|
|
|
4102
4397
|
}
|
|
4103
4398
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4104
4399
|
if (args.wait === false) {
|
|
4105
|
-
return
|
|
4400
|
+
return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4106
4401
|
}
|
|
4107
4402
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4108
4403
|
if (elapsed == null) {
|
|
4109
|
-
return
|
|
4404
|
+
return fail19(
|
|
4110
4405
|
`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.`
|
|
4111
4406
|
);
|
|
4112
4407
|
}
|
|
4113
|
-
return
|
|
4408
|
+
return text17(
|
|
4114
4409
|
`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.`
|
|
4115
4410
|
);
|
|
4116
4411
|
} catch (e) {
|
|
4117
|
-
return
|
|
4412
|
+
return fail19(errMsg19(e));
|
|
4118
4413
|
}
|
|
4119
4414
|
}
|
|
4120
4415
|
);
|
|
4121
4416
|
}
|
|
4122
4417
|
|
|
4123
4418
|
// src/tools/packs/triage.ts
|
|
4124
|
-
var
|
|
4125
|
-
var
|
|
4126
|
-
var
|
|
4419
|
+
var import_zod20 = require("zod");
|
|
4420
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4421
|
+
var fail20 = (message) => ({
|
|
4127
4422
|
isError: true,
|
|
4128
4423
|
content: [{ type: "text", text: message }]
|
|
4129
4424
|
});
|
|
4130
|
-
var
|
|
4425
|
+
var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
|
|
4131
4426
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4132
4427
|
var MAX_STORAGE_KEYS = 20;
|
|
4133
4428
|
var MAX_STORAGE_VALUE = 400;
|
|
@@ -4164,7 +4459,7 @@ async function section(title, run) {
|
|
|
4164
4459
|
return body ? `${title}
|
|
4165
4460
|
${body}` : null;
|
|
4166
4461
|
} catch (e) {
|
|
4167
|
-
const msg =
|
|
4462
|
+
const msg = errMsg20(e);
|
|
4168
4463
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4169
4464
|
return `${title}
|
|
4170
4465
|
(no answer \u2014 see the note above)`;
|
|
@@ -4179,7 +4474,7 @@ function registerTriageTools(server, sync) {
|
|
|
4179
4474
|
{
|
|
4180
4475
|
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.",
|
|
4181
4476
|
inputSchema: {
|
|
4182
|
-
deviceId:
|
|
4477
|
+
deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4183
4478
|
}
|
|
4184
4479
|
},
|
|
4185
4480
|
async (args) => {
|
|
@@ -4286,7 +4581,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4286
4581
|
|
|
4287
4582
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4288
4583
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4289
|
-
return
|
|
4584
|
+
return text18(
|
|
4290
4585
|
`${head}${banner}
|
|
4291
4586
|
|
|
4292
4587
|
${body}
|
|
@@ -4294,22 +4589,22 @@ ${body}
|
|
|
4294
4589
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4295
4590
|
);
|
|
4296
4591
|
} catch (e) {
|
|
4297
|
-
return
|
|
4592
|
+
return fail20(errMsg20(e));
|
|
4298
4593
|
}
|
|
4299
4594
|
}
|
|
4300
4595
|
);
|
|
4301
4596
|
}
|
|
4302
4597
|
|
|
4303
4598
|
// src/tools/packs/flow.ts
|
|
4304
|
-
var
|
|
4599
|
+
var import_zod21 = require("zod");
|
|
4305
4600
|
var UI_TOOL_ID = "highlight-updates";
|
|
4306
4601
|
var APP_TOOL_ID2 = "app";
|
|
4307
|
-
var
|
|
4308
|
-
var
|
|
4602
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4603
|
+
var fail21 = (message) => ({
|
|
4309
4604
|
isError: true,
|
|
4310
4605
|
content: [{ type: "text", text: message }]
|
|
4311
4606
|
});
|
|
4312
|
-
var
|
|
4607
|
+
var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
|
|
4313
4608
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4314
4609
|
var POLL_MS = 250;
|
|
4315
4610
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4326,19 +4621,19 @@ function formatElement2(e) {
|
|
|
4326
4621
|
const display = e.text ?? e.label;
|
|
4327
4622
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4328
4623
|
}
|
|
4329
|
-
var stepSchema =
|
|
4330
|
-
tap:
|
|
4331
|
-
longPress:
|
|
4332
|
-
type:
|
|
4333
|
-
into:
|
|
4334
|
-
set:
|
|
4335
|
-
reload:
|
|
4336
|
-
waitMs:
|
|
4337
|
-
waitFor:
|
|
4338
|
-
waitGone:
|
|
4339
|
-
expect:
|
|
4340
|
-
expectGone:
|
|
4341
|
-
timeoutMs:
|
|
4624
|
+
var stepSchema = import_zod21.z.object({
|
|
4625
|
+
tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4626
|
+
longPress: import_zod21.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4627
|
+
type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4628
|
+
into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4629
|
+
set: import_zod21.z.union([import_zod21.z.number(), import_zod21.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4630
|
+
reload: import_zod21.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4631
|
+
waitMs: import_zod21.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4632
|
+
waitFor: import_zod21.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4633
|
+
waitGone: import_zod21.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4634
|
+
expect: import_zod21.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
4635
|
+
expectGone: import_zod21.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
4636
|
+
timeoutMs: import_zod21.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4342
4637
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4343
4638
|
function registerFlowTools(server, sync) {
|
|
4344
4639
|
server.registerTool(
|
|
@@ -4346,10 +4641,10 @@ function registerFlowTools(server, sync) {
|
|
|
4346
4641
|
{
|
|
4347
4642
|
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.",
|
|
4348
4643
|
inputSchema: {
|
|
4349
|
-
steps:
|
|
4350
|
-
include:
|
|
4351
|
-
continueOnFailure:
|
|
4352
|
-
deviceId:
|
|
4644
|
+
steps: import_zod21.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4645
|
+
include: import_zod21.z.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."),
|
|
4646
|
+
continueOnFailure: import_zod21.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4647
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4353
4648
|
}
|
|
4354
4649
|
},
|
|
4355
4650
|
async (args) => {
|
|
@@ -4521,7 +4816,7 @@ function registerFlowTools(server, sync) {
|
|
|
4521
4816
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
4522
4817
|
}
|
|
4523
4818
|
} catch (e) {
|
|
4524
|
-
stepErrors.push(
|
|
4819
|
+
stepErrors.push(errMsg21(e));
|
|
4525
4820
|
}
|
|
4526
4821
|
if (stepErrors.length) {
|
|
4527
4822
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -4544,7 +4839,7 @@ function registerFlowTools(server, sync) {
|
|
|
4544
4839
|
${shown.map(formatElement2).join("\n")}`
|
|
4545
4840
|
);
|
|
4546
4841
|
} catch (e) {
|
|
4547
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
4842
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
|
|
4548
4843
|
}
|
|
4549
4844
|
}
|
|
4550
4845
|
if (include === "all") {
|
|
@@ -4573,9 +4868,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4573
4868
|
}
|
|
4574
4869
|
}
|
|
4575
4870
|
}
|
|
4576
|
-
return
|
|
4871
|
+
return text19(parts.join("\n\n"));
|
|
4577
4872
|
} catch (e) {
|
|
4578
|
-
return
|
|
4873
|
+
return fail21(errMsg21(e));
|
|
4579
4874
|
}
|
|
4580
4875
|
}
|
|
4581
4876
|
);
|
|
@@ -4596,25 +4891,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4596
4891
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4597
4892
|
|
|
4598
4893
|
// src/tools/packs/renders.ts
|
|
4599
|
-
var
|
|
4894
|
+
var import_zod22 = require("zod");
|
|
4600
4895
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4601
|
-
var
|
|
4602
|
-
var
|
|
4896
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4897
|
+
var fail22 = (message) => ({
|
|
4603
4898
|
isError: true,
|
|
4604
4899
|
content: [{ type: "text", text: message }]
|
|
4605
4900
|
});
|
|
4606
|
-
var
|
|
4901
|
+
var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
|
|
4607
4902
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4608
4903
|
var DEFAULT_IDLE_MS = 3e3;
|
|
4609
4904
|
var SETTLE_MS = 400;
|
|
4610
4905
|
var DEFAULT_TOP = 12;
|
|
4611
4906
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4612
|
-
var stepSchema2 =
|
|
4613
|
-
tap:
|
|
4614
|
-
type:
|
|
4615
|
-
into:
|
|
4616
|
-
waitMs:
|
|
4617
|
-
repeat:
|
|
4907
|
+
var stepSchema2 = import_zod22.z.object({
|
|
4908
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4909
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4910
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4911
|
+
waitMs: import_zod22.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
4912
|
+
repeat: import_zod22.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4618
4913
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4619
4914
|
function selector2(needle) {
|
|
4620
4915
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4707,14 +5002,14 @@ function registerRenderTools(server, sync) {
|
|
|
4707
5002
|
{
|
|
4708
5003
|
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.",
|
|
4709
5004
|
inputSchema: {
|
|
4710
|
-
steps:
|
|
5005
|
+
steps: import_zod22.z.array(stepSchema2).max(20).optional().describe(
|
|
4711
5006
|
"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."
|
|
4712
5007
|
),
|
|
4713
|
-
idleMs:
|
|
4714
|
-
label:
|
|
4715
|
-
compareToPrevious:
|
|
4716
|
-
top:
|
|
4717
|
-
deviceId:
|
|
5008
|
+
idleMs: import_zod22.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5009
|
+
label: import_zod22.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5010
|
+
compareToPrevious: import_zod22.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5011
|
+
top: import_zod22.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5012
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4718
5013
|
}
|
|
4719
5014
|
},
|
|
4720
5015
|
async (args) => {
|
|
@@ -4723,18 +5018,18 @@ function registerRenderTools(server, sync) {
|
|
|
4723
5018
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4724
5019
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4725
5020
|
if (!uiTool) {
|
|
4726
|
-
return
|
|
5021
|
+
return fail22(
|
|
4727
5022
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4728
5023
|
);
|
|
4729
5024
|
}
|
|
4730
5025
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4731
|
-
return
|
|
5026
|
+
return fail22(
|
|
4732
5027
|
"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."
|
|
4733
5028
|
);
|
|
4734
5029
|
}
|
|
4735
5030
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4736
5031
|
if (began && began.ok === false) {
|
|
4737
|
-
return
|
|
5032
|
+
return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4738
5033
|
}
|
|
4739
5034
|
await delay5(SETTLE_MS);
|
|
4740
5035
|
const startedAt = Date.now();
|
|
@@ -4779,7 +5074,7 @@ function registerRenderTools(server, sync) {
|
|
|
4779
5074
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
4780
5075
|
const summary = ended?.summary;
|
|
4781
5076
|
if (!summary) {
|
|
4782
|
-
return
|
|
5077
|
+
return fail22(
|
|
4783
5078
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
4784
5079
|
);
|
|
4785
5080
|
}
|
|
@@ -4804,9 +5099,9 @@ function registerRenderTools(server, sync) {
|
|
|
4804
5099
|
} else if (!args.compareToPrevious) {
|
|
4805
5100
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
4806
5101
|
}
|
|
4807
|
-
return
|
|
5102
|
+
return text20(parts.join("\n"));
|
|
4808
5103
|
} catch (e) {
|
|
4809
|
-
return
|
|
5104
|
+
return fail22(errMsg22(e));
|
|
4810
5105
|
}
|
|
4811
5106
|
}
|
|
4812
5107
|
);
|
|
@@ -5027,6 +5322,7 @@ require the connected app to have an active Buoy Pro license. If they error with
|
|
|
5027
5322
|
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
5028
5323
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
5029
5324
|
"measure_renders",
|
|
5325
|
+
"scenario_action",
|
|
5030
5326
|
"run_flow",
|
|
5031
5327
|
"run_benchmark_batch",
|
|
5032
5328
|
"get_triage",
|
|
@@ -5077,6 +5373,7 @@ function createMcpServer(sync) {
|
|
|
5077
5373
|
registerRoutesTools(server, sync);
|
|
5078
5374
|
registerConsoleTools(server, sync);
|
|
5079
5375
|
registerTimeMachineTools(server, sync);
|
|
5376
|
+
registerScenariosTools(server, sync);
|
|
5080
5377
|
registerScreenshotTools(server, sync);
|
|
5081
5378
|
registerInteractTools(server, sync);
|
|
5082
5379
|
registerJsThreadTools(server, sync);
|
|
@@ -5086,7 +5383,7 @@ function createMcpServer(sync) {
|
|
|
5086
5383
|
return server;
|
|
5087
5384
|
}
|
|
5088
5385
|
var deviceIdField = {
|
|
5089
|
-
deviceId:
|
|
5386
|
+
deviceId: import_zod23.z.string().optional().describe(
|
|
5090
5387
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5091
5388
|
)
|
|
5092
5389
|
};
|