@buoy-gg/mcp 7.0.29 → 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-V7PYMDI2.mjs → chunk-HUL2D27L.mjs} +462 -163
- package/dist/cli.js +475 -176
- package/dist/cli.mjs +1 -1
- package/dist/index.js +475 -176
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -307,7 +307,7 @@ var SyncClient = class {
|
|
|
307
307
|
};
|
|
308
308
|
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
309
309
|
dev2
|
|
310
|
-
)}
|
|
310
|
+
)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;
|
|
311
311
|
if (deviceId === ALL_DEVICES) {
|
|
312
312
|
if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
|
|
313
313
|
return ALL_DEVICES;
|
|
@@ -640,7 +640,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
640
640
|
|
|
641
641
|
// src/server.ts
|
|
642
642
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
643
|
-
var
|
|
643
|
+
var import_zod23 = require("zod");
|
|
644
644
|
|
|
645
645
|
// src/tools/generic.ts
|
|
646
646
|
var import_zod = require("zod");
|
|
@@ -651,7 +651,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
651
651
|
var import_node_path = __toESM(require("path"));
|
|
652
652
|
|
|
653
653
|
// src/version.ts
|
|
654
|
-
var VERSION = "7.0.
|
|
654
|
+
var VERSION = "7.0.31" ? "7.0.31" : "0.0.0-dev";
|
|
655
655
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
656
656
|
|
|
657
657
|
// src/update/checkForUpdate.ts
|
|
@@ -748,7 +748,9 @@ function registerGenericTools(server, sync) {
|
|
|
748
748
|
return json({
|
|
749
749
|
devices,
|
|
750
750
|
...anyPro ? {} : {
|
|
751
|
-
|
|
751
|
+
// A headless build has no on-device UI, so "upgrade in the app"
|
|
752
|
+
// cannot be followed there — name the `licenseKey` prop too.
|
|
753
|
+
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Enter a license in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless (no on-device UI)."
|
|
752
754
|
},
|
|
753
755
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
754
756
|
});
|
|
@@ -2909,8 +2911,303 @@ ${lines.join("\n")}`);
|
|
|
2909
2911
|
);
|
|
2910
2912
|
}
|
|
2911
2913
|
|
|
2912
|
-
// src/tools/packs/
|
|
2914
|
+
// src/tools/packs/scenarios.ts
|
|
2913
2915
|
var import_zod13 = require("zod");
|
|
2916
|
+
var SCENARIOS_TOOL_ID = "scenarios";
|
|
2917
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2918
|
+
var fail13 = (message) => ({
|
|
2919
|
+
isError: true,
|
|
2920
|
+
content: [{ type: "text", text: message }]
|
|
2921
|
+
});
|
|
2922
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2923
|
+
function varSpec(v) {
|
|
2924
|
+
const opts = v.options?.length ? ` one of ${v.options.join("|")}` : "";
|
|
2925
|
+
const def = v.default !== void 0 ? ` (default ${String(v.default)})` : "";
|
|
2926
|
+
return `${v.key}: ${v.type}${opts}${def}`;
|
|
2927
|
+
}
|
|
2928
|
+
function summarize(s) {
|
|
2929
|
+
const vars = s.vars?.length ? `
|
|
2930
|
+
variables: ${s.vars.map(varSpec).join("; ")}` : "";
|
|
2931
|
+
const steps = s.stepLines?.length ? `
|
|
2932
|
+
${s.stepLines.map((st) => ` \xB7 ${st.label} [${st.durability}]`).join("\n")}` : "";
|
|
2933
|
+
const meta = [
|
|
2934
|
+
`v${s.version}`,
|
|
2935
|
+
s.author,
|
|
2936
|
+
s.usage?.runs ? `${s.usage.runs} run(s)` : null,
|
|
2937
|
+
s.authoredAgainst ? `authored against ${s.authoredAgainst}` : null,
|
|
2938
|
+
s.unsavedToRepo ? "unsaved to repo" : null
|
|
2939
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2940
|
+
const outcome = s.expectedOutcome ? `
|
|
2941
|
+
expected: ${s.expectedOutcome}` : "";
|
|
2942
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${meta}${vars}${outcome}${steps}`;
|
|
2943
|
+
}
|
|
2944
|
+
function renderSection(title, list, folders) {
|
|
2945
|
+
if (!list.length) return `**${title} (0):**
|
|
2946
|
+
_none_`;
|
|
2947
|
+
if (!folders.length) {
|
|
2948
|
+
return `**${title} (${list.length}):**
|
|
2949
|
+
${list.map(summarize).join("\n")}`;
|
|
2950
|
+
}
|
|
2951
|
+
const parts = [`**${title} (${list.length}):**`];
|
|
2952
|
+
for (const folder of folders) {
|
|
2953
|
+
const inIt = list.filter(
|
|
2954
|
+
(s) => (s.folder ?? "").toLowerCase() === folder.toLowerCase()
|
|
2955
|
+
);
|
|
2956
|
+
if (!inIt.length) continue;
|
|
2957
|
+
parts.push(`_${folder} (${inIt.length})_
|
|
2958
|
+
${inIt.map(summarize).join("\n")}`);
|
|
2959
|
+
}
|
|
2960
|
+
const ungrouped = list.filter((s) => !s.folder);
|
|
2961
|
+
if (ungrouped.length) {
|
|
2962
|
+
parts.push(
|
|
2963
|
+
`_Ungrouped (${ungrouped.length})_
|
|
2964
|
+
${ungrouped.map(summarize).join("\n")}`
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
return parts.join("\n");
|
|
2968
|
+
}
|
|
2969
|
+
function formatActive(a) {
|
|
2970
|
+
const vars = Object.entries(a.varValues).map(([k, v]) => `${k}=${String(v)}`).join(", ");
|
|
2971
|
+
return [
|
|
2972
|
+
`**${a.name}** (\`${a.scenarioId}\` v${a.version}) \u2014 active since ${new Date(a.runAt).toISOString()}`,
|
|
2973
|
+
vars ? `variables: ${vars}` : null,
|
|
2974
|
+
`effects:
|
|
2975
|
+
${a.effects.map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`
|
|
2976
|
+
].filter(Boolean).join("\n");
|
|
2977
|
+
}
|
|
2978
|
+
function registerScenariosTools(server, sync) {
|
|
2979
|
+
server.registerTool(
|
|
2980
|
+
"get_scenarios",
|
|
2981
|
+
{
|
|
2982
|
+
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.",
|
|
2983
|
+
inputSchema: {
|
|
2984
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2985
|
+
}
|
|
2986
|
+
},
|
|
2987
|
+
async (args) => {
|
|
2988
|
+
try {
|
|
2989
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2990
|
+
const { payload } = await sync.getSnapshot(SCENARIOS_TOOL_ID, target);
|
|
2991
|
+
const p = payload ?? {};
|
|
2992
|
+
const code = p.code ?? [];
|
|
2993
|
+
const device = p.device ?? [];
|
|
2994
|
+
const drafts = p.drafts ?? [];
|
|
2995
|
+
const folders = p.folders ?? [];
|
|
2996
|
+
const parts = [];
|
|
2997
|
+
parts.push(
|
|
2998
|
+
p.active ? `\u26A0\uFE0F **SIMULATED \u2014 a scenario is active.** The app is showing bent state right now.
|
|
2999
|
+
${formatActive(p.active)}` : "**No scenario active** \u2014 the app is showing real data."
|
|
3000
|
+
);
|
|
3001
|
+
if (folders.length) {
|
|
3002
|
+
parts.push(
|
|
3003
|
+
`**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.`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
parts.push(renderSection("Shipped with this app", code, folders));
|
|
3007
|
+
parts.push(renderSection("Saved on this device", device, folders));
|
|
3008
|
+
if (drafts.length) {
|
|
3009
|
+
parts.push(
|
|
3010
|
+
`**Drafts awaiting review on the device (${drafts.length}):** these are INERT and cannot run until a human accepts them on the device.
|
|
3011
|
+
${drafts.map(summarize).join("\n")}`
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
return text12(parts.join("\n\n"));
|
|
3015
|
+
} catch (e) {
|
|
3016
|
+
return fail13(errMsg13(e));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
);
|
|
3020
|
+
server.registerTool(
|
|
3021
|
+
"scenario_action",
|
|
3022
|
+
{
|
|
3023
|
+
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.`,
|
|
3024
|
+
inputSchema: {
|
|
3025
|
+
action: import_zod13.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3026
|
+
folder: import_zod13.z.string().optional().describe(
|
|
3027
|
+
"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."
|
|
3028
|
+
),
|
|
3029
|
+
id: import_zod13.z.string().optional().describe(
|
|
3030
|
+
"Scenario id (required for preview/run/delete; from get_scenarios)."
|
|
3031
|
+
),
|
|
3032
|
+
params: import_zod13.z.record(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional().describe(
|
|
3033
|
+
"Variable values for preview/run, e.g. {storeId:'220'}. Declared defaults fill in the rest."
|
|
3034
|
+
),
|
|
3035
|
+
scenario: import_zod13.z.object({
|
|
3036
|
+
id: import_zod13.z.string().describe("Stable kebab-case id."),
|
|
3037
|
+
name: import_zod13.z.string().describe("Human name; may contain {{variable}} placeholders."),
|
|
3038
|
+
description: import_zod13.z.string().optional(),
|
|
3039
|
+
expectedOutcome: import_zod13.z.string().optional().describe(
|
|
3040
|
+
"What SHOULD happen once applied \u2014 this is how a tester tells a bug from expected behavior without asking a developer. Write it."
|
|
3041
|
+
),
|
|
3042
|
+
author: import_zod13.z.string().optional(),
|
|
3043
|
+
authoredAgainst: import_zod13.z.string().optional().describe("App version this was written against."),
|
|
3044
|
+
folder: import_zod13.z.string().optional().describe(
|
|
3045
|
+
'The flow this belongs to \u2014 "Checkout", "Login". Reuse a folder from get_scenarios; a scenario nobody can find is a scenario nobody runs.'
|
|
3046
|
+
),
|
|
3047
|
+
tags: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
3048
|
+
vars: import_zod13.z.array(
|
|
3049
|
+
import_zod13.z.object({
|
|
3050
|
+
key: import_zod13.z.string(),
|
|
3051
|
+
label: import_zod13.z.string().optional(),
|
|
3052
|
+
type: import_zod13.z.enum(["string", "number", "boolean", "enum"]),
|
|
3053
|
+
options: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()])).optional(),
|
|
3054
|
+
default: import_zod13.z.union([import_zod13.z.string(), import_zod13.z.number(), import_zod13.z.boolean()]).optional()
|
|
3055
|
+
})
|
|
3056
|
+
).optional().describe(
|
|
3057
|
+
"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."
|
|
3058
|
+
),
|
|
3059
|
+
steps: import_zod13.z.array(
|
|
3060
|
+
import_zod13.z.object({
|
|
3061
|
+
id: import_zod13.z.string().optional(),
|
|
3062
|
+
tool: import_zod13.z.string().describe(
|
|
3063
|
+
"Tool id: network | storage | impersonate | route-events | query | time-machine"
|
|
3064
|
+
),
|
|
3065
|
+
action: import_zod13.z.string().describe(
|
|
3066
|
+
"Action name on that tool, e.g. upsertOverrideRule, async.setItem, startImpersonation, navigate."
|
|
3067
|
+
),
|
|
3068
|
+
params: import_zod13.z.unknown().optional(),
|
|
3069
|
+
label: import_zod13.z.string().optional()
|
|
3070
|
+
})
|
|
3071
|
+
).describe("Steps, in order."),
|
|
3072
|
+
undoSteps: import_zod13.z.array(
|
|
3073
|
+
import_zod13.z.object({
|
|
3074
|
+
id: import_zod13.z.string().optional(),
|
|
3075
|
+
tool: import_zod13.z.string(),
|
|
3076
|
+
action: import_zod13.z.string(),
|
|
3077
|
+
params: import_zod13.z.unknown().optional(),
|
|
3078
|
+
label: import_zod13.z.string().optional()
|
|
3079
|
+
})
|
|
3080
|
+
).optional().describe(
|
|
3081
|
+
"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."
|
|
3082
|
+
)
|
|
3083
|
+
}).optional().describe("The scenario to save (action 'save')."),
|
|
3084
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3085
|
+
}
|
|
3086
|
+
},
|
|
3087
|
+
async (args) => {
|
|
3088
|
+
try {
|
|
3089
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
3090
|
+
const call = (action, params) => sync.callAction(SCENARIOS_TOOL_ID, action, params, target);
|
|
3091
|
+
switch (args.action) {
|
|
3092
|
+
case "preview": {
|
|
3093
|
+
if (!args.id) return fail13("preview requires `id` (from get_scenarios).");
|
|
3094
|
+
const r = await call("preview", {
|
|
3095
|
+
id: args.id,
|
|
3096
|
+
params: args.params ?? {}
|
|
3097
|
+
});
|
|
3098
|
+
const lines = [
|
|
3099
|
+
`**Dry run \u2014 \`${args.id}\`** (nothing has been changed)`,
|
|
3100
|
+
`variables: ${Object.entries(r.values).map(([k, v]) => `${k}=${String(v)}`).join(", ") || "none"}`,
|
|
3101
|
+
`**Would do:**
|
|
3102
|
+
${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
3103
|
+
];
|
|
3104
|
+
if (r.expectedOutcome) lines.push(`**Expected outcome:** ${r.expectedOutcome}`);
|
|
3105
|
+
if (r.conflict) {
|
|
3106
|
+
lines.push(
|
|
3107
|
+
`\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.`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3111
|
+
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3112
|
+
lines.push(
|
|
3113
|
+
r.ok ? "\u2705 Pre-flight passed \u2014 safe to run." : `\u274C **Cannot run:**
|
|
3114
|
+
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3115
|
+
);
|
|
3116
|
+
return text12(lines.join("\n\n"));
|
|
3117
|
+
}
|
|
3118
|
+
case "run": {
|
|
3119
|
+
if (!args.id) return fail13("run requires `id` (from get_scenarios).");
|
|
3120
|
+
const r = await call("run", {
|
|
3121
|
+
id: args.id,
|
|
3122
|
+
params: args.params ?? {}
|
|
3123
|
+
});
|
|
3124
|
+
if (r.preflightErrors?.length) {
|
|
3125
|
+
return fail13(
|
|
3126
|
+
`Pre-flight refused to run \`${args.id}\` \u2014 nothing was changed:
|
|
3127
|
+
${r.preflightErrors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
const steps = (r.steps ?? []).map((s) => {
|
|
3131
|
+
const mark = s.skipped ? "\u2013" : s.ok ? "\u2713" : "\u2717";
|
|
3132
|
+
const err = s.error ? `
|
|
3133
|
+
${s.error}` : "";
|
|
3134
|
+
return ` ${mark} ${s.label} (${s.ms}ms)${err}`;
|
|
3135
|
+
}).join("\n");
|
|
3136
|
+
if (!r.ok) {
|
|
3137
|
+
return fail13(
|
|
3138
|
+
`Run FAILED \u2014 it stopped at the failing step and nothing after it ran. The steps before it DID apply:
|
|
3139
|
+
${steps}
|
|
3140
|
+
|
|
3141
|
+
The device is now in a partial state. Deactivate to reverse what landed, or fix the step and run again.`
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
return text12(
|
|
3145
|
+
[
|
|
3146
|
+
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3147
|
+
steps,
|
|
3148
|
+
`**Now bent:**
|
|
3149
|
+
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3150
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, or anything you observe from here is partly fake."
|
|
3151
|
+
].join("\n\n")
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
case "deactivate": {
|
|
3155
|
+
const r = await call("deactivate", {});
|
|
3156
|
+
const rep = r.report;
|
|
3157
|
+
const lines = [
|
|
3158
|
+
`Removed ${rep.removedRules} override rule(s)`,
|
|
3159
|
+
rep.stoppedImpersonation ? "Ended impersonation" : null,
|
|
3160
|
+
rep.undoStepsRan ? `Ran ${rep.undoStepsRan} undo step(s)` : null
|
|
3161
|
+
].filter(Boolean);
|
|
3162
|
+
const warn = rep.unrestoredStorageKeys.length ? `
|
|
3163
|
+
|
|
3164
|
+
\u26A0\uFE0F **NOT restored:** ${rep.unrestoredStorageKeys.join(", ")} \u2014 a storage write has no automatic inverse. The app still holds these values.` : "";
|
|
3165
|
+
const errs = rep.errors.length ? `
|
|
3166
|
+
|
|
3167
|
+
Errors: ${rep.errors.join("; ")}` : "";
|
|
3168
|
+
return text12(`**Deactivated.**
|
|
3169
|
+
${lines.map((l) => ` \xB7 ${l}`).join("\n")}${warn}${errs}`);
|
|
3170
|
+
}
|
|
3171
|
+
case "save": {
|
|
3172
|
+
if (!args.scenario) return fail13("save requires `scenario`.");
|
|
3173
|
+
const r = await call("save", { scenario: args.scenario });
|
|
3174
|
+
if (!r.ok) {
|
|
3175
|
+
return fail13(
|
|
3176
|
+
`Save rejected:
|
|
3177
|
+
${(r.errors ?? [r.error ?? "unknown error"]).map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
return text12(
|
|
3181
|
+
`Saved \`${r.id}\` **as a draft**.
|
|
3182
|
+
|
|
3183
|
+
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.`
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
case "setFolder": {
|
|
3187
|
+
if (!args.id) return fail13("setFolder requires `id` (from get_scenarios).");
|
|
3188
|
+
const r = await call("setFolder", {
|
|
3189
|
+
id: args.id,
|
|
3190
|
+
folder: args.folder
|
|
3191
|
+
});
|
|
3192
|
+
return text12(
|
|
3193
|
+
r.folder ? `Filed \`${r.id}\` under **${r.folder}**.` : `Removed \`${r.id}\` from its folder \u2014 it now shows under Ungrouped.`
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
case "delete": {
|
|
3197
|
+
if (!args.id) return fail13("delete requires `id`.");
|
|
3198
|
+
await call("delete", { id: args.id });
|
|
3199
|
+
return text12(`Deleted \`${args.id}\` from the device library.`);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
} catch (e) {
|
|
3203
|
+
return fail13(errMsg13(e));
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// src/tools/packs/screenshot.ts
|
|
3210
|
+
var import_zod14 = require("zod");
|
|
2914
3211
|
|
|
2915
3212
|
// src/capture/simctl.ts
|
|
2916
3213
|
var import_node_child_process = require("child_process");
|
|
@@ -3024,8 +3321,8 @@ function frameToRect(frame, screen) {
|
|
|
3024
3321
|
var TOOL_ID = "highlight-updates";
|
|
3025
3322
|
var MARGIN_PT = 12;
|
|
3026
3323
|
var CAPTURE_PADDING_PT = 6;
|
|
3027
|
-
var
|
|
3028
|
-
var
|
|
3324
|
+
var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3325
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
3029
3326
|
function reasonMessage(res, query) {
|
|
3030
3327
|
switch (res.reason) {
|
|
3031
3328
|
case "no-renders":
|
|
@@ -3055,10 +3352,10 @@ function registerScreenshotTools(server, sync) {
|
|
|
3055
3352
|
{
|
|
3056
3353
|
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.",
|
|
3057
3354
|
inputSchema: {
|
|
3058
|
-
query:
|
|
3059
|
-
nativeTag:
|
|
3060
|
-
scrollIntoView:
|
|
3061
|
-
deviceId:
|
|
3355
|
+
query: import_zod14.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3356
|
+
nativeTag: import_zod14.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3357
|
+
scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
|
|
3358
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3062
3359
|
}
|
|
3063
3360
|
},
|
|
3064
3361
|
async (args) => {
|
|
@@ -3100,11 +3397,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3100
3397
|
}
|
|
3101
3398
|
}
|
|
3102
3399
|
if (!located.matched || !located.rect || !located.screen) {
|
|
3103
|
-
return
|
|
3400
|
+
return fail14(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
3104
3401
|
}
|
|
3105
3402
|
const shot = await captureIosFramebuffer();
|
|
3106
3403
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
3107
|
-
return
|
|
3404
|
+
return fail14(shot.error ?? "Screenshot capture failed.");
|
|
3108
3405
|
}
|
|
3109
3406
|
const pxScale = shot.width / located.screen.width;
|
|
3110
3407
|
const r = located.rect;
|
|
@@ -3120,7 +3417,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
3120
3417
|
}
|
|
3121
3418
|
);
|
|
3122
3419
|
if (!crop.ok || !crop.buffer) {
|
|
3123
|
-
return
|
|
3420
|
+
return fail14(crop.error ?? "Crop failed.");
|
|
3124
3421
|
}
|
|
3125
3422
|
const label = located.componentName || "component";
|
|
3126
3423
|
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
@@ -3133,21 +3430,21 @@ function registerScreenshotTools(server, sync) {
|
|
|
3133
3430
|
]
|
|
3134
3431
|
};
|
|
3135
3432
|
} catch (e) {
|
|
3136
|
-
return
|
|
3433
|
+
return fail14(errMsg14(e));
|
|
3137
3434
|
}
|
|
3138
3435
|
}
|
|
3139
3436
|
);
|
|
3140
3437
|
}
|
|
3141
3438
|
|
|
3142
3439
|
// src/tools/packs/interact.ts
|
|
3143
|
-
var
|
|
3440
|
+
var import_zod15 = require("zod");
|
|
3144
3441
|
var TOOL_ID2 = "highlight-updates";
|
|
3145
|
-
var
|
|
3146
|
-
var
|
|
3442
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3443
|
+
var fail15 = (message) => ({
|
|
3147
3444
|
isError: true,
|
|
3148
3445
|
content: [{ type: "text", text: message }]
|
|
3149
3446
|
});
|
|
3150
|
-
var
|
|
3447
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3151
3448
|
function formatElement(e) {
|
|
3152
3449
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3153
3450
|
line += e.name;
|
|
@@ -3170,8 +3467,8 @@ function registerInteractTools(server, sync) {
|
|
|
3170
3467
|
{
|
|
3171
3468
|
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.",
|
|
3172
3469
|
inputSchema: {
|
|
3173
|
-
interactiveOnly:
|
|
3174
|
-
deviceId:
|
|
3470
|
+
interactiveOnly: import_zod15.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3471
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3175
3472
|
}
|
|
3176
3473
|
},
|
|
3177
3474
|
async (args) => {
|
|
@@ -3181,16 +3478,16 @@ function registerInteractTools(server, sync) {
|
|
|
3181
3478
|
const all = res?.elements ?? [];
|
|
3182
3479
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3183
3480
|
if (elements.length === 0) {
|
|
3184
|
-
return
|
|
3481
|
+
return text13(
|
|
3185
3482
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3186
3483
|
);
|
|
3187
3484
|
}
|
|
3188
3485
|
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 }).`;
|
|
3189
|
-
return
|
|
3486
|
+
return text13(`${header}
|
|
3190
3487
|
|
|
3191
3488
|
${elements.map(formatElement).join("\n")}`);
|
|
3192
3489
|
} catch (e) {
|
|
3193
|
-
return
|
|
3490
|
+
return fail15(errMsg15(e));
|
|
3194
3491
|
}
|
|
3195
3492
|
}
|
|
3196
3493
|
);
|
|
@@ -3199,20 +3496,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3199
3496
|
{
|
|
3200
3497
|
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.",
|
|
3201
3498
|
inputSchema: {
|
|
3202
|
-
nativeTag:
|
|
3203
|
-
testID:
|
|
3204
|
-
query:
|
|
3205
|
-
value:
|
|
3206
|
-
text:
|
|
3207
|
-
longPress:
|
|
3208
|
-
scrollIntoView:
|
|
3209
|
-
deviceId:
|
|
3499
|
+
nativeTag: import_zod15.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3500
|
+
testID: import_zod15.z.string().optional().describe("Exact testID of the element."),
|
|
3501
|
+
query: import_zod15.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3502
|
+
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)."),
|
|
3503
|
+
text: import_zod15.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3504
|
+
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."),
|
|
3505
|
+
scrollIntoView: import_zod15.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3506
|
+
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3210
3507
|
}
|
|
3211
3508
|
},
|
|
3212
3509
|
async (args) => {
|
|
3213
3510
|
try {
|
|
3214
3511
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3215
|
-
return
|
|
3512
|
+
return fail15("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3216
3513
|
}
|
|
3217
3514
|
const target = sync.requireProDevice(args.deviceId);
|
|
3218
3515
|
const res = await sync.callAction(
|
|
@@ -3237,26 +3534,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3237
3534
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3238
3535
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3239
3536
|
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}`;
|
|
3240
|
-
return
|
|
3537
|
+
return text13(`${verb}${scrolledNote}.${flowTip}`);
|
|
3241
3538
|
}
|
|
3242
3539
|
let msg = res?.reason ?? "Tap failed.";
|
|
3243
3540
|
if (res?.candidates?.length) {
|
|
3244
3541
|
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");
|
|
3245
3542
|
}
|
|
3246
|
-
return
|
|
3543
|
+
return fail15(msg);
|
|
3247
3544
|
} catch (e) {
|
|
3248
|
-
return
|
|
3545
|
+
return fail15(errMsg15(e));
|
|
3249
3546
|
}
|
|
3250
3547
|
}
|
|
3251
3548
|
);
|
|
3252
3549
|
}
|
|
3253
3550
|
|
|
3254
3551
|
// src/tools/packs/jsThread.ts
|
|
3255
|
-
var
|
|
3552
|
+
var import_zod16 = require("zod");
|
|
3256
3553
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3257
|
-
var
|
|
3258
|
-
var
|
|
3259
|
-
var
|
|
3554
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3555
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3556
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3260
3557
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3261
3558
|
function sparkline(values) {
|
|
3262
3559
|
const max = Math.max(1, ...values);
|
|
@@ -3327,9 +3624,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3327
3624
|
{
|
|
3328
3625
|
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.',
|
|
3329
3626
|
inputSchema: {
|
|
3330
|
-
deviceId:
|
|
3331
|
-
durationMs:
|
|
3332
|
-
clearFirst:
|
|
3627
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3628
|
+
durationMs: import_zod16.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
3629
|
+
clearFirst: import_zod16.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3333
3630
|
}
|
|
3334
3631
|
},
|
|
3335
3632
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3342,7 +3639,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3342
3639
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3343
3640
|
target
|
|
3344
3641
|
);
|
|
3345
|
-
if (!snap) return
|
|
3642
|
+
if (!snap) return fail16("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3346
3643
|
const md = formatTopMarkdown(snap, duration);
|
|
3347
3644
|
const slim = {
|
|
3348
3645
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3361,13 +3658,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3361
3658
|
worstFreezeMs: r.worstFreezeMs
|
|
3362
3659
|
}))
|
|
3363
3660
|
};
|
|
3364
|
-
return
|
|
3661
|
+
return text14(`${md}
|
|
3365
3662
|
|
|
3366
3663
|
\`\`\`json
|
|
3367
3664
|
${JSON.stringify(slim)}
|
|
3368
3665
|
\`\`\``);
|
|
3369
3666
|
} catch (e) {
|
|
3370
|
-
return
|
|
3667
|
+
return fail16(errMsg16(e));
|
|
3371
3668
|
}
|
|
3372
3669
|
}
|
|
3373
3670
|
);
|
|
@@ -3376,8 +3673,8 @@ ${JSON.stringify(slim)}
|
|
|
3376
3673
|
{
|
|
3377
3674
|
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).',
|
|
3378
3675
|
inputSchema: {
|
|
3379
|
-
deviceId:
|
|
3380
|
-
key:
|
|
3676
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3677
|
+
key: import_zod16.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3381
3678
|
}
|
|
3382
3679
|
},
|
|
3383
3680
|
async ({ deviceId, key }) => {
|
|
@@ -3390,18 +3687,18 @@ ${JSON.stringify(slim)}
|
|
|
3390
3687
|
target
|
|
3391
3688
|
);
|
|
3392
3689
|
if (!detail) {
|
|
3393
|
-
return
|
|
3690
|
+
return fail16(
|
|
3394
3691
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3395
3692
|
);
|
|
3396
3693
|
}
|
|
3397
3694
|
const md = formatDetailMarkdown(detail);
|
|
3398
|
-
return
|
|
3695
|
+
return text14(`${md}
|
|
3399
3696
|
|
|
3400
3697
|
\`\`\`json
|
|
3401
3698
|
${JSON.stringify(detail)}
|
|
3402
3699
|
\`\`\``);
|
|
3403
3700
|
} catch (e) {
|
|
3404
|
-
return
|
|
3701
|
+
return fail16(errMsg16(e));
|
|
3405
3702
|
}
|
|
3406
3703
|
}
|
|
3407
3704
|
);
|
|
@@ -3410,27 +3707,27 @@ ${JSON.stringify(detail)}
|
|
|
3410
3707
|
{
|
|
3411
3708
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3412
3709
|
inputSchema: {
|
|
3413
|
-
deviceId:
|
|
3710
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3414
3711
|
}
|
|
3415
3712
|
},
|
|
3416
3713
|
async ({ deviceId }) => {
|
|
3417
3714
|
try {
|
|
3418
3715
|
const target = sync.requireProDevice(deviceId);
|
|
3419
3716
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3420
|
-
return
|
|
3717
|
+
return text14("Cleared js-top stats.");
|
|
3421
3718
|
} catch (e) {
|
|
3422
|
-
return
|
|
3719
|
+
return fail16(errMsg16(e));
|
|
3423
3720
|
}
|
|
3424
3721
|
}
|
|
3425
3722
|
);
|
|
3426
3723
|
}
|
|
3427
3724
|
|
|
3428
3725
|
// src/tools/packs/images.ts
|
|
3429
|
-
var
|
|
3726
|
+
var import_zod17 = require("zod");
|
|
3430
3727
|
var IMAGES_TOOL_ID = "images";
|
|
3431
|
-
var
|
|
3432
|
-
var
|
|
3433
|
-
var
|
|
3728
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3729
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3730
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3434
3731
|
function uriTail(uri) {
|
|
3435
3732
|
if (!uri) return "(no source)";
|
|
3436
3733
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -3518,9 +3815,9 @@ function registerImagesTools(server, sync) {
|
|
|
3518
3815
|
{
|
|
3519
3816
|
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.",
|
|
3520
3817
|
inputSchema: {
|
|
3521
|
-
deviceId:
|
|
3522
|
-
limit:
|
|
3523
|
-
status:
|
|
3818
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3819
|
+
limit: import_zod17.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
3820
|
+
status: import_zod17.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3524
3821
|
}
|
|
3525
3822
|
},
|
|
3526
3823
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3533,7 +3830,7 @@ function registerImagesTools(server, sync) {
|
|
|
3533
3830
|
{ limit: capped, status },
|
|
3534
3831
|
target
|
|
3535
3832
|
);
|
|
3536
|
-
if (!list) return
|
|
3833
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3537
3834
|
const md = formatListMarkdown(list, capped);
|
|
3538
3835
|
const slim = {
|
|
3539
3836
|
stats: list.stats,
|
|
@@ -3560,13 +3857,13 @@ function registerImagesTools(server, sync) {
|
|
|
3560
3857
|
layoutShifts: r.layoutShifts
|
|
3561
3858
|
}))
|
|
3562
3859
|
};
|
|
3563
|
-
return
|
|
3860
|
+
return text15(`${md}
|
|
3564
3861
|
|
|
3565
3862
|
\`\`\`json
|
|
3566
3863
|
${JSON.stringify(slim)}
|
|
3567
3864
|
\`\`\``);
|
|
3568
3865
|
} catch (e) {
|
|
3569
|
-
return
|
|
3866
|
+
return fail17(errMsg17(e));
|
|
3570
3867
|
}
|
|
3571
3868
|
}
|
|
3572
3869
|
);
|
|
@@ -3575,9 +3872,9 @@ ${JSON.stringify(slim)}
|
|
|
3575
3872
|
{
|
|
3576
3873
|
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.",
|
|
3577
3874
|
inputSchema: {
|
|
3578
|
-
deviceId:
|
|
3579
|
-
id:
|
|
3580
|
-
action:
|
|
3875
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3876
|
+
id: import_zod17.z.number().describe("Record id from get_images."),
|
|
3877
|
+
action: import_zod17.z.enum([
|
|
3581
3878
|
"hard_reload",
|
|
3582
3879
|
"retry",
|
|
3583
3880
|
"evict_disk",
|
|
@@ -3589,7 +3886,7 @@ ${JSON.stringify(slim)}
|
|
|
3589
3886
|
"swap_url",
|
|
3590
3887
|
"restore"
|
|
3591
3888
|
]).describe("What to do."),
|
|
3592
|
-
url:
|
|
3889
|
+
url: import_zod17.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3593
3890
|
}
|
|
3594
3891
|
},
|
|
3595
3892
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3597,8 +3894,8 @@ ${JSON.stringify(slim)}
|
|
|
3597
3894
|
const target = sync.requireProDevice(deviceId);
|
|
3598
3895
|
if (action === "prove_savings") {
|
|
3599
3896
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3600
|
-
if (!savings) return
|
|
3601
|
-
if (!savings.ok) return
|
|
3897
|
+
if (!savings) return fail17("Device returned nothing for proveSavings.");
|
|
3898
|
+
if (!savings.ok) return fail17(savings.message);
|
|
3602
3899
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3603
3900
|
if (savings.originalBytes != null)
|
|
3604
3901
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3610,11 +3907,11 @@ ${JSON.stringify(slim)}
|
|
|
3610
3907
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3611
3908
|
if (savings.optimizedUri)
|
|
3612
3909
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3613
|
-
return
|
|
3910
|
+
return text15(parts.join("\n"));
|
|
3614
3911
|
}
|
|
3615
3912
|
const entry = ACTION_MAP[action];
|
|
3616
|
-
if (!entry) return
|
|
3617
|
-
if (action === "swap_url" && !url) return
|
|
3913
|
+
if (!entry) return fail17(`Unknown action: ${action}`);
|
|
3914
|
+
if (action === "swap_url" && !url) return fail17("swap_url requires the `url` param.");
|
|
3618
3915
|
const result = await sync.callAction(
|
|
3619
3916
|
IMAGES_TOOL_ID,
|
|
3620
3917
|
entry.action,
|
|
@@ -3634,9 +3931,9 @@ ${JSON.stringify(slim)}
|
|
|
3634
3931
|
);
|
|
3635
3932
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3636
3933
|
}
|
|
3637
|
-
return
|
|
3934
|
+
return text15(lines.join("\n"));
|
|
3638
3935
|
} catch (e) {
|
|
3639
|
-
return
|
|
3936
|
+
return fail17(errMsg17(e));
|
|
3640
3937
|
}
|
|
3641
3938
|
}
|
|
3642
3939
|
);
|
|
@@ -3645,12 +3942,12 @@ ${JSON.stringify(slim)}
|
|
|
3645
3942
|
{
|
|
3646
3943
|
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.",
|
|
3647
3944
|
inputSchema: {
|
|
3648
|
-
deviceId:
|
|
3649
|
-
network:
|
|
3650
|
-
blankImages:
|
|
3651
|
-
massAction:
|
|
3652
|
-
clearRegistry:
|
|
3653
|
-
clearExpoCaches:
|
|
3945
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3946
|
+
network: import_zod17.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3947
|
+
blankImages: import_zod17.z.boolean().optional().describe("Render all images without a source."),
|
|
3948
|
+
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."),
|
|
3949
|
+
clearRegistry: import_zod17.z.boolean().optional().describe("Clear the captured image log."),
|
|
3950
|
+
clearExpoCaches: import_zod17.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3654
3951
|
}
|
|
3655
3952
|
},
|
|
3656
3953
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3677,17 +3974,17 @@ ${JSON.stringify(slim)}
|
|
|
3677
3974
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3678
3975
|
done.push(r?.message ?? "expo caches cleared");
|
|
3679
3976
|
}
|
|
3680
|
-
if (done.length === 0) return
|
|
3681
|
-
return
|
|
3977
|
+
if (done.length === 0) return fail17("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3978
|
+
return text15(`Applied: ${done.join(" \xB7 ")}`);
|
|
3682
3979
|
} catch (e) {
|
|
3683
|
-
return
|
|
3980
|
+
return fail17(errMsg17(e));
|
|
3684
3981
|
}
|
|
3685
3982
|
}
|
|
3686
3983
|
);
|
|
3687
3984
|
}
|
|
3688
3985
|
|
|
3689
3986
|
// src/tools/packs/assets.ts
|
|
3690
|
-
var
|
|
3987
|
+
var import_zod18 = require("zod");
|
|
3691
3988
|
|
|
3692
3989
|
// src/tools/packs/assetsGraphNote.ts
|
|
3693
3990
|
function graphUnavailableNote(ss) {
|
|
@@ -3702,9 +3999,9 @@ function graphUnavailableNote(ss) {
|
|
|
3702
3999
|
|
|
3703
4000
|
// src/tools/packs/assets.ts
|
|
3704
4001
|
var ASSETS_TOOL_ID = "assets";
|
|
3705
|
-
var
|
|
3706
|
-
var
|
|
3707
|
-
var
|
|
4002
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4003
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4004
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3708
4005
|
function kb2(bytes) {
|
|
3709
4006
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3710
4007
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -3792,10 +4089,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3792
4089
|
{
|
|
3793
4090
|
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.",
|
|
3794
4091
|
inputSchema: {
|
|
3795
|
-
deviceId:
|
|
3796
|
-
limit:
|
|
3797
|
-
kind:
|
|
3798
|
-
unusedOnly:
|
|
4092
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4093
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4094
|
+
kind: import_zod18.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4095
|
+
unusedOnly: import_zod18.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3799
4096
|
}
|
|
3800
4097
|
},
|
|
3801
4098
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3808,7 +4105,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3808
4105
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3809
4106
|
target
|
|
3810
4107
|
);
|
|
3811
|
-
if (!list) return
|
|
4108
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3812
4109
|
if (unusedOnly) {
|
|
3813
4110
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3814
4111
|
}
|
|
@@ -3832,13 +4129,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3832
4129
|
sizeSource: r.sizeSource
|
|
3833
4130
|
}))
|
|
3834
4131
|
};
|
|
3835
|
-
return
|
|
4132
|
+
return text16(`${md}
|
|
3836
4133
|
|
|
3837
4134
|
\`\`\`json
|
|
3838
4135
|
${JSON.stringify(slim)}
|
|
3839
4136
|
\`\`\``);
|
|
3840
4137
|
} catch (e) {
|
|
3841
|
-
return
|
|
4138
|
+
return fail18(errMsg18(e));
|
|
3842
4139
|
}
|
|
3843
4140
|
}
|
|
3844
4141
|
);
|
|
@@ -3847,9 +4144,9 @@ ${JSON.stringify(slim)}
|
|
|
3847
4144
|
{
|
|
3848
4145
|
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).",
|
|
3849
4146
|
inputSchema: {
|
|
3850
|
-
deviceId:
|
|
3851
|
-
action:
|
|
3852
|
-
id:
|
|
4147
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4148
|
+
action: import_zod18.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4149
|
+
id: import_zod18.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3853
4150
|
}
|
|
3854
4151
|
},
|
|
3855
4152
|
async ({ deviceId, action, id }) => {
|
|
@@ -3859,19 +4156,19 @@ ${JSON.stringify(slim)}
|
|
|
3859
4156
|
case "rescan": {
|
|
3860
4157
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3861
4158
|
const ss = result?.scanStatus;
|
|
3862
|
-
if (!ss) return
|
|
3863
|
-
return
|
|
4159
|
+
if (!ss) return fail18("Device returned nothing for rescan.");
|
|
4160
|
+
return text16(
|
|
3864
4161
|
`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" : "")
|
|
3865
4162
|
);
|
|
3866
4163
|
}
|
|
3867
4164
|
case "measure_sizes": {
|
|
3868
4165
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3869
|
-
return
|
|
4166
|
+
return text16(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3870
4167
|
}
|
|
3871
4168
|
case "detail": {
|
|
3872
|
-
if (id == null) return
|
|
4169
|
+
if (id == null) return fail18("detail requires the `id` param (from get_assets).");
|
|
3873
4170
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3874
|
-
if (!detail) return
|
|
4171
|
+
if (!detail) return fail18(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3875
4172
|
const lines = [
|
|
3876
4173
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3877
4174
|
"",
|
|
@@ -3889,32 +4186,32 @@ ${JSON.stringify(slim)}
|
|
|
3889
4186
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3890
4187
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3891
4188
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3892
|
-
return
|
|
4189
|
+
return text16(lines.join("\n"));
|
|
3893
4190
|
}
|
|
3894
4191
|
case "save_baseline": {
|
|
3895
4192
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3896
|
-
return
|
|
4193
|
+
return text16(
|
|
3897
4194
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3898
4195
|
);
|
|
3899
4196
|
}
|
|
3900
4197
|
case "clear_baseline": {
|
|
3901
4198
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3902
|
-
return
|
|
4199
|
+
return text16("Baseline cleared.");
|
|
3903
4200
|
}
|
|
3904
4201
|
case "clear_records": {
|
|
3905
4202
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3906
|
-
return
|
|
4203
|
+
return text16(result?.message ?? "Inventory cleared.");
|
|
3907
4204
|
}
|
|
3908
4205
|
}
|
|
3909
4206
|
} catch (e) {
|
|
3910
|
-
return
|
|
4207
|
+
return fail18(errMsg18(e));
|
|
3911
4208
|
}
|
|
3912
4209
|
}
|
|
3913
4210
|
);
|
|
3914
4211
|
}
|
|
3915
4212
|
|
|
3916
4213
|
// src/tools/packs/reload.ts
|
|
3917
|
-
var
|
|
4214
|
+
var import_zod19 = require("zod");
|
|
3918
4215
|
|
|
3919
4216
|
// src/capture/devServerReload.ts
|
|
3920
4217
|
var import_ws = require("ws");
|
|
@@ -3977,9 +4274,9 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
3977
4274
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3978
4275
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3979
4276
|
var PROBE_INTERVAL_MS = 400;
|
|
3980
|
-
var
|
|
3981
|
-
var
|
|
3982
|
-
var
|
|
4277
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4278
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4279
|
+
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
3983
4280
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3984
4281
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3985
4282
|
try {
|
|
@@ -4018,12 +4315,12 @@ function registerReloadTools(server, sync) {
|
|
|
4018
4315
|
{
|
|
4019
4316
|
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.",
|
|
4020
4317
|
inputSchema: {
|
|
4021
|
-
strategy:
|
|
4318
|
+
strategy: import_zod19.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4022
4319
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4023
4320
|
),
|
|
4024
|
-
wait:
|
|
4025
|
-
timeoutMs:
|
|
4026
|
-
deviceId:
|
|
4321
|
+
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."),
|
|
4322
|
+
timeoutMs: import_zod19.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4323
|
+
deviceId: import_zod19.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4027
4324
|
}
|
|
4028
4325
|
},
|
|
4029
4326
|
async (args) => {
|
|
@@ -4065,32 +4362,32 @@ function registerReloadTools(server, sync) {
|
|
|
4065
4362
|
}
|
|
4066
4363
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4067
4364
|
if (args.wait === false) {
|
|
4068
|
-
return
|
|
4365
|
+
return text17(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4069
4366
|
}
|
|
4070
4367
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4071
4368
|
if (elapsed == null) {
|
|
4072
|
-
return
|
|
4369
|
+
return fail19(
|
|
4073
4370
|
`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.`
|
|
4074
4371
|
);
|
|
4075
4372
|
}
|
|
4076
|
-
return
|
|
4373
|
+
return text17(
|
|
4077
4374
|
`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.`
|
|
4078
4375
|
);
|
|
4079
4376
|
} catch (e) {
|
|
4080
|
-
return
|
|
4377
|
+
return fail19(errMsg19(e));
|
|
4081
4378
|
}
|
|
4082
4379
|
}
|
|
4083
4380
|
);
|
|
4084
4381
|
}
|
|
4085
4382
|
|
|
4086
4383
|
// src/tools/packs/triage.ts
|
|
4087
|
-
var
|
|
4088
|
-
var
|
|
4089
|
-
var
|
|
4384
|
+
var import_zod20 = require("zod");
|
|
4385
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4386
|
+
var fail20 = (message) => ({
|
|
4090
4387
|
isError: true,
|
|
4091
4388
|
content: [{ type: "text", text: message }]
|
|
4092
4389
|
});
|
|
4093
|
-
var
|
|
4390
|
+
var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
|
|
4094
4391
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4095
4392
|
var MAX_STORAGE_KEYS = 20;
|
|
4096
4393
|
var MAX_STORAGE_VALUE = 400;
|
|
@@ -4127,7 +4424,7 @@ async function section(title, run) {
|
|
|
4127
4424
|
return body ? `${title}
|
|
4128
4425
|
${body}` : null;
|
|
4129
4426
|
} catch (e) {
|
|
4130
|
-
const msg =
|
|
4427
|
+
const msg = errMsg20(e);
|
|
4131
4428
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4132
4429
|
return `${title}
|
|
4133
4430
|
(no answer \u2014 see the note above)`;
|
|
@@ -4142,7 +4439,7 @@ function registerTriageTools(server, sync) {
|
|
|
4142
4439
|
{
|
|
4143
4440
|
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.",
|
|
4144
4441
|
inputSchema: {
|
|
4145
|
-
deviceId:
|
|
4442
|
+
deviceId: import_zod20.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4146
4443
|
}
|
|
4147
4444
|
},
|
|
4148
4445
|
async (args) => {
|
|
@@ -4249,7 +4546,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4249
4546
|
|
|
4250
4547
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4251
4548
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4252
|
-
return
|
|
4549
|
+
return text18(
|
|
4253
4550
|
`${head}${banner}
|
|
4254
4551
|
|
|
4255
4552
|
${body}
|
|
@@ -4257,22 +4554,22 @@ ${body}
|
|
|
4257
4554
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4258
4555
|
);
|
|
4259
4556
|
} catch (e) {
|
|
4260
|
-
return
|
|
4557
|
+
return fail20(errMsg20(e));
|
|
4261
4558
|
}
|
|
4262
4559
|
}
|
|
4263
4560
|
);
|
|
4264
4561
|
}
|
|
4265
4562
|
|
|
4266
4563
|
// src/tools/packs/flow.ts
|
|
4267
|
-
var
|
|
4564
|
+
var import_zod21 = require("zod");
|
|
4268
4565
|
var UI_TOOL_ID = "highlight-updates";
|
|
4269
4566
|
var APP_TOOL_ID2 = "app";
|
|
4270
|
-
var
|
|
4271
|
-
var
|
|
4567
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4568
|
+
var fail21 = (message) => ({
|
|
4272
4569
|
isError: true,
|
|
4273
4570
|
content: [{ type: "text", text: message }]
|
|
4274
4571
|
});
|
|
4275
|
-
var
|
|
4572
|
+
var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
|
|
4276
4573
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4277
4574
|
var POLL_MS = 250;
|
|
4278
4575
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4289,19 +4586,19 @@ function formatElement2(e) {
|
|
|
4289
4586
|
const display = e.text ?? e.label;
|
|
4290
4587
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4291
4588
|
}
|
|
4292
|
-
var stepSchema =
|
|
4293
|
-
tap:
|
|
4294
|
-
longPress:
|
|
4295
|
-
type:
|
|
4296
|
-
into:
|
|
4297
|
-
set:
|
|
4298
|
-
reload:
|
|
4299
|
-
waitMs:
|
|
4300
|
-
waitFor:
|
|
4301
|
-
waitGone:
|
|
4302
|
-
expect:
|
|
4303
|
-
expectGone:
|
|
4304
|
-
timeoutMs:
|
|
4589
|
+
var stepSchema = import_zod21.z.object({
|
|
4590
|
+
tap: import_zod21.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4591
|
+
longPress: import_zod21.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4592
|
+
type: import_zod21.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4593
|
+
into: import_zod21.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4594
|
+
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."),
|
|
4595
|
+
reload: import_zod21.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4596
|
+
waitMs: import_zod21.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4597
|
+
waitFor: import_zod21.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4598
|
+
waitGone: import_zod21.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4599
|
+
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."),
|
|
4600
|
+
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."),
|
|
4601
|
+
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).")
|
|
4305
4602
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4306
4603
|
function registerFlowTools(server, sync) {
|
|
4307
4604
|
server.registerTool(
|
|
@@ -4309,10 +4606,10 @@ function registerFlowTools(server, sync) {
|
|
|
4309
4606
|
{
|
|
4310
4607
|
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.",
|
|
4311
4608
|
inputSchema: {
|
|
4312
|
-
steps:
|
|
4313
|
-
include:
|
|
4314
|
-
continueOnFailure:
|
|
4315
|
-
deviceId:
|
|
4609
|
+
steps: import_zod21.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4610
|
+
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."),
|
|
4611
|
+
continueOnFailure: import_zod21.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4612
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4316
4613
|
}
|
|
4317
4614
|
},
|
|
4318
4615
|
async (args) => {
|
|
@@ -4484,7 +4781,7 @@ function registerFlowTools(server, sync) {
|
|
|
4484
4781
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
4485
4782
|
}
|
|
4486
4783
|
} catch (e) {
|
|
4487
|
-
stepErrors.push(
|
|
4784
|
+
stepErrors.push(errMsg21(e));
|
|
4488
4785
|
}
|
|
4489
4786
|
if (stepErrors.length) {
|
|
4490
4787
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -4507,7 +4804,7 @@ function registerFlowTools(server, sync) {
|
|
|
4507
4804
|
${shown.map(formatElement2).join("\n")}`
|
|
4508
4805
|
);
|
|
4509
4806
|
} catch (e) {
|
|
4510
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
4807
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
|
|
4511
4808
|
}
|
|
4512
4809
|
}
|
|
4513
4810
|
if (include === "all") {
|
|
@@ -4536,9 +4833,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4536
4833
|
}
|
|
4537
4834
|
}
|
|
4538
4835
|
}
|
|
4539
|
-
return
|
|
4836
|
+
return text19(parts.join("\n\n"));
|
|
4540
4837
|
} catch (e) {
|
|
4541
|
-
return
|
|
4838
|
+
return fail21(errMsg21(e));
|
|
4542
4839
|
}
|
|
4543
4840
|
}
|
|
4544
4841
|
);
|
|
@@ -4559,25 +4856,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4559
4856
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4560
4857
|
|
|
4561
4858
|
// src/tools/packs/renders.ts
|
|
4562
|
-
var
|
|
4859
|
+
var import_zod22 = require("zod");
|
|
4563
4860
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4564
|
-
var
|
|
4565
|
-
var
|
|
4861
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4862
|
+
var fail22 = (message) => ({
|
|
4566
4863
|
isError: true,
|
|
4567
4864
|
content: [{ type: "text", text: message }]
|
|
4568
4865
|
});
|
|
4569
|
-
var
|
|
4866
|
+
var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
|
|
4570
4867
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4571
4868
|
var DEFAULT_IDLE_MS = 3e3;
|
|
4572
4869
|
var SETTLE_MS = 400;
|
|
4573
4870
|
var DEFAULT_TOP = 12;
|
|
4574
4871
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4575
|
-
var stepSchema2 =
|
|
4576
|
-
tap:
|
|
4577
|
-
type:
|
|
4578
|
-
into:
|
|
4579
|
-
waitMs:
|
|
4580
|
-
repeat:
|
|
4872
|
+
var stepSchema2 = import_zod22.z.object({
|
|
4873
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4874
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4875
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4876
|
+
waitMs: import_zod22.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
4877
|
+
repeat: import_zod22.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4581
4878
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4582
4879
|
function selector2(needle) {
|
|
4583
4880
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4670,14 +4967,14 @@ function registerRenderTools(server, sync) {
|
|
|
4670
4967
|
{
|
|
4671
4968
|
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.",
|
|
4672
4969
|
inputSchema: {
|
|
4673
|
-
steps:
|
|
4970
|
+
steps: import_zod22.z.array(stepSchema2).max(20).optional().describe(
|
|
4674
4971
|
"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."
|
|
4675
4972
|
),
|
|
4676
|
-
idleMs:
|
|
4677
|
-
label:
|
|
4678
|
-
compareToPrevious:
|
|
4679
|
-
top:
|
|
4680
|
-
deviceId:
|
|
4973
|
+
idleMs: import_zod22.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
4974
|
+
label: import_zod22.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
4975
|
+
compareToPrevious: import_zod22.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
4976
|
+
top: import_zod22.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
4977
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4681
4978
|
}
|
|
4682
4979
|
},
|
|
4683
4980
|
async (args) => {
|
|
@@ -4686,18 +4983,18 @@ function registerRenderTools(server, sync) {
|
|
|
4686
4983
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4687
4984
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4688
4985
|
if (!uiTool) {
|
|
4689
|
-
return
|
|
4986
|
+
return fail22(
|
|
4690
4987
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4691
4988
|
);
|
|
4692
4989
|
}
|
|
4693
4990
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4694
|
-
return
|
|
4991
|
+
return fail22(
|
|
4695
4992
|
"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."
|
|
4696
4993
|
);
|
|
4697
4994
|
}
|
|
4698
4995
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4699
4996
|
if (began && began.ok === false) {
|
|
4700
|
-
return
|
|
4997
|
+
return fail22(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4701
4998
|
}
|
|
4702
4999
|
await delay5(SETTLE_MS);
|
|
4703
5000
|
const startedAt = Date.now();
|
|
@@ -4742,7 +5039,7 @@ function registerRenderTools(server, sync) {
|
|
|
4742
5039
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
4743
5040
|
const summary = ended?.summary;
|
|
4744
5041
|
if (!summary) {
|
|
4745
|
-
return
|
|
5042
|
+
return fail22(
|
|
4746
5043
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
4747
5044
|
);
|
|
4748
5045
|
}
|
|
@@ -4767,9 +5064,9 @@ function registerRenderTools(server, sync) {
|
|
|
4767
5064
|
} else if (!args.compareToPrevious) {
|
|
4768
5065
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
4769
5066
|
}
|
|
4770
|
-
return
|
|
5067
|
+
return text20(parts.join("\n"));
|
|
4771
5068
|
} catch (e) {
|
|
4772
|
-
return
|
|
5069
|
+
return fail22(errMsg22(e));
|
|
4773
5070
|
}
|
|
4774
5071
|
}
|
|
4775
5072
|
);
|
|
@@ -4990,6 +5287,7 @@ require the connected app to have an active Buoy Pro license. If they error with
|
|
|
4990
5287
|
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
4991
5288
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
4992
5289
|
"measure_renders",
|
|
5290
|
+
"scenario_action",
|
|
4993
5291
|
"run_flow",
|
|
4994
5292
|
"run_benchmark_batch",
|
|
4995
5293
|
"get_triage",
|
|
@@ -5040,6 +5338,7 @@ function createMcpServer(sync) {
|
|
|
5040
5338
|
registerRoutesTools(server, sync);
|
|
5041
5339
|
registerConsoleTools(server, sync);
|
|
5042
5340
|
registerTimeMachineTools(server, sync);
|
|
5341
|
+
registerScenariosTools(server, sync);
|
|
5043
5342
|
registerScreenshotTools(server, sync);
|
|
5044
5343
|
registerInteractTools(server, sync);
|
|
5045
5344
|
registerJsThreadTools(server, sync);
|
|
@@ -5049,7 +5348,7 @@ function createMcpServer(sync) {
|
|
|
5049
5348
|
return server;
|
|
5050
5349
|
}
|
|
5051
5350
|
var deviceIdField = {
|
|
5052
|
-
deviceId:
|
|
5351
|
+
deviceId: import_zod23.z.string().optional().describe(
|
|
5053
5352
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5054
5353
|
)
|
|
5055
5354
|
};
|