@buoy-gg/mcp 7.0.30 → 7.0.32
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/README.md +13 -1
- package/dist/{chunk-POUWWZXB.mjs → chunk-HWYVQNMR.mjs} +883 -203
- package/dist/cli.js +915 -235
- package/dist/cli.mjs +1 -1
- package/dist/index.js +915 -235
- 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_zod24 = 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.32" ? "7.0.32" : "0.0.0-dev";
|
|
690
690
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
691
691
|
|
|
692
692
|
// src/update/checkForUpdate.ts
|
|
@@ -1419,7 +1419,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1419
1419
|
let sawActive = false;
|
|
1420
1420
|
let idleStreak = 0;
|
|
1421
1421
|
let lastCount = -1;
|
|
1422
|
-
let
|
|
1422
|
+
let resolved2 = null;
|
|
1423
1423
|
while (Date.now() - start < cap3) {
|
|
1424
1424
|
let snap2;
|
|
1425
1425
|
try {
|
|
@@ -1431,7 +1431,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1431
1431
|
const completed = snap2.live?.automationCompleted;
|
|
1432
1432
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1433
1433
|
if (completed?.phase === "done") {
|
|
1434
|
-
|
|
1434
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1435
1435
|
break;
|
|
1436
1436
|
}
|
|
1437
1437
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1441,11 +1441,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1441
1441
|
const count = runCountFor(snap2.index, newest);
|
|
1442
1442
|
if (count === lastCount) {
|
|
1443
1443
|
if (sawActive && ++idleStreak >= 3) {
|
|
1444
|
-
|
|
1444
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1445
1445
|
break;
|
|
1446
1446
|
}
|
|
1447
1447
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1448
|
-
|
|
1448
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1449
1449
|
break;
|
|
1450
1450
|
}
|
|
1451
1451
|
} else {
|
|
@@ -1457,11 +1457,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1457
1457
|
}
|
|
1458
1458
|
await delay2(POLL_INTERVAL_MS);
|
|
1459
1459
|
}
|
|
1460
|
-
if (!
|
|
1460
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1461
1461
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1462
1462
|
});
|
|
1463
1463
|
const snap = await readPerfSnapshot(sync, target);
|
|
1464
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1464
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1465
1465
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1466
1466
|
});
|
|
1467
1467
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -1788,8 +1788,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
1788
1788
|
}
|
|
1789
1789
|
function bodyLines(e, fetched) {
|
|
1790
1790
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
1791
|
-
const label = (inline,
|
|
1792
|
-
const value = inline ??
|
|
1791
|
+
const label = (inline, resolved2, omitted) => {
|
|
1792
|
+
const value = inline ?? resolved2;
|
|
1793
1793
|
if (value !== void 0 && value !== null) return truncate(value);
|
|
1794
1794
|
if (!omitted) return "(none)";
|
|
1795
1795
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -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":
|
|
@@ -3075,9 +3370,9 @@ function reasonMessage(res, query) {
|
|
|
3075
3370
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
3076
3371
|
}
|
|
3077
3372
|
}
|
|
3078
|
-
function candidatesText(
|
|
3079
|
-
if (!
|
|
3080
|
-
const lines =
|
|
3373
|
+
function candidatesText(candidates2) {
|
|
3374
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
3375
|
+
const lines = candidates2.map((c) => {
|
|
3081
3376
|
const id = c.testID || c.nativeID;
|
|
3082
3377
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
3083
3378
|
});
|
|
@@ -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,390 @@ function registerScreenshotTools(server, sync) {
|
|
|
3170
3465
|
]
|
|
3171
3466
|
};
|
|
3172
3467
|
} catch (e) {
|
|
3173
|
-
return
|
|
3468
|
+
return fail14(errMsg14(e));
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/tools/packs/camera.ts
|
|
3475
|
+
var import_zod15 = require("zod");
|
|
3476
|
+
|
|
3477
|
+
// src/capture/camera.ts
|
|
3478
|
+
var import_node_child_process2 = require("child_process");
|
|
3479
|
+
var import_node_util2 = require("util");
|
|
3480
|
+
var import_node_fs3 = require("fs");
|
|
3481
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3482
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3483
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3484
|
+
function candidates() {
|
|
3485
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3486
|
+
const list = [
|
|
3487
|
+
...fromEnv ? [import_node_path3.default.join(fromEnv, "buoycam")] : [],
|
|
3488
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3489
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3490
|
+
];
|
|
3491
|
+
let dir = __dirname;
|
|
3492
|
+
for (let i = 0; i < 8; i++) {
|
|
3493
|
+
list.push(import_node_path3.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3494
|
+
const parent = import_node_path3.default.dirname(dir);
|
|
3495
|
+
if (parent === dir) break;
|
|
3496
|
+
dir = parent;
|
|
3497
|
+
}
|
|
3498
|
+
return list;
|
|
3499
|
+
}
|
|
3500
|
+
var resolved = null;
|
|
3501
|
+
function cliPath() {
|
|
3502
|
+
if (resolved) return resolved;
|
|
3503
|
+
resolved = candidates().find((c) => (0, import_node_fs3.existsSync)(c)) ?? null;
|
|
3504
|
+
return resolved;
|
|
3505
|
+
}
|
|
3506
|
+
async function run(args) {
|
|
3507
|
+
const cli = cliPath();
|
|
3508
|
+
if (!cli) {
|
|
3509
|
+
return {
|
|
3510
|
+
ok: false,
|
|
3511
|
+
error: "The simulator camera components were not found. They ship with the Buoy desktop app \u2014 install it, or set BUOY_SIMCAM_DIR to the directory holding `buoycam`."
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
try {
|
|
3515
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3516
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
const stdout = err.stdout ?? "";
|
|
3519
|
+
try {
|
|
3520
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3521
|
+
} catch {
|
|
3522
|
+
return {
|
|
3523
|
+
ok: false,
|
|
3524
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
function describeDiagnosis(checks) {
|
|
3530
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3531
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3532
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3533
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3534
|
+
}
|
|
3535
|
+
const lines = [];
|
|
3536
|
+
for (const c of [...failed, ...warned]) {
|
|
3537
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3538
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3539
|
+
}
|
|
3540
|
+
const headline = failed.length > 0 ? `${failed.length} problem${failed.length === 1 ? "" : "s"} found.` : `No hard failures, but ${warned.length} thing${warned.length === 1 ? "" : "s"} worth knowing.`;
|
|
3541
|
+
return [headline, "", ...lines].join("\n");
|
|
3542
|
+
}
|
|
3543
|
+
function describeDevices(devices, apps) {
|
|
3544
|
+
if (devices.length === 0) {
|
|
3545
|
+
return "No booted simulators.\n\n_Boot one first \u2014 `xcrun simctl boot <udid>`, or open Simulator.app. The camera attaches to a simulator, so nothing else here works until one is running._";
|
|
3546
|
+
}
|
|
3547
|
+
const lines = [
|
|
3548
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3549
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3550
|
+
];
|
|
3551
|
+
if (devices.length > 1 && apps === null) {
|
|
3552
|
+
lines.push(
|
|
3553
|
+
"",
|
|
3554
|
+
"_More than one is booted, so pass `udid` to list a specific one's apps \u2014 and to every other camera tool that takes one._"
|
|
3555
|
+
);
|
|
3556
|
+
return lines.join("\n");
|
|
3557
|
+
}
|
|
3558
|
+
if (apps === null) {
|
|
3559
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3560
|
+
return lines.join("\n");
|
|
3561
|
+
}
|
|
3562
|
+
if (apps.length === 0) {
|
|
3563
|
+
lines.push("", "No apps installed on it.");
|
|
3564
|
+
return lines.join("\n");
|
|
3565
|
+
}
|
|
3566
|
+
lines.push(
|
|
3567
|
+
"",
|
|
3568
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3569
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3570
|
+
"",
|
|
3571
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3572
|
+
);
|
|
3573
|
+
return lines.join("\n");
|
|
3574
|
+
}
|
|
3575
|
+
function describeInputs(cameras, windows) {
|
|
3576
|
+
const lines = [];
|
|
3577
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3578
|
+
if (cameras.length) {
|
|
3579
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3580
|
+
} else {
|
|
3581
|
+
lines.push(
|
|
3582
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
lines.push("");
|
|
3586
|
+
lines.push(
|
|
3587
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3588
|
+
);
|
|
3589
|
+
if (windows.length) {
|
|
3590
|
+
lines.push(
|
|
3591
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3592
|
+
);
|
|
3593
|
+
} else {
|
|
3594
|
+
lines.push(
|
|
3595
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
return lines.join("\n");
|
|
3599
|
+
}
|
|
3600
|
+
function entitlementPaths() {
|
|
3601
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3602
|
+
if (fromEnv) return [fromEnv];
|
|
3603
|
+
return [
|
|
3604
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3605
|
+
import_node_path3.default.join(import_node_os3.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3606
|
+
];
|
|
3607
|
+
}
|
|
3608
|
+
function readEntitlement() {
|
|
3609
|
+
for (const p of entitlementPaths()) {
|
|
3610
|
+
try {
|
|
3611
|
+
if (!(0, import_node_fs3.existsSync)(p)) continue;
|
|
3612
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf8"));
|
|
3613
|
+
return {
|
|
3614
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3615
|
+
isPro: parsed.isPro === true
|
|
3616
|
+
};
|
|
3617
|
+
} catch {
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return { tier: "anonymous", isPro: false };
|
|
3621
|
+
}
|
|
3622
|
+
function proRequired(tool) {
|
|
3623
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3624
|
+
|
|
3625
|
+
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel without a licence. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
|
|
3626
|
+
|
|
3627
|
+
_Tell the user to enter a licence in Buoy Desktop, or see https://buoy.gg/pricing. Don't try to work around it \u2014 use the desktop panel by hand instead._`;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// src/tools/packs/camera.ts
|
|
3631
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3632
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3633
|
+
var fail15 = (message) => ({
|
|
3634
|
+
isError: true,
|
|
3635
|
+
content: [{ type: "text", text: message }]
|
|
3636
|
+
});
|
|
3637
|
+
function registerCameraTools(server) {
|
|
3638
|
+
server.registerTool(
|
|
3639
|
+
"camera_source",
|
|
3640
|
+
{
|
|
3641
|
+
description: "Give the iOS Simulator a camera, and choose what it shows. Use this before testing anything that opens a camera \u2014 a QR or barcode scanner, document capture, an avatar picker. `barcode` generates the code from text, so testing a scanner needs no printed code and no second device; pdf417 is the barcode on a driver's licence. `screen` points the phone's camera at this Mac's screen. Needs no Buoy integration in the app under test.",
|
|
3642
|
+
inputSchema: {
|
|
3643
|
+
kind: import_zod15.z.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3644
|
+
value: import_zod15.z.string().optional().describe(
|
|
3645
|
+
"Text for `barcode`, a file path for `image`/`video`, a camera uniqueId for `camera` (see `camera_inputs`), or one of `behind` / `display` / `window:<id>` / `region:<id>[:<gutter>]` for `screen`."
|
|
3646
|
+
),
|
|
3647
|
+
format: import_zod15.z.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3648
|
+
}
|
|
3649
|
+
},
|
|
3650
|
+
async (args) => {
|
|
3651
|
+
const blocked = gate("camera_source");
|
|
3652
|
+
if (blocked) return blocked;
|
|
3653
|
+
const cmd = ["source", args.kind];
|
|
3654
|
+
if (args.value) cmd.push(args.value);
|
|
3655
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3656
|
+
const result = await run(cmd);
|
|
3657
|
+
if (!result.ok) {
|
|
3658
|
+
return fail15(
|
|
3659
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
const what = args.kind === "barcode" ? `a ${args.format ?? "qr"} code reading "${args.value}"` : args.kind === "screen" ? `this Mac's screen (${args.value ?? "behind the simulator window"})` : args.value ?? args.kind;
|
|
3663
|
+
return text13(
|
|
3664
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3665
|
+
|
|
3666
|
+
_Apps already running do not pick up a camera they started without \u2014 use \`camera_launch\` to relaunch one, or \`camera_zero_setup\` so that anything the simulator launches gets it._`
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
);
|
|
3670
|
+
server.registerTool(
|
|
3671
|
+
"camera_status",
|
|
3672
|
+
{
|
|
3673
|
+
description: "Check what the simulator camera is doing and what it is currently decoding. Use this to confirm a scanner test is actually seeing a code before concluding the app is broken.",
|
|
3674
|
+
inputSchema: {}
|
|
3675
|
+
},
|
|
3676
|
+
async () => {
|
|
3677
|
+
const blocked = gate("camera_status");
|
|
3678
|
+
if (blocked) return blocked;
|
|
3679
|
+
const result = await run(["status"]);
|
|
3680
|
+
if (!result.ok || !result.data) {
|
|
3681
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3682
|
+
}
|
|
3683
|
+
const { publishing, fps, codes } = result.data;
|
|
3684
|
+
if (!publishing) {
|
|
3685
|
+
return text13(
|
|
3686
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
return text13(
|
|
3690
|
+
[
|
|
3691
|
+
`Publishing at ${fps} fps.`,
|
|
3692
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3693
|
+
"",
|
|
3694
|
+
"_A code shown here is what the app receives. If the app is not reacting to it, the problem is in the app, not the camera \u2014 `camera_diagnose` will say whether the app is even reading the feed._"
|
|
3695
|
+
].join("\n")
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
);
|
|
3699
|
+
server.registerTool(
|
|
3700
|
+
"camera_launch",
|
|
3701
|
+
{
|
|
3702
|
+
description: "Relaunch a simulator app with the camera attached. A camera can only be attached when a process starts, so an app that is already running cannot be given one \u2014 this terminates and relaunches it.",
|
|
3703
|
+
inputSchema: {
|
|
3704
|
+
bundleId: import_zod15.z.string().describe("The app's bundle identifier."),
|
|
3705
|
+
udid: import_zod15.z.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3706
|
+
}
|
|
3707
|
+
},
|
|
3708
|
+
async (args) => {
|
|
3709
|
+
const blocked = gate("camera_launch");
|
|
3710
|
+
if (blocked) return blocked;
|
|
3711
|
+
const cmd = ["launch", args.bundleId];
|
|
3712
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3713
|
+
const result = await run(cmd);
|
|
3714
|
+
if (!result.ok) {
|
|
3715
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3716
|
+
}
|
|
3717
|
+
return text13(
|
|
3718
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
);
|
|
3722
|
+
server.registerTool(
|
|
3723
|
+
"camera_diagnose",
|
|
3724
|
+
{
|
|
3725
|
+
description: "Work out why the simulator camera is not working. Checks that the pieces are present, a simulator is booted, something is publishing, SOMETHING IS READING IT, the library is loaded in the target app, no other tool owns the injection, and no camera library in the project disables simulator cameras at compile time.",
|
|
3726
|
+
inputSchema: {
|
|
3727
|
+
bundleId: import_zod15.z.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3728
|
+
}
|
|
3729
|
+
},
|
|
3730
|
+
async (args) => {
|
|
3731
|
+
const cmd = ["diagnose"];
|
|
3732
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3733
|
+
const result = await run(cmd);
|
|
3734
|
+
if (!result.data?.checks) {
|
|
3735
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3736
|
+
}
|
|
3737
|
+
const ent = readEntitlement();
|
|
3738
|
+
const tierNote = ent.isPro ? "" : "\n\n_This install is on the **free** tier, so the other `camera_*` tools are blocked. The camera still works by hand in the Buoy desktop panel._";
|
|
3739
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3740
|
+
}
|
|
3741
|
+
);
|
|
3742
|
+
server.registerTool(
|
|
3743
|
+
"camera_stop",
|
|
3744
|
+
{
|
|
3745
|
+
description: "Turn the simulator camera off and stop publishing frames. The camera stays on until stopped \u2014 across app relaunches and after a test finishes \u2014 so call this when done, or the next thing that opens a camera will still see the last source.",
|
|
3746
|
+
inputSchema: {}
|
|
3747
|
+
},
|
|
3748
|
+
async () => {
|
|
3749
|
+
const blocked = gate("camera_stop");
|
|
3750
|
+
if (blocked) return blocked;
|
|
3751
|
+
const result = await run(["stop"]);
|
|
3752
|
+
if (!result.ok) {
|
|
3753
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3754
|
+
}
|
|
3755
|
+
return text13("The simulator camera is off.");
|
|
3756
|
+
}
|
|
3757
|
+
);
|
|
3758
|
+
server.registerTool(
|
|
3759
|
+
"camera_devices",
|
|
3760
|
+
{
|
|
3761
|
+
description: "List booted simulators and the apps installed on one. Use this to find the bundle id `camera_launch` needs, or the udid to pass when more than one simulator is booted. Lists every app on the simulator, not just Buoy-integrated ones \u2014 the camera works on any of them.",
|
|
3762
|
+
inputSchema: {
|
|
3763
|
+
udid: import_zod15.z.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3764
|
+
}
|
|
3765
|
+
},
|
|
3766
|
+
async (args) => {
|
|
3767
|
+
const blocked = gate("camera_devices");
|
|
3768
|
+
if (blocked) return blocked;
|
|
3769
|
+
const devicesResult = await run(["devices"]);
|
|
3770
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3771
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3772
|
+
}
|
|
3773
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3774
|
+
let apps = null;
|
|
3775
|
+
if (devices.length === 1 || args.udid) {
|
|
3776
|
+
const appsResult = await run([
|
|
3777
|
+
"apps",
|
|
3778
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3779
|
+
]);
|
|
3780
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3781
|
+
}
|
|
3782
|
+
return text13(describeDevices(devices, apps));
|
|
3783
|
+
}
|
|
3784
|
+
);
|
|
3785
|
+
server.registerTool(
|
|
3786
|
+
"camera_inputs",
|
|
3787
|
+
{
|
|
3788
|
+
description: "List what this Mac can point the simulator camera at: its webcams (the uniqueId for `camera_source` with kind `camera`) and the windows the screen source can film (the id for `window:<id>`). Call this before choosing a `camera` or windowed `screen` source \u2014 those ids cannot be guessed.",
|
|
3789
|
+
inputSchema: {}
|
|
3790
|
+
},
|
|
3791
|
+
async () => {
|
|
3792
|
+
const blocked = gate("camera_inputs");
|
|
3793
|
+
if (blocked) return blocked;
|
|
3794
|
+
const [cams, wins] = await Promise.all([
|
|
3795
|
+
run(["cameras"]),
|
|
3796
|
+
run(["windows"])
|
|
3797
|
+
]);
|
|
3798
|
+
if (!cams.ok && !wins.ok) {
|
|
3799
|
+
return fail15(
|
|
3800
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
return text13(
|
|
3804
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
);
|
|
3808
|
+
server.registerTool(
|
|
3809
|
+
"camera_zero_setup",
|
|
3810
|
+
{
|
|
3811
|
+
description: "Make every app the simulator launches get a camera, however it was launched \u2014 `expo start`, `react-native run-ios`, Xcode, a test runner. Use this when you are not the one starting the app, so `camera_launch` is not an option. It loads Buoy's library into every process in that simulator, SpringBoard included, so turn it off when finished.",
|
|
3812
|
+
inputSchema: {
|
|
3813
|
+
action: import_zod15.z.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3814
|
+
udid: import_zod15.z.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3174
3815
|
}
|
|
3816
|
+
},
|
|
3817
|
+
async (args) => {
|
|
3818
|
+
const blocked = gate("camera_zero_setup");
|
|
3819
|
+
if (blocked) return blocked;
|
|
3820
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3821
|
+
if (!result.ok) {
|
|
3822
|
+
return fail15(
|
|
3823
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3824
|
+
);
|
|
3825
|
+
}
|
|
3826
|
+
const d = result.data ?? {};
|
|
3827
|
+
if (args.action === "status") {
|
|
3828
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3829
|
+
return text13(
|
|
3830
|
+
d.foreign ? `Zero-setup is on, but **another tool owns it** (\`${d.path}\`). Quit that tool before turning Buoy's on.` : "Zero-setup mode is **on** \u2014 anything this simulator launches gets the camera."
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
if (args.action === "on") {
|
|
3834
|
+
return text13(
|
|
3835
|
+
"Zero-setup mode is **on**. Anything this simulator launches from now on gets the camera \u2014 start your app however you normally would.\n\n_This loads the library into every process in the simulator. Turn it off with `camera_zero_setup` action `off` when you are done._"
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
return text13("Zero-setup mode is **off**.");
|
|
3175
3839
|
}
|
|
3176
3840
|
);
|
|
3177
3841
|
}
|
|
3178
3842
|
|
|
3179
3843
|
// src/tools/packs/interact.ts
|
|
3180
|
-
var
|
|
3844
|
+
var import_zod16 = require("zod");
|
|
3181
3845
|
var TOOL_ID2 = "highlight-updates";
|
|
3182
|
-
var
|
|
3183
|
-
var
|
|
3846
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3847
|
+
var fail16 = (message) => ({
|
|
3184
3848
|
isError: true,
|
|
3185
3849
|
content: [{ type: "text", text: message }]
|
|
3186
3850
|
});
|
|
3187
|
-
var
|
|
3851
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3188
3852
|
function formatElement(e) {
|
|
3189
3853
|
let line = e.interactive ? "\u2022 " : " ";
|
|
3190
3854
|
line += e.name;
|
|
@@ -3207,8 +3871,8 @@ function registerInteractTools(server, sync) {
|
|
|
3207
3871
|
{
|
|
3208
3872
|
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
3873
|
inputSchema: {
|
|
3210
|
-
interactiveOnly:
|
|
3211
|
-
deviceId:
|
|
3874
|
+
interactiveOnly: import_zod16.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3875
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3212
3876
|
}
|
|
3213
3877
|
},
|
|
3214
3878
|
async (args) => {
|
|
@@ -3218,16 +3882,16 @@ function registerInteractTools(server, sync) {
|
|
|
3218
3882
|
const all = res?.elements ?? [];
|
|
3219
3883
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3220
3884
|
if (elements.length === 0) {
|
|
3221
|
-
return
|
|
3885
|
+
return text14(
|
|
3222
3886
|
"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
3887
|
);
|
|
3224
3888
|
}
|
|
3225
3889
|
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
|
|
3890
|
+
return text14(`${header}
|
|
3227
3891
|
|
|
3228
3892
|
${elements.map(formatElement).join("\n")}`);
|
|
3229
3893
|
} catch (e) {
|
|
3230
|
-
return
|
|
3894
|
+
return fail16(errMsg15(e));
|
|
3231
3895
|
}
|
|
3232
3896
|
}
|
|
3233
3897
|
);
|
|
@@ -3236,20 +3900,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3236
3900
|
{
|
|
3237
3901
|
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
3902
|
inputSchema: {
|
|
3239
|
-
nativeTag:
|
|
3240
|
-
testID:
|
|
3241
|
-
query:
|
|
3242
|
-
value:
|
|
3243
|
-
text:
|
|
3244
|
-
longPress:
|
|
3245
|
-
scrollIntoView:
|
|
3246
|
-
deviceId:
|
|
3903
|
+
nativeTag: import_zod16.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3904
|
+
testID: import_zod16.z.string().optional().describe("Exact testID of the element."),
|
|
3905
|
+
query: import_zod16.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3906
|
+
value: import_zod16.z.union([import_zod16.z.number(), import_zod16.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)."),
|
|
3907
|
+
text: import_zod16.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3908
|
+
longPress: import_zod16.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3909
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3910
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3247
3911
|
}
|
|
3248
3912
|
},
|
|
3249
3913
|
async (args) => {
|
|
3250
3914
|
try {
|
|
3251
3915
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3252
|
-
return
|
|
3916
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3253
3917
|
}
|
|
3254
3918
|
const target = sync.requireProDevice(args.deviceId);
|
|
3255
3919
|
const res = await sync.callAction(
|
|
@@ -3274,26 +3938,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3274
3938
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3275
3939
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3276
3940
|
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
|
|
3941
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3278
3942
|
}
|
|
3279
3943
|
let msg = res?.reason ?? "Tap failed.";
|
|
3280
3944
|
if (res?.candidates?.length) {
|
|
3281
3945
|
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
3946
|
}
|
|
3283
|
-
return
|
|
3947
|
+
return fail16(msg);
|
|
3284
3948
|
} catch (e) {
|
|
3285
|
-
return
|
|
3949
|
+
return fail16(errMsg15(e));
|
|
3286
3950
|
}
|
|
3287
3951
|
}
|
|
3288
3952
|
);
|
|
3289
3953
|
}
|
|
3290
3954
|
|
|
3291
3955
|
// src/tools/packs/jsThread.ts
|
|
3292
|
-
var
|
|
3956
|
+
var import_zod17 = require("zod");
|
|
3293
3957
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3294
|
-
var
|
|
3295
|
-
var
|
|
3296
|
-
var
|
|
3958
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3959
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3960
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3297
3961
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3298
3962
|
function sparkline(values) {
|
|
3299
3963
|
const max = Math.max(1, ...values);
|
|
@@ -3364,9 +4028,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3364
4028
|
{
|
|
3365
4029
|
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
4030
|
inputSchema: {
|
|
3367
|
-
deviceId:
|
|
3368
|
-
durationMs:
|
|
3369
|
-
clearFirst:
|
|
4031
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4032
|
+
durationMs: import_zod17.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
4033
|
+
clearFirst: import_zod17.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3370
4034
|
}
|
|
3371
4035
|
},
|
|
3372
4036
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3379,7 +4043,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3379
4043
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3380
4044
|
target
|
|
3381
4045
|
);
|
|
3382
|
-
if (!snap) return
|
|
4046
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3383
4047
|
const md = formatTopMarkdown(snap, duration);
|
|
3384
4048
|
const slim = {
|
|
3385
4049
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3398,13 +4062,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3398
4062
|
worstFreezeMs: r.worstFreezeMs
|
|
3399
4063
|
}))
|
|
3400
4064
|
};
|
|
3401
|
-
return
|
|
4065
|
+
return text15(`${md}
|
|
3402
4066
|
|
|
3403
4067
|
\`\`\`json
|
|
3404
4068
|
${JSON.stringify(slim)}
|
|
3405
4069
|
\`\`\``);
|
|
3406
4070
|
} catch (e) {
|
|
3407
|
-
return
|
|
4071
|
+
return fail17(errMsg16(e));
|
|
3408
4072
|
}
|
|
3409
4073
|
}
|
|
3410
4074
|
);
|
|
@@ -3413,8 +4077,8 @@ ${JSON.stringify(slim)}
|
|
|
3413
4077
|
{
|
|
3414
4078
|
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
4079
|
inputSchema: {
|
|
3416
|
-
deviceId:
|
|
3417
|
-
key:
|
|
4080
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4081
|
+
key: import_zod17.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3418
4082
|
}
|
|
3419
4083
|
},
|
|
3420
4084
|
async ({ deviceId, key }) => {
|
|
@@ -3427,18 +4091,18 @@ ${JSON.stringify(slim)}
|
|
|
3427
4091
|
target
|
|
3428
4092
|
);
|
|
3429
4093
|
if (!detail) {
|
|
3430
|
-
return
|
|
4094
|
+
return fail17(
|
|
3431
4095
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3432
4096
|
);
|
|
3433
4097
|
}
|
|
3434
4098
|
const md = formatDetailMarkdown(detail);
|
|
3435
|
-
return
|
|
4099
|
+
return text15(`${md}
|
|
3436
4100
|
|
|
3437
4101
|
\`\`\`json
|
|
3438
4102
|
${JSON.stringify(detail)}
|
|
3439
4103
|
\`\`\``);
|
|
3440
4104
|
} catch (e) {
|
|
3441
|
-
return
|
|
4105
|
+
return fail17(errMsg16(e));
|
|
3442
4106
|
}
|
|
3443
4107
|
}
|
|
3444
4108
|
);
|
|
@@ -3447,27 +4111,27 @@ ${JSON.stringify(detail)}
|
|
|
3447
4111
|
{
|
|
3448
4112
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3449
4113
|
inputSchema: {
|
|
3450
|
-
deviceId:
|
|
4114
|
+
deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3451
4115
|
}
|
|
3452
4116
|
},
|
|
3453
4117
|
async ({ deviceId }) => {
|
|
3454
4118
|
try {
|
|
3455
4119
|
const target = sync.requireProDevice(deviceId);
|
|
3456
4120
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3457
|
-
return
|
|
4121
|
+
return text15("Cleared js-top stats.");
|
|
3458
4122
|
} catch (e) {
|
|
3459
|
-
return
|
|
4123
|
+
return fail17(errMsg16(e));
|
|
3460
4124
|
}
|
|
3461
4125
|
}
|
|
3462
4126
|
);
|
|
3463
4127
|
}
|
|
3464
4128
|
|
|
3465
4129
|
// src/tools/packs/images.ts
|
|
3466
|
-
var
|
|
4130
|
+
var import_zod18 = require("zod");
|
|
3467
4131
|
var IMAGES_TOOL_ID = "images";
|
|
3468
|
-
var
|
|
3469
|
-
var
|
|
3470
|
-
var
|
|
4132
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4133
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4134
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3471
4135
|
function uriTail(uri) {
|
|
3472
4136
|
if (!uri) return "(no source)";
|
|
3473
4137
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -3555,9 +4219,9 @@ function registerImagesTools(server, sync) {
|
|
|
3555
4219
|
{
|
|
3556
4220
|
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
4221
|
inputSchema: {
|
|
3558
|
-
deviceId:
|
|
3559
|
-
limit:
|
|
3560
|
-
status:
|
|
4222
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4223
|
+
limit: import_zod18.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4224
|
+
status: import_zod18.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3561
4225
|
}
|
|
3562
4226
|
},
|
|
3563
4227
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3570,7 +4234,7 @@ function registerImagesTools(server, sync) {
|
|
|
3570
4234
|
{ limit: capped, status },
|
|
3571
4235
|
target
|
|
3572
4236
|
);
|
|
3573
|
-
if (!list) return
|
|
4237
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3574
4238
|
const md = formatListMarkdown(list, capped);
|
|
3575
4239
|
const slim = {
|
|
3576
4240
|
stats: list.stats,
|
|
@@ -3597,13 +4261,13 @@ function registerImagesTools(server, sync) {
|
|
|
3597
4261
|
layoutShifts: r.layoutShifts
|
|
3598
4262
|
}))
|
|
3599
4263
|
};
|
|
3600
|
-
return
|
|
4264
|
+
return text16(`${md}
|
|
3601
4265
|
|
|
3602
4266
|
\`\`\`json
|
|
3603
4267
|
${JSON.stringify(slim)}
|
|
3604
4268
|
\`\`\``);
|
|
3605
4269
|
} catch (e) {
|
|
3606
|
-
return
|
|
4270
|
+
return fail18(errMsg17(e));
|
|
3607
4271
|
}
|
|
3608
4272
|
}
|
|
3609
4273
|
);
|
|
@@ -3612,9 +4276,9 @@ ${JSON.stringify(slim)}
|
|
|
3612
4276
|
{
|
|
3613
4277
|
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
4278
|
inputSchema: {
|
|
3615
|
-
deviceId:
|
|
3616
|
-
id:
|
|
3617
|
-
action:
|
|
4279
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4280
|
+
id: import_zod18.z.number().describe("Record id from get_images."),
|
|
4281
|
+
action: import_zod18.z.enum([
|
|
3618
4282
|
"hard_reload",
|
|
3619
4283
|
"retry",
|
|
3620
4284
|
"evict_disk",
|
|
@@ -3626,7 +4290,7 @@ ${JSON.stringify(slim)}
|
|
|
3626
4290
|
"swap_url",
|
|
3627
4291
|
"restore"
|
|
3628
4292
|
]).describe("What to do."),
|
|
3629
|
-
url:
|
|
4293
|
+
url: import_zod18.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
3630
4294
|
}
|
|
3631
4295
|
},
|
|
3632
4296
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3634,8 +4298,8 @@ ${JSON.stringify(slim)}
|
|
|
3634
4298
|
const target = sync.requireProDevice(deviceId);
|
|
3635
4299
|
if (action === "prove_savings") {
|
|
3636
4300
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3637
|
-
if (!savings) return
|
|
3638
|
-
if (!savings.ok) return
|
|
4301
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4302
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3639
4303
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3640
4304
|
if (savings.originalBytes != null)
|
|
3641
4305
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3647,11 +4311,11 @@ ${JSON.stringify(slim)}
|
|
|
3647
4311
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3648
4312
|
if (savings.optimizedUri)
|
|
3649
4313
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3650
|
-
return
|
|
4314
|
+
return text16(parts.join("\n"));
|
|
3651
4315
|
}
|
|
3652
4316
|
const entry = ACTION_MAP[action];
|
|
3653
|
-
if (!entry) return
|
|
3654
|
-
if (action === "swap_url" && !url) return
|
|
4317
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4318
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3655
4319
|
const result = await sync.callAction(
|
|
3656
4320
|
IMAGES_TOOL_ID,
|
|
3657
4321
|
entry.action,
|
|
@@ -3671,9 +4335,9 @@ ${JSON.stringify(slim)}
|
|
|
3671
4335
|
);
|
|
3672
4336
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3673
4337
|
}
|
|
3674
|
-
return
|
|
4338
|
+
return text16(lines.join("\n"));
|
|
3675
4339
|
} catch (e) {
|
|
3676
|
-
return
|
|
4340
|
+
return fail18(errMsg17(e));
|
|
3677
4341
|
}
|
|
3678
4342
|
}
|
|
3679
4343
|
);
|
|
@@ -3682,12 +4346,12 @@ ${JSON.stringify(slim)}
|
|
|
3682
4346
|
{
|
|
3683
4347
|
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
4348
|
inputSchema: {
|
|
3685
|
-
deviceId:
|
|
3686
|
-
network:
|
|
3687
|
-
blankImages:
|
|
3688
|
-
massAction:
|
|
3689
|
-
clearRegistry:
|
|
3690
|
-
clearExpoCaches:
|
|
4349
|
+
deviceId: import_zod18.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4350
|
+
network: import_zod18.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4351
|
+
blankImages: import_zod18.z.boolean().optional().describe("Render all images without a source."),
|
|
4352
|
+
massAction: import_zod18.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."),
|
|
4353
|
+
clearRegistry: import_zod18.z.boolean().optional().describe("Clear the captured image log."),
|
|
4354
|
+
clearExpoCaches: import_zod18.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3691
4355
|
}
|
|
3692
4356
|
},
|
|
3693
4357
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3714,17 +4378,17 @@ ${JSON.stringify(slim)}
|
|
|
3714
4378
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3715
4379
|
done.push(r?.message ?? "expo caches cleared");
|
|
3716
4380
|
}
|
|
3717
|
-
if (done.length === 0) return
|
|
3718
|
-
return
|
|
4381
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4382
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
3719
4383
|
} catch (e) {
|
|
3720
|
-
return
|
|
4384
|
+
return fail18(errMsg17(e));
|
|
3721
4385
|
}
|
|
3722
4386
|
}
|
|
3723
4387
|
);
|
|
3724
4388
|
}
|
|
3725
4389
|
|
|
3726
4390
|
// src/tools/packs/assets.ts
|
|
3727
|
-
var
|
|
4391
|
+
var import_zod19 = require("zod");
|
|
3728
4392
|
|
|
3729
4393
|
// src/tools/packs/assetsGraphNote.ts
|
|
3730
4394
|
function graphUnavailableNote(ss) {
|
|
@@ -3739,9 +4403,9 @@ function graphUnavailableNote(ss) {
|
|
|
3739
4403
|
|
|
3740
4404
|
// src/tools/packs/assets.ts
|
|
3741
4405
|
var ASSETS_TOOL_ID = "assets";
|
|
3742
|
-
var
|
|
3743
|
-
var
|
|
3744
|
-
var
|
|
4406
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4407
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4408
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3745
4409
|
function kb2(bytes) {
|
|
3746
4410
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3747
4411
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -3829,10 +4493,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3829
4493
|
{
|
|
3830
4494
|
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
4495
|
inputSchema: {
|
|
3832
|
-
deviceId:
|
|
3833
|
-
limit:
|
|
3834
|
-
kind:
|
|
3835
|
-
unusedOnly:
|
|
4496
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4497
|
+
limit: import_zod19.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4498
|
+
kind: import_zod19.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4499
|
+
unusedOnly: import_zod19.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3836
4500
|
}
|
|
3837
4501
|
},
|
|
3838
4502
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3845,7 +4509,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3845
4509
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3846
4510
|
target
|
|
3847
4511
|
);
|
|
3848
|
-
if (!list) return
|
|
4512
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3849
4513
|
if (unusedOnly) {
|
|
3850
4514
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3851
4515
|
}
|
|
@@ -3869,13 +4533,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3869
4533
|
sizeSource: r.sizeSource
|
|
3870
4534
|
}))
|
|
3871
4535
|
};
|
|
3872
|
-
return
|
|
4536
|
+
return text17(`${md}
|
|
3873
4537
|
|
|
3874
4538
|
\`\`\`json
|
|
3875
4539
|
${JSON.stringify(slim)}
|
|
3876
4540
|
\`\`\``);
|
|
3877
4541
|
} catch (e) {
|
|
3878
|
-
return
|
|
4542
|
+
return fail19(errMsg18(e));
|
|
3879
4543
|
}
|
|
3880
4544
|
}
|
|
3881
4545
|
);
|
|
@@ -3884,9 +4548,9 @@ ${JSON.stringify(slim)}
|
|
|
3884
4548
|
{
|
|
3885
4549
|
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
4550
|
inputSchema: {
|
|
3887
|
-
deviceId:
|
|
3888
|
-
action:
|
|
3889
|
-
id:
|
|
4551
|
+
deviceId: import_zod19.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4552
|
+
action: import_zod19.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4553
|
+
id: import_zod19.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3890
4554
|
}
|
|
3891
4555
|
},
|
|
3892
4556
|
async ({ deviceId, action, id }) => {
|
|
@@ -3896,19 +4560,19 @@ ${JSON.stringify(slim)}
|
|
|
3896
4560
|
case "rescan": {
|
|
3897
4561
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3898
4562
|
const ss = result?.scanStatus;
|
|
3899
|
-
if (!ss) return
|
|
3900
|
-
return
|
|
4563
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4564
|
+
return text17(
|
|
3901
4565
|
`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
4566
|
);
|
|
3903
4567
|
}
|
|
3904
4568
|
case "measure_sizes": {
|
|
3905
4569
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3906
|
-
return
|
|
4570
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3907
4571
|
}
|
|
3908
4572
|
case "detail": {
|
|
3909
|
-
if (id == null) return
|
|
4573
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
3910
4574
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3911
|
-
if (!detail) return
|
|
4575
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3912
4576
|
const lines = [
|
|
3913
4577
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3914
4578
|
"",
|
|
@@ -3926,32 +4590,32 @@ ${JSON.stringify(slim)}
|
|
|
3926
4590
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3927
4591
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3928
4592
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3929
|
-
return
|
|
4593
|
+
return text17(lines.join("\n"));
|
|
3930
4594
|
}
|
|
3931
4595
|
case "save_baseline": {
|
|
3932
4596
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3933
|
-
return
|
|
4597
|
+
return text17(
|
|
3934
4598
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3935
4599
|
);
|
|
3936
4600
|
}
|
|
3937
4601
|
case "clear_baseline": {
|
|
3938
4602
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3939
|
-
return
|
|
4603
|
+
return text17("Baseline cleared.");
|
|
3940
4604
|
}
|
|
3941
4605
|
case "clear_records": {
|
|
3942
4606
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3943
|
-
return
|
|
4607
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
3944
4608
|
}
|
|
3945
4609
|
}
|
|
3946
4610
|
} catch (e) {
|
|
3947
|
-
return
|
|
4611
|
+
return fail19(errMsg18(e));
|
|
3948
4612
|
}
|
|
3949
4613
|
}
|
|
3950
4614
|
);
|
|
3951
4615
|
}
|
|
3952
4616
|
|
|
3953
4617
|
// src/tools/packs/reload.ts
|
|
3954
|
-
var
|
|
4618
|
+
var import_zod20 = require("zod");
|
|
3955
4619
|
|
|
3956
4620
|
// src/capture/devServerReload.ts
|
|
3957
4621
|
var import_ws = require("ws");
|
|
@@ -4014,9 +4678,9 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4014
4678
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4015
4679
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4016
4680
|
var PROBE_INTERVAL_MS = 400;
|
|
4017
|
-
var
|
|
4018
|
-
var
|
|
4019
|
-
var
|
|
4681
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4682
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4683
|
+
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4020
4684
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4021
4685
|
async function ping(sync, deviceId, timeoutMs) {
|
|
4022
4686
|
try {
|
|
@@ -4055,12 +4719,12 @@ function registerReloadTools(server, sync) {
|
|
|
4055
4719
|
{
|
|
4056
4720
|
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
4721
|
inputSchema: {
|
|
4058
|
-
strategy:
|
|
4722
|
+
strategy: import_zod20.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4059
4723
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4060
4724
|
),
|
|
4061
|
-
wait:
|
|
4062
|
-
timeoutMs:
|
|
4063
|
-
deviceId:
|
|
4725
|
+
wait: import_zod20.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4726
|
+
timeoutMs: import_zod20.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4727
|
+
deviceId: import_zod20.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4064
4728
|
}
|
|
4065
4729
|
},
|
|
4066
4730
|
async (args) => {
|
|
@@ -4102,32 +4766,32 @@ function registerReloadTools(server, sync) {
|
|
|
4102
4766
|
}
|
|
4103
4767
|
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
4768
|
if (args.wait === false) {
|
|
4105
|
-
return
|
|
4769
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4106
4770
|
}
|
|
4107
4771
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4108
4772
|
if (elapsed == null) {
|
|
4109
|
-
return
|
|
4773
|
+
return fail20(
|
|
4110
4774
|
`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
4775
|
);
|
|
4112
4776
|
}
|
|
4113
|
-
return
|
|
4777
|
+
return text18(
|
|
4114
4778
|
`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
4779
|
);
|
|
4116
4780
|
} catch (e) {
|
|
4117
|
-
return
|
|
4781
|
+
return fail20(errMsg19(e));
|
|
4118
4782
|
}
|
|
4119
4783
|
}
|
|
4120
4784
|
);
|
|
4121
4785
|
}
|
|
4122
4786
|
|
|
4123
4787
|
// src/tools/packs/triage.ts
|
|
4124
|
-
var
|
|
4125
|
-
var
|
|
4126
|
-
var
|
|
4788
|
+
var import_zod21 = require("zod");
|
|
4789
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4790
|
+
var fail21 = (message) => ({
|
|
4127
4791
|
isError: true,
|
|
4128
4792
|
content: [{ type: "text", text: message }]
|
|
4129
4793
|
});
|
|
4130
|
-
var
|
|
4794
|
+
var errMsg20 = (e) => e instanceof Error ? e.message : String(e);
|
|
4131
4795
|
var PROBE_TIMEOUT_MS2 = 5e3;
|
|
4132
4796
|
var MAX_STORAGE_KEYS = 20;
|
|
4133
4797
|
var MAX_STORAGE_VALUE = 400;
|
|
@@ -4158,13 +4822,13 @@ async function otherBackends(sync, target, opts) {
|
|
|
4158
4822
|
return notes.length ? `
|
|
4159
4823
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4160
4824
|
}
|
|
4161
|
-
async function section(title,
|
|
4825
|
+
async function section(title, run2) {
|
|
4162
4826
|
try {
|
|
4163
|
-
const body = await
|
|
4827
|
+
const body = await run2();
|
|
4164
4828
|
return body ? `${title}
|
|
4165
4829
|
${body}` : null;
|
|
4166
4830
|
} catch (e) {
|
|
4167
|
-
const msg =
|
|
4831
|
+
const msg = errMsg20(e);
|
|
4168
4832
|
if (/not responding|timed out|got no answer/i.test(msg)) {
|
|
4169
4833
|
return `${title}
|
|
4170
4834
|
(no answer \u2014 see the note above)`;
|
|
@@ -4179,7 +4843,7 @@ function registerTriageTools(server, sync) {
|
|
|
4179
4843
|
{
|
|
4180
4844
|
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
4845
|
inputSchema: {
|
|
4182
|
-
deviceId:
|
|
4846
|
+
deviceId: import_zod21.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4183
4847
|
}
|
|
4184
4848
|
},
|
|
4185
4849
|
async (args) => {
|
|
@@ -4286,7 +4950,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4286
4950
|
|
|
4287
4951
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4288
4952
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4289
|
-
return
|
|
4953
|
+
return text19(
|
|
4290
4954
|
`${head}${banner}
|
|
4291
4955
|
|
|
4292
4956
|
${body}
|
|
@@ -4294,22 +4958,22 @@ ${body}
|
|
|
4294
4958
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4295
4959
|
);
|
|
4296
4960
|
} catch (e) {
|
|
4297
|
-
return
|
|
4961
|
+
return fail21(errMsg20(e));
|
|
4298
4962
|
}
|
|
4299
4963
|
}
|
|
4300
4964
|
);
|
|
4301
4965
|
}
|
|
4302
4966
|
|
|
4303
4967
|
// src/tools/packs/flow.ts
|
|
4304
|
-
var
|
|
4968
|
+
var import_zod22 = require("zod");
|
|
4305
4969
|
var UI_TOOL_ID = "highlight-updates";
|
|
4306
4970
|
var APP_TOOL_ID2 = "app";
|
|
4307
|
-
var
|
|
4308
|
-
var
|
|
4971
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4972
|
+
var fail22 = (message) => ({
|
|
4309
4973
|
isError: true,
|
|
4310
4974
|
content: [{ type: "text", text: message }]
|
|
4311
4975
|
});
|
|
4312
|
-
var
|
|
4976
|
+
var errMsg21 = (e) => e instanceof Error ? e.message : String(e);
|
|
4313
4977
|
var delay4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4314
4978
|
var POLL_MS = 250;
|
|
4315
4979
|
var DEFAULT_WAIT_MS2 = 5e3;
|
|
@@ -4326,19 +4990,19 @@ function formatElement2(e) {
|
|
|
4326
4990
|
const display = e.text ?? e.label;
|
|
4327
4991
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4328
4992
|
}
|
|
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:
|
|
4993
|
+
var stepSchema = import_zod22.z.object({
|
|
4994
|
+
tap: import_zod22.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4995
|
+
longPress: import_zod22.z.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4996
|
+
type: import_zod22.z.string().optional().describe("Text to type (needs `into`)."),
|
|
4997
|
+
into: import_zod22.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4998
|
+
set: import_zod22.z.union([import_zod22.z.number(), import_zod22.z.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4999
|
+
reload: import_zod22.z.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
5000
|
+
waitMs: import_zod22.z.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
5001
|
+
waitFor: import_zod22.z.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
5002
|
+
waitGone: import_zod22.z.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
5003
|
+
expect: import_zod22.z.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
5004
|
+
expectGone: import_zod22.z.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
5005
|
+
timeoutMs: import_zod22.z.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4342
5006
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4343
5007
|
function registerFlowTools(server, sync) {
|
|
4344
5008
|
server.registerTool(
|
|
@@ -4346,10 +5010,10 @@ function registerFlowTools(server, sync) {
|
|
|
4346
5010
|
{
|
|
4347
5011
|
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
5012
|
inputSchema: {
|
|
4349
|
-
steps:
|
|
4350
|
-
include:
|
|
4351
|
-
continueOnFailure:
|
|
4352
|
-
deviceId:
|
|
5013
|
+
steps: import_zod22.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5014
|
+
include: import_zod22.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."),
|
|
5015
|
+
continueOnFailure: import_zod22.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5016
|
+
deviceId: import_zod22.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4353
5017
|
}
|
|
4354
5018
|
},
|
|
4355
5019
|
async (args) => {
|
|
@@ -4521,7 +5185,7 @@ function registerFlowTools(server, sync) {
|
|
|
4521
5185
|
else stepErrors.push(`expected "${step.expectGone}" to be gone \u2014 still on screen`);
|
|
4522
5186
|
}
|
|
4523
5187
|
} catch (e) {
|
|
4524
|
-
stepErrors.push(
|
|
5188
|
+
stepErrors.push(errMsg21(e));
|
|
4525
5189
|
}
|
|
4526
5190
|
if (stepErrors.length) {
|
|
4527
5191
|
lines.push(`${label} \u2717 ${describeStep(step)} \u2014 ${stepErrors.join("; ")}`);
|
|
@@ -4544,7 +5208,7 @@ function registerFlowTools(server, sync) {
|
|
|
4544
5208
|
${shown.map(formatElement2).join("\n")}`
|
|
4545
5209
|
);
|
|
4546
5210
|
} catch (e) {
|
|
4547
|
-
parts.push(`**Screen now:** unavailable \u2014 ${cap2(
|
|
5211
|
+
parts.push(`**Screen now:** unavailable \u2014 ${cap2(errMsg21(e))}`);
|
|
4548
5212
|
}
|
|
4549
5213
|
}
|
|
4550
5214
|
if (include === "all") {
|
|
@@ -4573,9 +5237,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4573
5237
|
}
|
|
4574
5238
|
}
|
|
4575
5239
|
}
|
|
4576
|
-
return
|
|
5240
|
+
return text20(parts.join("\n\n"));
|
|
4577
5241
|
} catch (e) {
|
|
4578
|
-
return
|
|
5242
|
+
return fail22(errMsg21(e));
|
|
4579
5243
|
}
|
|
4580
5244
|
}
|
|
4581
5245
|
);
|
|
@@ -4596,25 +5260,25 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4596
5260
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4597
5261
|
|
|
4598
5262
|
// src/tools/packs/renders.ts
|
|
4599
|
-
var
|
|
5263
|
+
var import_zod23 = require("zod");
|
|
4600
5264
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4601
|
-
var
|
|
4602
|
-
var
|
|
5265
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5266
|
+
var fail23 = (message) => ({
|
|
4603
5267
|
isError: true,
|
|
4604
5268
|
content: [{ type: "text", text: message }]
|
|
4605
5269
|
});
|
|
4606
|
-
var
|
|
5270
|
+
var errMsg22 = (e) => e instanceof Error ? e.message : String(e);
|
|
4607
5271
|
var delay5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4608
5272
|
var DEFAULT_IDLE_MS = 3e3;
|
|
4609
5273
|
var SETTLE_MS = 400;
|
|
4610
5274
|
var DEFAULT_TOP = 12;
|
|
4611
5275
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4612
|
-
var stepSchema2 =
|
|
4613
|
-
tap:
|
|
4614
|
-
type:
|
|
4615
|
-
into:
|
|
4616
|
-
waitMs:
|
|
4617
|
-
repeat:
|
|
5276
|
+
var stepSchema2 = import_zod23.z.object({
|
|
5277
|
+
tap: import_zod23.z.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5278
|
+
type: import_zod23.z.string().optional().describe("Text to type (needs `into`)."),
|
|
5279
|
+
into: import_zod23.z.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5280
|
+
waitMs: import_zod23.z.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5281
|
+
repeat: import_zod23.z.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4618
5282
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4619
5283
|
function selector2(needle) {
|
|
4620
5284
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4707,14 +5371,14 @@ function registerRenderTools(server, sync) {
|
|
|
4707
5371
|
{
|
|
4708
5372
|
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
5373
|
inputSchema: {
|
|
4710
|
-
steps:
|
|
5374
|
+
steps: import_zod23.z.array(stepSchema2).max(20).optional().describe(
|
|
4711
5375
|
"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
5376
|
),
|
|
4713
|
-
idleMs:
|
|
4714
|
-
label:
|
|
4715
|
-
compareToPrevious:
|
|
4716
|
-
top:
|
|
4717
|
-
deviceId:
|
|
5377
|
+
idleMs: import_zod23.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5378
|
+
label: import_zod23.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5379
|
+
compareToPrevious: import_zod23.z.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5380
|
+
top: import_zod23.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5381
|
+
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4718
5382
|
}
|
|
4719
5383
|
},
|
|
4720
5384
|
async (args) => {
|
|
@@ -4723,18 +5387,18 @@ function registerRenderTools(server, sync) {
|
|
|
4723
5387
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4724
5388
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4725
5389
|
if (!uiTool) {
|
|
4726
|
-
return
|
|
5390
|
+
return fail23(
|
|
4727
5391
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4728
5392
|
);
|
|
4729
5393
|
}
|
|
4730
5394
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4731
|
-
return
|
|
5395
|
+
return fail23(
|
|
4732
5396
|
"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
5397
|
);
|
|
4734
5398
|
}
|
|
4735
5399
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4736
5400
|
if (began && began.ok === false) {
|
|
4737
|
-
return
|
|
5401
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4738
5402
|
}
|
|
4739
5403
|
await delay5(SETTLE_MS);
|
|
4740
5404
|
const startedAt = Date.now();
|
|
@@ -4779,7 +5443,7 @@ function registerRenderTools(server, sync) {
|
|
|
4779
5443
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
4780
5444
|
const summary = ended?.summary;
|
|
4781
5445
|
if (!summary) {
|
|
4782
|
-
return
|
|
5446
|
+
return fail23(
|
|
4783
5447
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
4784
5448
|
);
|
|
4785
5449
|
}
|
|
@@ -4804,17 +5468,17 @@ function registerRenderTools(server, sync) {
|
|
|
4804
5468
|
} else if (!args.compareToPrevious) {
|
|
4805
5469
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
4806
5470
|
}
|
|
4807
|
-
return
|
|
5471
|
+
return text21(parts.join("\n"));
|
|
4808
5472
|
} catch (e) {
|
|
4809
|
-
return
|
|
5473
|
+
return fail23(errMsg22(e));
|
|
4810
5474
|
}
|
|
4811
5475
|
}
|
|
4812
5476
|
);
|
|
4813
5477
|
}
|
|
4814
5478
|
|
|
4815
5479
|
// src/verify/editWatcher.ts
|
|
4816
|
-
var
|
|
4817
|
-
var
|
|
5480
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
5481
|
+
var import_node_path4 = __toESM(require("path"));
|
|
4818
5482
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
4819
5483
|
".ts",
|
|
4820
5484
|
".tsx",
|
|
@@ -4857,14 +5521,14 @@ var EditWatcher = class {
|
|
|
4857
5521
|
if (mode === "never" || this.started) return;
|
|
4858
5522
|
this.started = true;
|
|
4859
5523
|
try {
|
|
4860
|
-
this.watcher =
|
|
5524
|
+
this.watcher = import_node_fs4.default.watch(
|
|
4861
5525
|
rootDir,
|
|
4862
5526
|
{ recursive: true },
|
|
4863
5527
|
(_event, filename) => {
|
|
4864
5528
|
if (!filename) return;
|
|
4865
5529
|
const rel2 = String(filename);
|
|
4866
|
-
if (rel2.split(
|
|
4867
|
-
if (!WATCHED_EXT.has(
|
|
5530
|
+
if (rel2.split(import_node_path4.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5531
|
+
if (!WATCHED_EXT.has(import_node_path4.default.extname(rel2))) return;
|
|
4868
5532
|
this.noteEdit(rel2);
|
|
4869
5533
|
}
|
|
4870
5534
|
);
|
|
@@ -4964,6 +5628,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
4964
5628
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
4965
5629
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
4966
5630
|
|
|
5631
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5632
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5633
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5634
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5635
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5636
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5637
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5638
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5639
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5640
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5641
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5642
|
+
and no Buoy integration in the app.
|
|
5643
|
+
|
|
4967
5644
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
4968
5645
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
4969
5646
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -5027,6 +5704,7 @@ require the connected app to have an active Buoy Pro license. If they error with
|
|
|
5027
5704
|
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
5028
5705
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
5029
5706
|
"measure_renders",
|
|
5707
|
+
"scenario_action",
|
|
5030
5708
|
"run_flow",
|
|
5031
5709
|
"run_benchmark_batch",
|
|
5032
5710
|
"get_triage",
|
|
@@ -5077,7 +5755,9 @@ function createMcpServer(sync) {
|
|
|
5077
5755
|
registerRoutesTools(server, sync);
|
|
5078
5756
|
registerConsoleTools(server, sync);
|
|
5079
5757
|
registerTimeMachineTools(server, sync);
|
|
5758
|
+
registerScenariosTools(server, sync);
|
|
5080
5759
|
registerScreenshotTools(server, sync);
|
|
5760
|
+
registerCameraTools(server);
|
|
5081
5761
|
registerInteractTools(server, sync);
|
|
5082
5762
|
registerJsThreadTools(server, sync);
|
|
5083
5763
|
registerImagesTools(server, sync);
|
|
@@ -5086,16 +5766,16 @@ function createMcpServer(sync) {
|
|
|
5086
5766
|
return server;
|
|
5087
5767
|
}
|
|
5088
5768
|
var deviceIdField = {
|
|
5089
|
-
deviceId:
|
|
5769
|
+
deviceId: import_zod24.z.string().optional().describe(
|
|
5090
5770
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5091
5771
|
)
|
|
5092
5772
|
};
|
|
5093
5773
|
|
|
5094
5774
|
// src/init.ts
|
|
5095
|
-
var
|
|
5096
|
-
var
|
|
5097
|
-
var
|
|
5098
|
-
var
|
|
5775
|
+
var import_node_child_process3 = require("child_process");
|
|
5776
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5777
|
+
var import_node_os4 = __toESM(require("os"));
|
|
5778
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5099
5779
|
function defaultEnv() {
|
|
5100
5780
|
return { BUOY_VERIFY: "auto" };
|
|
5101
5781
|
}
|
|
@@ -5103,19 +5783,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5103
5783
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5104
5784
|
function runInit(opts = {}) {
|
|
5105
5785
|
const cwd = opts.cwd ?? process.cwd();
|
|
5106
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5786
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path5.default.resolve(__dirname, "../skill");
|
|
5107
5787
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5108
5788
|
const entry = resolveEntry(opts, log);
|
|
5109
5789
|
const done = [];
|
|
5110
|
-
upsertMcpConfig(
|
|
5111
|
-
|
|
5112
|
-
upsertMcpConfig(
|
|
5113
|
-
if (
|
|
5114
|
-
upsertMcpConfig(
|
|
5790
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5791
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.join(cwd, ".cursor"), { recursive: true });
|
|
5792
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5793
|
+
if (import_node_fs5.default.existsSync(import_node_path5.default.join(cwd, ".vscode"))) {
|
|
5794
|
+
upsertMcpConfig(import_node_path5.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5115
5795
|
}
|
|
5116
5796
|
installSkills(skillSrcDir, cwd, done);
|
|
5117
|
-
appendDebuggingGuide(
|
|
5118
|
-
appendDebuggingGuide(
|
|
5797
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5798
|
+
appendDebuggingGuide(import_node_path5.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
5119
5799
|
log("Buoy MCP installed:");
|
|
5120
5800
|
for (const line of done) log(" \u2022 " + line);
|
|
5121
5801
|
log("");
|
|
@@ -5157,21 +5837,21 @@ function npxEntry() {
|
|
|
5157
5837
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5158
5838
|
}
|
|
5159
5839
|
function localEntry(opts, log) {
|
|
5160
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5840
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path5.default.join(import_node_os4.default.homedir(), ".buoy-mcp");
|
|
5161
5841
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5162
|
-
const
|
|
5163
|
-
if (
|
|
5164
|
-
return { type: "stdio", command: "node", args: [
|
|
5842
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5843
|
+
if (cliPath2) {
|
|
5844
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5165
5845
|
}
|
|
5166
5846
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5167
5847
|
return npxEntry();
|
|
5168
5848
|
}
|
|
5169
5849
|
function installLocalCopy(installDir, registry, log) {
|
|
5170
5850
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
5171
|
-
|
|
5851
|
+
import_node_fs5.default.mkdirSync(installDir, { recursive: true });
|
|
5172
5852
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5173
5853
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
5174
|
-
const res = (0,
|
|
5854
|
+
const res = (0, import_node_child_process3.spawnSync)(
|
|
5175
5855
|
npm,
|
|
5176
5856
|
[
|
|
5177
5857
|
"install",
|
|
@@ -5191,12 +5871,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5191
5871
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5192
5872
|
return null;
|
|
5193
5873
|
}
|
|
5194
|
-
const
|
|
5195
|
-
if (!
|
|
5196
|
-
log(`\u26A0 install finished but ${
|
|
5874
|
+
const cliPath2 = import_node_path5.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5875
|
+
if (!import_node_fs5.default.existsSync(cliPath2)) {
|
|
5876
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5197
5877
|
return null;
|
|
5198
5878
|
}
|
|
5199
|
-
return
|
|
5879
|
+
return cliPath2;
|
|
5200
5880
|
}
|
|
5201
5881
|
function probeRegistry() {
|
|
5202
5882
|
const registry = effectiveRegistry();
|
|
@@ -5214,14 +5894,14 @@ function effectiveRegistry() {
|
|
|
5214
5894
|
}
|
|
5215
5895
|
function npmConfigGet(key) {
|
|
5216
5896
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
5217
|
-
const res = (0,
|
|
5897
|
+
const res = (0, import_node_child_process3.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 });
|
|
5218
5898
|
if (res.status !== 0 || typeof res.stdout !== "string") return null;
|
|
5219
5899
|
const val = res.stdout.trim();
|
|
5220
5900
|
if (!val || val === "undefined" || val === "null") return null;
|
|
5221
5901
|
return val;
|
|
5222
5902
|
}
|
|
5223
5903
|
function hostReachable(registry) {
|
|
5224
|
-
const done = (0,
|
|
5904
|
+
const done = (0, import_node_child_process3.spawnSync)(
|
|
5225
5905
|
process.execPath,
|
|
5226
5906
|
["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS3)],
|
|
5227
5907
|
{ encoding: "utf8", timeout: PROBE_TIMEOUT_MS3 + 2e3 }
|
|
@@ -5244,22 +5924,22 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
5244
5924
|
`;
|
|
5245
5925
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
5246
5926
|
let json3 = {};
|
|
5247
|
-
if (
|
|
5927
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5248
5928
|
try {
|
|
5249
|
-
json3 = JSON.parse(
|
|
5929
|
+
json3 = JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
|
|
5250
5930
|
} catch {
|
|
5251
|
-
|
|
5931
|
+
import_node_fs5.default.copyFileSync(file, file + ".bak");
|
|
5252
5932
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
5253
5933
|
json3 = {};
|
|
5254
5934
|
}
|
|
5255
5935
|
} else {
|
|
5256
|
-
|
|
5936
|
+
import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
|
|
5257
5937
|
}
|
|
5258
5938
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5259
5939
|
const existed = "buoy" in servers;
|
|
5260
5940
|
servers.buoy = entry;
|
|
5261
5941
|
json3[key] = servers;
|
|
5262
|
-
|
|
5942
|
+
import_node_fs5.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
5263
5943
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
5264
5944
|
}
|
|
5265
5945
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
@@ -5319,9 +5999,9 @@ mentions it.
|
|
|
5319
5999
|
`;
|
|
5320
6000
|
function appendDebuggingGuide(file, cwd, done) {
|
|
5321
6001
|
let existing = "";
|
|
5322
|
-
if (
|
|
6002
|
+
if (import_node_fs5.default.existsSync(file)) {
|
|
5323
6003
|
try {
|
|
5324
|
-
existing =
|
|
6004
|
+
existing = import_node_fs5.default.readFileSync(file, "utf8");
|
|
5325
6005
|
} catch {
|
|
5326
6006
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5327
6007
|
return;
|
|
@@ -5333,40 +6013,40 @@ function appendDebuggingGuide(file, cwd, done) {
|
|
|
5333
6013
|
}
|
|
5334
6014
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
5335
6015
|
try {
|
|
5336
|
-
|
|
6016
|
+
import_node_fs5.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
5337
6017
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
5338
6018
|
} catch {
|
|
5339
6019
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
5340
6020
|
}
|
|
5341
6021
|
}
|
|
5342
6022
|
function installSkills(skillSrcDir, cwd, done) {
|
|
5343
|
-
if (!
|
|
6023
|
+
if (!import_node_fs5.default.existsSync(skillSrcDir)) {
|
|
5344
6024
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5345
6025
|
return;
|
|
5346
6026
|
}
|
|
5347
|
-
const skills =
|
|
6027
|
+
const skills = import_node_fs5.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs5.default.existsSync(import_node_path5.default.join(skillSrcDir, e.name, "SKILL.md")));
|
|
5348
6028
|
if (skills.length === 0) {
|
|
5349
6029
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5350
6030
|
return;
|
|
5351
6031
|
}
|
|
5352
6032
|
for (const skill of skills) {
|
|
5353
|
-
const from =
|
|
5354
|
-
const to =
|
|
6033
|
+
const from = import_node_path5.default.join(skillSrcDir, skill.name);
|
|
6034
|
+
const to = import_node_path5.default.join(cwd, ".claude", "skills", skill.name);
|
|
5355
6035
|
copyDir(from, to);
|
|
5356
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6036
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path5.default.join(to, "SKILL.md"), cwd)}`);
|
|
5357
6037
|
}
|
|
5358
6038
|
}
|
|
5359
6039
|
function copyDir(from, to) {
|
|
5360
|
-
|
|
5361
|
-
for (const entry of
|
|
5362
|
-
const s =
|
|
5363
|
-
const d =
|
|
6040
|
+
import_node_fs5.default.mkdirSync(to, { recursive: true });
|
|
6041
|
+
for (const entry of import_node_fs5.default.readdirSync(from, { withFileTypes: true })) {
|
|
6042
|
+
const s = import_node_path5.default.join(from, entry.name);
|
|
6043
|
+
const d = import_node_path5.default.join(to, entry.name);
|
|
5364
6044
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5365
|
-
else
|
|
6045
|
+
else import_node_fs5.default.copyFileSync(s, d);
|
|
5366
6046
|
}
|
|
5367
6047
|
}
|
|
5368
6048
|
function rel(file, cwd) {
|
|
5369
|
-
const r =
|
|
6049
|
+
const r = import_node_path5.default.relative(cwd, file);
|
|
5370
6050
|
return r && !r.startsWith("..") ? r : file;
|
|
5371
6051
|
}
|
|
5372
6052
|
// Annotate the CommonJS export names for ESM import in node:
|