@buoy-gg/mcp 7.0.31 → 7.0.33
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-HUL2D27L.mjs → chunk-6HRZM4CM.mjs} +581 -193
- package/dist/cli.js +600 -212
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +600 -212
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -226,6 +226,7 @@ var SyncClient = class {
|
|
|
226
226
|
isConnected: d.isConnected !== false,
|
|
227
227
|
isPro: d.isPro === true,
|
|
228
228
|
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
229
|
+
collision: d.collision === true,
|
|
229
230
|
tools: caps?.tools ?? []
|
|
230
231
|
};
|
|
231
232
|
});
|
|
@@ -613,7 +614,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
613
614
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
614
615
|
|
|
615
616
|
// src/version.ts
|
|
616
|
-
var VERSION = "7.0.
|
|
617
|
+
var VERSION = "7.0.33" ? "7.0.33" : "0.0.0-dev";
|
|
617
618
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
618
619
|
|
|
619
620
|
// src/update/checkForUpdate.ts
|
|
@@ -711,8 +712,12 @@ function registerGenericTools(server, sync) {
|
|
|
711
712
|
const connected = devices.filter((d) => d.isConnected);
|
|
712
713
|
const anyPro = connected.some((d) => d.isPro);
|
|
713
714
|
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
715
|
+
const collided = connected.filter((d) => d.collision);
|
|
714
716
|
return json({
|
|
715
717
|
devices,
|
|
718
|
+
...collided.length > 0 ? {
|
|
719
|
+
warning: `${collided.map((d) => `"${d.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`
|
|
720
|
+
} : {},
|
|
716
721
|
...anyPro ? {} : {
|
|
717
722
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
718
723
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
@@ -1350,7 +1355,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1350
1355
|
let sawActive = false;
|
|
1351
1356
|
let idleStreak = 0;
|
|
1352
1357
|
let lastCount = -1;
|
|
1353
|
-
let
|
|
1358
|
+
let resolved2 = null;
|
|
1354
1359
|
while (Date.now() - start < cap3) {
|
|
1355
1360
|
let snap2;
|
|
1356
1361
|
try {
|
|
@@ -1362,7 +1367,7 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1362
1367
|
const completed = snap2.live?.automationCompleted;
|
|
1363
1368
|
if (completed?.phase === "cancelled") return fail2(`Batch ${completed.batchId} was cancelled.`);
|
|
1364
1369
|
if (completed?.phase === "done") {
|
|
1365
|
-
|
|
1370
|
+
resolved2 = { batchId: completed.batchId, failures: completed.failures ?? failuresFromIndex(snap2.index, completed.batchId) };
|
|
1366
1371
|
break;
|
|
1367
1372
|
}
|
|
1368
1373
|
const phase = snap2.live?.automation?.phase ?? "idle";
|
|
@@ -1372,11 +1377,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1372
1377
|
const count = runCountFor(snap2.index, newest);
|
|
1373
1378
|
if (count === lastCount) {
|
|
1374
1379
|
if (sawActive && ++idleStreak >= 3) {
|
|
1375
|
-
|
|
1380
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1376
1381
|
break;
|
|
1377
1382
|
}
|
|
1378
1383
|
if (!sawActive && Date.now() - start > 4e3) {
|
|
1379
|
-
|
|
1384
|
+
resolved2 = { batchId: newest, failures: failuresFromIndex(snap2.index, newest) };
|
|
1380
1385
|
break;
|
|
1381
1386
|
}
|
|
1382
1387
|
} else {
|
|
@@ -1388,11 +1393,11 @@ ${JSON.stringify(summary, null, 2)}
|
|
|
1388
1393
|
}
|
|
1389
1394
|
await delay2(POLL_INTERVAL_MS);
|
|
1390
1395
|
}
|
|
1391
|
-
if (!
|
|
1396
|
+
if (!resolved2) return fail2("No automation batch completed within the wait window.");
|
|
1392
1397
|
await sync.callAction(PERF_TOOL_ID, "refreshIndex", {}, target).catch(() => {
|
|
1393
1398
|
});
|
|
1394
1399
|
const snap = await readPerfSnapshot(sync, target);
|
|
1395
|
-
const ranking = rankBatchFromIndex(snap.index ?? [],
|
|
1400
|
+
const ranking = rankBatchFromIndex(snap.index ?? [], resolved2.batchId, void 0, resolved2.failures);
|
|
1396
1401
|
await sync.callAction(PERF_TOOL_ID, "acknowledgeAutomation", {}, target).catch(() => {
|
|
1397
1402
|
});
|
|
1398
1403
|
return text(`${formatRankingMarkdown(ranking)}
|
|
@@ -2359,9 +2364,9 @@ function reasonMessage(res, query) {
|
|
|
2359
2364
|
return res.reason ? `Locate failed: ${res.reason}` : "Couldn't locate that component.";
|
|
2360
2365
|
}
|
|
2361
2366
|
}
|
|
2362
|
-
function candidatesText(
|
|
2363
|
-
if (!
|
|
2364
|
-
const lines =
|
|
2367
|
+
function candidatesText(candidates2) {
|
|
2368
|
+
if (!candidates2 || candidates2.length === 0) return "";
|
|
2369
|
+
const lines = candidates2.map((c) => {
|
|
2365
2370
|
const id = c.testID || c.nativeID;
|
|
2366
2371
|
return `- ${c.componentName || c.viewType}${id ? ` (${id})` : ""} \u2014 nativeTag ${c.nativeTag}, rendered \xD7${c.renderCount}`;
|
|
2367
2372
|
});
|
|
@@ -2462,7 +2467,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2462
2467
|
|
|
2463
2468
|
// src/server.ts
|
|
2464
2469
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2465
|
-
import { z as
|
|
2470
|
+
import { z as z24 } from "zod";
|
|
2466
2471
|
|
|
2467
2472
|
// src/tools/packs/network.ts
|
|
2468
2473
|
import { z as z12 } from "zod";
|
|
@@ -2567,8 +2572,8 @@ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetc
|
|
|
2567
2572
|
}
|
|
2568
2573
|
function bodyLines(e, fetched) {
|
|
2569
2574
|
const got = e.id ? fetched.get(e.id) : void 0;
|
|
2570
|
-
const label = (inline,
|
|
2571
|
-
const value = inline ??
|
|
2575
|
+
const label = (inline, resolved2, omitted) => {
|
|
2576
|
+
const value = inline ?? resolved2;
|
|
2572
2577
|
if (value !== void 0 && value !== null) return truncate2(value);
|
|
2573
2578
|
if (!omitted) return "(none)";
|
|
2574
2579
|
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
@@ -3406,14 +3411,383 @@ It is INERT and cannot run yet. Ask the user to open Scenarios on the device, re
|
|
|
3406
3411
|
);
|
|
3407
3412
|
}
|
|
3408
3413
|
|
|
3409
|
-
// src/tools/packs/
|
|
3414
|
+
// src/tools/packs/camera.ts
|
|
3410
3415
|
import { z as z15 } from "zod";
|
|
3411
|
-
|
|
3416
|
+
|
|
3417
|
+
// src/capture/camera.ts
|
|
3418
|
+
import { execFile as execFile2 } from "child_process";
|
|
3419
|
+
import { promisify as promisify2 } from "util";
|
|
3420
|
+
import { existsSync, readFileSync } from "fs";
|
|
3421
|
+
import os3 from "os";
|
|
3422
|
+
import path3 from "path";
|
|
3423
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3424
|
+
function candidates() {
|
|
3425
|
+
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3426
|
+
const list = [
|
|
3427
|
+
...fromEnv ? [path3.join(fromEnv, "buoycam")] : [],
|
|
3428
|
+
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3429
|
+
path3.join(os3.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3430
|
+
];
|
|
3431
|
+
let dir = __dirname;
|
|
3432
|
+
for (let i = 0; i < 8; i++) {
|
|
3433
|
+
list.push(path3.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3434
|
+
const parent = path3.dirname(dir);
|
|
3435
|
+
if (parent === dir) break;
|
|
3436
|
+
dir = parent;
|
|
3437
|
+
}
|
|
3438
|
+
return list;
|
|
3439
|
+
}
|
|
3440
|
+
var resolved = null;
|
|
3441
|
+
function cliPath() {
|
|
3442
|
+
if (resolved) return resolved;
|
|
3443
|
+
resolved = candidates().find((c) => existsSync(c)) ?? null;
|
|
3444
|
+
return resolved;
|
|
3445
|
+
}
|
|
3446
|
+
async function run(args) {
|
|
3447
|
+
const cli = cliPath();
|
|
3448
|
+
if (!cli) {
|
|
3449
|
+
return {
|
|
3450
|
+
ok: false,
|
|
3451
|
+
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`."
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
try {
|
|
3455
|
+
const { stdout } = await execFileAsync2(cli, [...args, "--json"], { timeout: 3e4 });
|
|
3456
|
+
return { ok: true, data: JSON.parse(stdout) };
|
|
3457
|
+
} catch (err) {
|
|
3458
|
+
const stdout = err.stdout ?? "";
|
|
3459
|
+
try {
|
|
3460
|
+
return { ok: false, data: JSON.parse(stdout) };
|
|
3461
|
+
} catch {
|
|
3462
|
+
return {
|
|
3463
|
+
ok: false,
|
|
3464
|
+
error: err.message || "the simulator camera CLI failed"
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
function describeDiagnosis(checks) {
|
|
3470
|
+
const failed = checks.filter((c) => c.level === "fail");
|
|
3471
|
+
const warned = checks.filter((c) => c.level === "warn");
|
|
3472
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
3473
|
+
return "The simulator camera is healthy \u2014 every check passed.";
|
|
3474
|
+
}
|
|
3475
|
+
const lines = [];
|
|
3476
|
+
for (const c of [...failed, ...warned]) {
|
|
3477
|
+
lines.push(`- **${c.name}** (${c.level}): ${c.detail}`);
|
|
3478
|
+
if (c.fix) lines.push(` ${c.fix}`);
|
|
3479
|
+
}
|
|
3480
|
+
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.`;
|
|
3481
|
+
return [headline, "", ...lines].join("\n");
|
|
3482
|
+
}
|
|
3483
|
+
function describeDevices(devices, apps) {
|
|
3484
|
+
if (devices.length === 0) {
|
|
3485
|
+
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._";
|
|
3486
|
+
}
|
|
3487
|
+
const lines = [
|
|
3488
|
+
devices.length === 1 ? "1 booted simulator:" : `${devices.length} booted simulators:`,
|
|
3489
|
+
...devices.map((d) => `- **${d.name}** (${d.runtime}) \u2014 \`${d.udid}\``)
|
|
3490
|
+
];
|
|
3491
|
+
if (devices.length > 1 && apps === null) {
|
|
3492
|
+
lines.push(
|
|
3493
|
+
"",
|
|
3494
|
+
"_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._"
|
|
3495
|
+
);
|
|
3496
|
+
return lines.join("\n");
|
|
3497
|
+
}
|
|
3498
|
+
if (apps === null) {
|
|
3499
|
+
lines.push("", "_Could not read the installed apps._");
|
|
3500
|
+
return lines.join("\n");
|
|
3501
|
+
}
|
|
3502
|
+
if (apps.length === 0) {
|
|
3503
|
+
lines.push("", "No apps installed on it.");
|
|
3504
|
+
return lines.join("\n");
|
|
3505
|
+
}
|
|
3506
|
+
lines.push(
|
|
3507
|
+
"",
|
|
3508
|
+
`${apps.length} installed app${apps.length === 1 ? "" : "s"}:`,
|
|
3509
|
+
...apps.map((a) => `- ${a.name} \u2014 \`${a.bundleId}\``),
|
|
3510
|
+
"",
|
|
3511
|
+
"_Pass one of these bundle ids to `camera_launch` to relaunch it with the camera attached._"
|
|
3512
|
+
);
|
|
3513
|
+
return lines.join("\n");
|
|
3514
|
+
}
|
|
3515
|
+
function describeInputs(cameras, windows) {
|
|
3516
|
+
const lines = [];
|
|
3517
|
+
lines.push(cameras.length ? '**Mac cameras** (for `kind: "camera"`):' : "**Mac cameras**: none found.");
|
|
3518
|
+
if (cameras.length) {
|
|
3519
|
+
lines.push(...cameras.map((c) => `- ${c.name} \u2014 \`${c.uniqueId}\``));
|
|
3520
|
+
} else {
|
|
3521
|
+
lines.push(
|
|
3522
|
+
"_If this Mac has a webcam, Buoy has not been granted Camera permission (System Settings \u2192 Privacy & Security \u2192 Camera)._"
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
lines.push("");
|
|
3526
|
+
lines.push(
|
|
3527
|
+
windows.length ? '**Capturable windows** (for `kind: "screen"`, `value: "window:<id>"`):' : "**Capturable windows**: none found."
|
|
3528
|
+
);
|
|
3529
|
+
if (windows.length) {
|
|
3530
|
+
lines.push(
|
|
3531
|
+
...windows.map((w) => `- ${w.app}${w.title ? ` \u2014 ${w.title}` : ""} \u2014 \`window:${w.id}\``)
|
|
3532
|
+
);
|
|
3533
|
+
} else {
|
|
3534
|
+
lines.push(
|
|
3535
|
+
"_Screen Recording permission is almost certainly missing (System Settings \u2192 Privacy & Security \u2192 Screen & System Audio Recording)._"
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
return lines.join("\n");
|
|
3539
|
+
}
|
|
3540
|
+
function entitlementPaths() {
|
|
3541
|
+
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3542
|
+
if (fromEnv) return [fromEnv];
|
|
3543
|
+
return [
|
|
3544
|
+
path3.join(os3.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
3545
|
+
path3.join(os3.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3546
|
+
];
|
|
3547
|
+
}
|
|
3548
|
+
function readEntitlement() {
|
|
3549
|
+
for (const p of entitlementPaths()) {
|
|
3550
|
+
try {
|
|
3551
|
+
if (!existsSync(p)) continue;
|
|
3552
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
3553
|
+
return {
|
|
3554
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "anonymous",
|
|
3555
|
+
isPro: parsed.isPro === true
|
|
3556
|
+
};
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
return { tier: "anonymous", isPro: false };
|
|
3561
|
+
}
|
|
3562
|
+
function proRequired(tool) {
|
|
3563
|
+
return `\`${tool}\` is a Buoy Pro feature.
|
|
3564
|
+
|
|
3565
|
+
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.
|
|
3566
|
+
|
|
3567
|
+
_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._`;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
// src/tools/packs/camera.ts
|
|
3412
3571
|
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3572
|
+
var gate = (tool) => readEntitlement().isPro ? null : fail15(proRequired(tool));
|
|
3413
3573
|
var fail15 = (message) => ({
|
|
3414
3574
|
isError: true,
|
|
3415
3575
|
content: [{ type: "text", text: message }]
|
|
3416
3576
|
});
|
|
3577
|
+
function registerCameraTools(server) {
|
|
3578
|
+
server.registerTool(
|
|
3579
|
+
"camera_source",
|
|
3580
|
+
{
|
|
3581
|
+
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.",
|
|
3582
|
+
inputSchema: {
|
|
3583
|
+
kind: z15.enum(["barcode", "image", "video", "camera", "screen", "pattern"]).describe("What the camera should show."),
|
|
3584
|
+
value: z15.string().optional().describe(
|
|
3585
|
+
"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`."
|
|
3586
|
+
),
|
|
3587
|
+
format: z15.enum(["qr", "pdf417", "aztec", "code128"]).optional().describe("Barcode symbology; defaults to qr.")
|
|
3588
|
+
}
|
|
3589
|
+
},
|
|
3590
|
+
async (args) => {
|
|
3591
|
+
const blocked = gate("camera_source");
|
|
3592
|
+
if (blocked) return blocked;
|
|
3593
|
+
const cmd = ["source", args.kind];
|
|
3594
|
+
if (args.value) cmd.push(args.value);
|
|
3595
|
+
if (args.format) cmd.push("--format", args.format);
|
|
3596
|
+
const result = await run(cmd);
|
|
3597
|
+
if (!result.ok) {
|
|
3598
|
+
return fail15(
|
|
3599
|
+
result.error ?? result.data?.error ?? "could not set the camera source"
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
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;
|
|
3603
|
+
return text13(
|
|
3604
|
+
`The simulator camera is now showing ${what}, at ${result.data?.fps ?? 0} fps.
|
|
3605
|
+
|
|
3606
|
+
_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._`
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
);
|
|
3610
|
+
server.registerTool(
|
|
3611
|
+
"camera_status",
|
|
3612
|
+
{
|
|
3613
|
+
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.",
|
|
3614
|
+
inputSchema: {}
|
|
3615
|
+
},
|
|
3616
|
+
async () => {
|
|
3617
|
+
const blocked = gate("camera_status");
|
|
3618
|
+
if (blocked) return blocked;
|
|
3619
|
+
const result = await run(["status"]);
|
|
3620
|
+
if (!result.ok || !result.data) {
|
|
3621
|
+
return fail15(result.error ?? "could not read the simulator camera status");
|
|
3622
|
+
}
|
|
3623
|
+
const { publishing, fps, codes } = result.data;
|
|
3624
|
+
if (!publishing) {
|
|
3625
|
+
return text13(
|
|
3626
|
+
"The simulator camera is not publishing anything.\n\n_Set a source with `camera_source` first._"
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
return text13(
|
|
3630
|
+
[
|
|
3631
|
+
`Publishing at ${fps} fps.`,
|
|
3632
|
+
codes.length ? `Currently decoding: ${codes.map((c) => `\`${c}\``).join(", ")}` : "No barcode in the current frame.",
|
|
3633
|
+
"",
|
|
3634
|
+
"_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._"
|
|
3635
|
+
].join("\n")
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
);
|
|
3639
|
+
server.registerTool(
|
|
3640
|
+
"camera_launch",
|
|
3641
|
+
{
|
|
3642
|
+
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.",
|
|
3643
|
+
inputSchema: {
|
|
3644
|
+
bundleId: z15.string().describe("The app's bundle identifier."),
|
|
3645
|
+
udid: z15.string().optional().describe("Simulator udid, if more than one is booted.")
|
|
3646
|
+
}
|
|
3647
|
+
},
|
|
3648
|
+
async (args) => {
|
|
3649
|
+
const blocked = gate("camera_launch");
|
|
3650
|
+
if (blocked) return blocked;
|
|
3651
|
+
const cmd = ["launch", args.bundleId];
|
|
3652
|
+
if (args.udid) cmd.push("--udid", args.udid);
|
|
3653
|
+
const result = await run(cmd);
|
|
3654
|
+
if (!result.ok) {
|
|
3655
|
+
return fail15(result.error ?? result.data?.error ?? "could not launch the app");
|
|
3656
|
+
}
|
|
3657
|
+
return text13(
|
|
3658
|
+
`Relaunched \`${args.bundleId}\` with the camera attached${result.data?.pid ? ` (pid ${result.data.pid})` : ""}.`
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
);
|
|
3662
|
+
server.registerTool(
|
|
3663
|
+
"camera_diagnose",
|
|
3664
|
+
{
|
|
3665
|
+
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.",
|
|
3666
|
+
inputSchema: {
|
|
3667
|
+
bundleId: z15.string().optional().describe("Check a specific app \u2014 whether the library is actually loaded in it.")
|
|
3668
|
+
}
|
|
3669
|
+
},
|
|
3670
|
+
async (args) => {
|
|
3671
|
+
const cmd = ["diagnose"];
|
|
3672
|
+
if (args.bundleId) cmd.push(args.bundleId);
|
|
3673
|
+
const result = await run(cmd);
|
|
3674
|
+
if (!result.data?.checks) {
|
|
3675
|
+
return fail15(result.error ?? "could not run the simulator camera diagnosis");
|
|
3676
|
+
}
|
|
3677
|
+
const ent = readEntitlement();
|
|
3678
|
+
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._";
|
|
3679
|
+
return text13(describeDiagnosis(result.data.checks) + tierNote);
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
server.registerTool(
|
|
3683
|
+
"camera_stop",
|
|
3684
|
+
{
|
|
3685
|
+
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.",
|
|
3686
|
+
inputSchema: {}
|
|
3687
|
+
},
|
|
3688
|
+
async () => {
|
|
3689
|
+
const blocked = gate("camera_stop");
|
|
3690
|
+
if (blocked) return blocked;
|
|
3691
|
+
const result = await run(["stop"]);
|
|
3692
|
+
if (!result.ok) {
|
|
3693
|
+
return fail15(result.error ?? result.data?.error ?? "could not stop the camera");
|
|
3694
|
+
}
|
|
3695
|
+
return text13("The simulator camera is off.");
|
|
3696
|
+
}
|
|
3697
|
+
);
|
|
3698
|
+
server.registerTool(
|
|
3699
|
+
"camera_devices",
|
|
3700
|
+
{
|
|
3701
|
+
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.",
|
|
3702
|
+
inputSchema: {
|
|
3703
|
+
udid: z15.string().optional().describe("Which simulator's apps to list. Only needed when several are booted.")
|
|
3704
|
+
}
|
|
3705
|
+
},
|
|
3706
|
+
async (args) => {
|
|
3707
|
+
const blocked = gate("camera_devices");
|
|
3708
|
+
if (blocked) return blocked;
|
|
3709
|
+
const devicesResult = await run(["devices"]);
|
|
3710
|
+
if (!devicesResult.ok || !devicesResult.data) {
|
|
3711
|
+
return fail15(devicesResult.error ?? "could not list booted simulators");
|
|
3712
|
+
}
|
|
3713
|
+
const devices = devicesResult.data.devices ?? [];
|
|
3714
|
+
let apps = null;
|
|
3715
|
+
if (devices.length === 1 || args.udid) {
|
|
3716
|
+
const appsResult = await run([
|
|
3717
|
+
"apps",
|
|
3718
|
+
...args.udid ? ["--udid", args.udid] : []
|
|
3719
|
+
]);
|
|
3720
|
+
apps = appsResult.ok ? appsResult.data?.apps ?? [] : null;
|
|
3721
|
+
}
|
|
3722
|
+
return text13(describeDevices(devices, apps));
|
|
3723
|
+
}
|
|
3724
|
+
);
|
|
3725
|
+
server.registerTool(
|
|
3726
|
+
"camera_inputs",
|
|
3727
|
+
{
|
|
3728
|
+
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.",
|
|
3729
|
+
inputSchema: {}
|
|
3730
|
+
},
|
|
3731
|
+
async () => {
|
|
3732
|
+
const blocked = gate("camera_inputs");
|
|
3733
|
+
if (blocked) return blocked;
|
|
3734
|
+
const [cams, wins] = await Promise.all([
|
|
3735
|
+
run(["cameras"]),
|
|
3736
|
+
run(["windows"])
|
|
3737
|
+
]);
|
|
3738
|
+
if (!cams.ok && !wins.ok) {
|
|
3739
|
+
return fail15(
|
|
3740
|
+
cams.error ?? wins.error ?? "could not list the Mac's cameras or windows"
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
return text13(
|
|
3744
|
+
describeInputs(cams.data?.cameras ?? [], wins.data?.windows ?? [])
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
);
|
|
3748
|
+
server.registerTool(
|
|
3749
|
+
"camera_zero_setup",
|
|
3750
|
+
{
|
|
3751
|
+
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.",
|
|
3752
|
+
inputSchema: {
|
|
3753
|
+
action: z15.enum(["on", "off", "status"]).describe("Turn it on, off, or report the current state."),
|
|
3754
|
+
udid: z15.string().optional().describe("Which simulator. Only needed when several are booted.")
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
async (args) => {
|
|
3758
|
+
const blocked = gate("camera_zero_setup");
|
|
3759
|
+
if (blocked) return blocked;
|
|
3760
|
+
const result = await run(["zero-setup", args.action, ...args.udid ? ["--udid", args.udid] : []]);
|
|
3761
|
+
if (!result.ok) {
|
|
3762
|
+
return fail15(
|
|
3763
|
+
result.error ?? result.data?.error ?? "could not change zero-setup mode"
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3766
|
+
const d = result.data ?? {};
|
|
3767
|
+
if (args.action === "status") {
|
|
3768
|
+
if (!d.enabled) return text13("Zero-setup mode is **off**.");
|
|
3769
|
+
return text13(
|
|
3770
|
+
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."
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3773
|
+
if (args.action === "on") {
|
|
3774
|
+
return text13(
|
|
3775
|
+
"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._"
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
return text13("Zero-setup mode is **off**.");
|
|
3779
|
+
}
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
// src/tools/packs/interact.ts
|
|
3784
|
+
import { z as z16 } from "zod";
|
|
3785
|
+
var TOOL_ID2 = "highlight-updates";
|
|
3786
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3787
|
+
var fail16 = (message) => ({
|
|
3788
|
+
isError: true,
|
|
3789
|
+
content: [{ type: "text", text: message }]
|
|
3790
|
+
});
|
|
3417
3791
|
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
3418
3792
|
function formatElement(e) {
|
|
3419
3793
|
let line = e.interactive ? "\u2022 " : " ";
|
|
@@ -3437,8 +3811,8 @@ function registerInteractTools(server, sync) {
|
|
|
3437
3811
|
{
|
|
3438
3812
|
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.",
|
|
3439
3813
|
inputSchema: {
|
|
3440
|
-
interactiveOnly:
|
|
3441
|
-
deviceId:
|
|
3814
|
+
interactiveOnly: z16.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
3815
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3442
3816
|
}
|
|
3443
3817
|
},
|
|
3444
3818
|
async (args) => {
|
|
@@ -3448,16 +3822,16 @@ function registerInteractTools(server, sync) {
|
|
|
3448
3822
|
const all = res?.elements ?? [];
|
|
3449
3823
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
3450
3824
|
if (elements.length === 0) {
|
|
3451
|
-
return
|
|
3825
|
+
return text14(
|
|
3452
3826
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
3453
3827
|
);
|
|
3454
3828
|
}
|
|
3455
3829
|
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 }).`;
|
|
3456
|
-
return
|
|
3830
|
+
return text14(`${header}
|
|
3457
3831
|
|
|
3458
3832
|
${elements.map(formatElement).join("\n")}`);
|
|
3459
3833
|
} catch (e) {
|
|
3460
|
-
return
|
|
3834
|
+
return fail16(errMsg15(e));
|
|
3461
3835
|
}
|
|
3462
3836
|
}
|
|
3463
3837
|
);
|
|
@@ -3466,20 +3840,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3466
3840
|
{
|
|
3467
3841
|
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.",
|
|
3468
3842
|
inputSchema: {
|
|
3469
|
-
nativeTag:
|
|
3470
|
-
testID:
|
|
3471
|
-
query:
|
|
3472
|
-
value:
|
|
3473
|
-
text:
|
|
3474
|
-
longPress:
|
|
3475
|
-
scrollIntoView:
|
|
3476
|
-
deviceId:
|
|
3843
|
+
nativeTag: z16.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
3844
|
+
testID: z16.string().optional().describe("Exact testID of the element."),
|
|
3845
|
+
query: z16.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
3846
|
+
value: z16.union([z16.number(), z16.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
3847
|
+
text: z16.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
3848
|
+
longPress: z16.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
3849
|
+
scrollIntoView: z16.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
3850
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3477
3851
|
}
|
|
3478
3852
|
},
|
|
3479
3853
|
async (args) => {
|
|
3480
3854
|
try {
|
|
3481
3855
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
3482
|
-
return
|
|
3856
|
+
return fail16("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
3483
3857
|
}
|
|
3484
3858
|
const target = sync.requireProDevice(args.deviceId);
|
|
3485
3859
|
const res = await sync.callAction(
|
|
@@ -3504,25 +3878,25 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
3504
3878
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
3505
3879
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
3506
3880
|
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}`;
|
|
3507
|
-
return
|
|
3881
|
+
return text14(`${verb}${scrolledNote}.${flowTip}`);
|
|
3508
3882
|
}
|
|
3509
3883
|
let msg = res?.reason ?? "Tap failed.";
|
|
3510
3884
|
if (res?.candidates?.length) {
|
|
3511
3885
|
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");
|
|
3512
3886
|
}
|
|
3513
|
-
return
|
|
3887
|
+
return fail16(msg);
|
|
3514
3888
|
} catch (e) {
|
|
3515
|
-
return
|
|
3889
|
+
return fail16(errMsg15(e));
|
|
3516
3890
|
}
|
|
3517
3891
|
}
|
|
3518
3892
|
);
|
|
3519
3893
|
}
|
|
3520
3894
|
|
|
3521
3895
|
// src/tools/packs/jsThread.ts
|
|
3522
|
-
import { z as
|
|
3896
|
+
import { z as z17 } from "zod";
|
|
3523
3897
|
var JS_TOP_TOOL_ID = "js-top";
|
|
3524
|
-
var
|
|
3525
|
-
var
|
|
3898
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3899
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3526
3900
|
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3527
3901
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
3528
3902
|
function sparkline(values) {
|
|
@@ -3594,9 +3968,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
3594
3968
|
{
|
|
3595
3969
|
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.',
|
|
3596
3970
|
inputSchema: {
|
|
3597
|
-
deviceId:
|
|
3598
|
-
durationMs:
|
|
3599
|
-
clearFirst:
|
|
3971
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3972
|
+
durationMs: z17.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
3973
|
+
clearFirst: z17.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
3600
3974
|
}
|
|
3601
3975
|
},
|
|
3602
3976
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -3609,7 +3983,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
3609
3983
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
3610
3984
|
target
|
|
3611
3985
|
);
|
|
3612
|
-
if (!snap) return
|
|
3986
|
+
if (!snap) return fail17("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
3613
3987
|
const md = formatTopMarkdown(snap, duration);
|
|
3614
3988
|
const slim = {
|
|
3615
3989
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -3628,13 +4002,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
3628
4002
|
worstFreezeMs: r.worstFreezeMs
|
|
3629
4003
|
}))
|
|
3630
4004
|
};
|
|
3631
|
-
return
|
|
4005
|
+
return text15(`${md}
|
|
3632
4006
|
|
|
3633
4007
|
\`\`\`json
|
|
3634
4008
|
${JSON.stringify(slim)}
|
|
3635
4009
|
\`\`\``);
|
|
3636
4010
|
} catch (e) {
|
|
3637
|
-
return
|
|
4011
|
+
return fail17(errMsg16(e));
|
|
3638
4012
|
}
|
|
3639
4013
|
}
|
|
3640
4014
|
);
|
|
@@ -3643,8 +4017,8 @@ ${JSON.stringify(slim)}
|
|
|
3643
4017
|
{
|
|
3644
4018
|
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).',
|
|
3645
4019
|
inputSchema: {
|
|
3646
|
-
deviceId:
|
|
3647
|
-
key:
|
|
4020
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4021
|
+
key: z17.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
3648
4022
|
}
|
|
3649
4023
|
},
|
|
3650
4024
|
async ({ deviceId, key }) => {
|
|
@@ -3657,18 +4031,18 @@ ${JSON.stringify(slim)}
|
|
|
3657
4031
|
target
|
|
3658
4032
|
);
|
|
3659
4033
|
if (!detail) {
|
|
3660
|
-
return
|
|
4034
|
+
return fail17(
|
|
3661
4035
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
3662
4036
|
);
|
|
3663
4037
|
}
|
|
3664
4038
|
const md = formatDetailMarkdown(detail);
|
|
3665
|
-
return
|
|
4039
|
+
return text15(`${md}
|
|
3666
4040
|
|
|
3667
4041
|
\`\`\`json
|
|
3668
4042
|
${JSON.stringify(detail)}
|
|
3669
4043
|
\`\`\``);
|
|
3670
4044
|
} catch (e) {
|
|
3671
|
-
return
|
|
4045
|
+
return fail17(errMsg16(e));
|
|
3672
4046
|
}
|
|
3673
4047
|
}
|
|
3674
4048
|
);
|
|
@@ -3677,26 +4051,26 @@ ${JSON.stringify(detail)}
|
|
|
3677
4051
|
{
|
|
3678
4052
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
3679
4053
|
inputSchema: {
|
|
3680
|
-
deviceId:
|
|
4054
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3681
4055
|
}
|
|
3682
4056
|
},
|
|
3683
4057
|
async ({ deviceId }) => {
|
|
3684
4058
|
try {
|
|
3685
4059
|
const target = sync.requireProDevice(deviceId);
|
|
3686
4060
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
3687
|
-
return
|
|
4061
|
+
return text15("Cleared js-top stats.");
|
|
3688
4062
|
} catch (e) {
|
|
3689
|
-
return
|
|
4063
|
+
return fail17(errMsg16(e));
|
|
3690
4064
|
}
|
|
3691
4065
|
}
|
|
3692
4066
|
);
|
|
3693
4067
|
}
|
|
3694
4068
|
|
|
3695
4069
|
// src/tools/packs/images.ts
|
|
3696
|
-
import { z as
|
|
4070
|
+
import { z as z18 } from "zod";
|
|
3697
4071
|
var IMAGES_TOOL_ID = "images";
|
|
3698
|
-
var
|
|
3699
|
-
var
|
|
4072
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4073
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3700
4074
|
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3701
4075
|
function uriTail(uri) {
|
|
3702
4076
|
if (!uri) return "(no source)";
|
|
@@ -3785,9 +4159,9 @@ function registerImagesTools(server, sync) {
|
|
|
3785
4159
|
{
|
|
3786
4160
|
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.",
|
|
3787
4161
|
inputSchema: {
|
|
3788
|
-
deviceId:
|
|
3789
|
-
limit:
|
|
3790
|
-
status:
|
|
4162
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4163
|
+
limit: z18.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
4164
|
+
status: z18.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
3791
4165
|
}
|
|
3792
4166
|
},
|
|
3793
4167
|
async ({ deviceId, limit, status }) => {
|
|
@@ -3800,7 +4174,7 @@ function registerImagesTools(server, sync) {
|
|
|
3800
4174
|
{ limit: capped, status },
|
|
3801
4175
|
target
|
|
3802
4176
|
);
|
|
3803
|
-
if (!list) return
|
|
4177
|
+
if (!list) return fail18("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
3804
4178
|
const md = formatListMarkdown(list, capped);
|
|
3805
4179
|
const slim = {
|
|
3806
4180
|
stats: list.stats,
|
|
@@ -3827,13 +4201,13 @@ function registerImagesTools(server, sync) {
|
|
|
3827
4201
|
layoutShifts: r.layoutShifts
|
|
3828
4202
|
}))
|
|
3829
4203
|
};
|
|
3830
|
-
return
|
|
4204
|
+
return text16(`${md}
|
|
3831
4205
|
|
|
3832
4206
|
\`\`\`json
|
|
3833
4207
|
${JSON.stringify(slim)}
|
|
3834
4208
|
\`\`\``);
|
|
3835
4209
|
} catch (e) {
|
|
3836
|
-
return
|
|
4210
|
+
return fail18(errMsg17(e));
|
|
3837
4211
|
}
|
|
3838
4212
|
}
|
|
3839
4213
|
);
|
|
@@ -3842,9 +4216,9 @@ ${JSON.stringify(slim)}
|
|
|
3842
4216
|
{
|
|
3843
4217
|
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.",
|
|
3844
4218
|
inputSchema: {
|
|
3845
|
-
deviceId:
|
|
3846
|
-
id:
|
|
3847
|
-
action:
|
|
4219
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4220
|
+
id: z18.number().describe("Record id from get_images."),
|
|
4221
|
+
action: z18.enum([
|
|
3848
4222
|
"hard_reload",
|
|
3849
4223
|
"retry",
|
|
3850
4224
|
"evict_disk",
|
|
@@ -3856,7 +4230,7 @@ ${JSON.stringify(slim)}
|
|
|
3856
4230
|
"swap_url",
|
|
3857
4231
|
"restore"
|
|
3858
4232
|
]).describe("What to do."),
|
|
3859
|
-
url:
|
|
4233
|
+
url: z18.string().optional().describe("Replacement URL (swap_url only).")
|
|
3860
4234
|
}
|
|
3861
4235
|
},
|
|
3862
4236
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -3864,8 +4238,8 @@ ${JSON.stringify(slim)}
|
|
|
3864
4238
|
const target = sync.requireProDevice(deviceId);
|
|
3865
4239
|
if (action === "prove_savings") {
|
|
3866
4240
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
3867
|
-
if (!savings) return
|
|
3868
|
-
if (!savings.ok) return
|
|
4241
|
+
if (!savings) return fail18("Device returned nothing for proveSavings.");
|
|
4242
|
+
if (!savings.ok) return fail18(savings.message);
|
|
3869
4243
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
3870
4244
|
if (savings.originalBytes != null)
|
|
3871
4245
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -3877,11 +4251,11 @@ ${JSON.stringify(slim)}
|
|
|
3877
4251
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
3878
4252
|
if (savings.optimizedUri)
|
|
3879
4253
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
3880
|
-
return
|
|
4254
|
+
return text16(parts.join("\n"));
|
|
3881
4255
|
}
|
|
3882
4256
|
const entry = ACTION_MAP[action];
|
|
3883
|
-
if (!entry) return
|
|
3884
|
-
if (action === "swap_url" && !url) return
|
|
4257
|
+
if (!entry) return fail18(`Unknown action: ${action}`);
|
|
4258
|
+
if (action === "swap_url" && !url) return fail18("swap_url requires the `url` param.");
|
|
3885
4259
|
const result = await sync.callAction(
|
|
3886
4260
|
IMAGES_TOOL_ID,
|
|
3887
4261
|
entry.action,
|
|
@@ -3901,9 +4275,9 @@ ${JSON.stringify(slim)}
|
|
|
3901
4275
|
);
|
|
3902
4276
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
3903
4277
|
}
|
|
3904
|
-
return
|
|
4278
|
+
return text16(lines.join("\n"));
|
|
3905
4279
|
} catch (e) {
|
|
3906
|
-
return
|
|
4280
|
+
return fail18(errMsg17(e));
|
|
3907
4281
|
}
|
|
3908
4282
|
}
|
|
3909
4283
|
);
|
|
@@ -3912,12 +4286,12 @@ ${JSON.stringify(slim)}
|
|
|
3912
4286
|
{
|
|
3913
4287
|
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.",
|
|
3914
4288
|
inputSchema: {
|
|
3915
|
-
deviceId:
|
|
3916
|
-
network:
|
|
3917
|
-
blankImages:
|
|
3918
|
-
massAction:
|
|
3919
|
-
clearRegistry:
|
|
3920
|
-
clearExpoCaches:
|
|
4289
|
+
deviceId: z18.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4290
|
+
network: z18.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4291
|
+
blankImages: z18.boolean().optional().describe("Render all images without a source."),
|
|
4292
|
+
massAction: z18.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."),
|
|
4293
|
+
clearRegistry: z18.boolean().optional().describe("Clear the captured image log."),
|
|
4294
|
+
clearExpoCaches: z18.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
3921
4295
|
}
|
|
3922
4296
|
},
|
|
3923
4297
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3944,17 +4318,17 @@ ${JSON.stringify(slim)}
|
|
|
3944
4318
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3945
4319
|
done.push(r?.message ?? "expo caches cleared");
|
|
3946
4320
|
}
|
|
3947
|
-
if (done.length === 0) return
|
|
3948
|
-
return
|
|
4321
|
+
if (done.length === 0) return fail18("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
4322
|
+
return text16(`Applied: ${done.join(" \xB7 ")}`);
|
|
3949
4323
|
} catch (e) {
|
|
3950
|
-
return
|
|
4324
|
+
return fail18(errMsg17(e));
|
|
3951
4325
|
}
|
|
3952
4326
|
}
|
|
3953
4327
|
);
|
|
3954
4328
|
}
|
|
3955
4329
|
|
|
3956
4330
|
// src/tools/packs/assets.ts
|
|
3957
|
-
import { z as
|
|
4331
|
+
import { z as z19 } from "zod";
|
|
3958
4332
|
|
|
3959
4333
|
// src/tools/packs/assetsGraphNote.ts
|
|
3960
4334
|
function graphUnavailableNote(ss) {
|
|
@@ -3969,8 +4343,8 @@ function graphUnavailableNote(ss) {
|
|
|
3969
4343
|
|
|
3970
4344
|
// src/tools/packs/assets.ts
|
|
3971
4345
|
var ASSETS_TOOL_ID = "assets";
|
|
3972
|
-
var
|
|
3973
|
-
var
|
|
4346
|
+
var text17 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4347
|
+
var fail19 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3974
4348
|
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3975
4349
|
function kb2(bytes) {
|
|
3976
4350
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
@@ -4059,10 +4433,10 @@ function registerAssetsTools(server, sync) {
|
|
|
4059
4433
|
{
|
|
4060
4434
|
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.",
|
|
4061
4435
|
inputSchema: {
|
|
4062
|
-
deviceId:
|
|
4063
|
-
limit:
|
|
4064
|
-
kind:
|
|
4065
|
-
unusedOnly:
|
|
4436
|
+
deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4437
|
+
limit: z19.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
4438
|
+
kind: z19.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
4439
|
+
unusedOnly: z19.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
4066
4440
|
}
|
|
4067
4441
|
},
|
|
4068
4442
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -4075,7 +4449,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4075
4449
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
4076
4450
|
target
|
|
4077
4451
|
);
|
|
4078
|
-
if (!list) return
|
|
4452
|
+
if (!list) return fail19("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
4079
4453
|
if (unusedOnly) {
|
|
4080
4454
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
4081
4455
|
}
|
|
@@ -4099,13 +4473,13 @@ function registerAssetsTools(server, sync) {
|
|
|
4099
4473
|
sizeSource: r.sizeSource
|
|
4100
4474
|
}))
|
|
4101
4475
|
};
|
|
4102
|
-
return
|
|
4476
|
+
return text17(`${md}
|
|
4103
4477
|
|
|
4104
4478
|
\`\`\`json
|
|
4105
4479
|
${JSON.stringify(slim)}
|
|
4106
4480
|
\`\`\``);
|
|
4107
4481
|
} catch (e) {
|
|
4108
|
-
return
|
|
4482
|
+
return fail19(errMsg18(e));
|
|
4109
4483
|
}
|
|
4110
4484
|
}
|
|
4111
4485
|
);
|
|
@@ -4114,9 +4488,9 @@ ${JSON.stringify(slim)}
|
|
|
4114
4488
|
{
|
|
4115
4489
|
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).",
|
|
4116
4490
|
inputSchema: {
|
|
4117
|
-
deviceId:
|
|
4118
|
-
action:
|
|
4119
|
-
id:
|
|
4491
|
+
deviceId: z19.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
4492
|
+
action: z19.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
4493
|
+
id: z19.number().optional().describe("Asset id from get_assets (detail only).")
|
|
4120
4494
|
}
|
|
4121
4495
|
},
|
|
4122
4496
|
async ({ deviceId, action, id }) => {
|
|
@@ -4126,19 +4500,19 @@ ${JSON.stringify(slim)}
|
|
|
4126
4500
|
case "rescan": {
|
|
4127
4501
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
4128
4502
|
const ss = result?.scanStatus;
|
|
4129
|
-
if (!ss) return
|
|
4130
|
-
return
|
|
4503
|
+
if (!ss) return fail19("Device returned nothing for rescan.");
|
|
4504
|
+
return text17(
|
|
4131
4505
|
`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" : "")
|
|
4132
4506
|
);
|
|
4133
4507
|
}
|
|
4134
4508
|
case "measure_sizes": {
|
|
4135
4509
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
4136
|
-
return
|
|
4510
|
+
return text17(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
4137
4511
|
}
|
|
4138
4512
|
case "detail": {
|
|
4139
|
-
if (id == null) return
|
|
4513
|
+
if (id == null) return fail19("detail requires the `id` param (from get_assets).");
|
|
4140
4514
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
4141
|
-
if (!detail) return
|
|
4515
|
+
if (!detail) return fail19(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
4142
4516
|
const lines = [
|
|
4143
4517
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
4144
4518
|
"",
|
|
@@ -4156,32 +4530,32 @@ ${JSON.stringify(slim)}
|
|
|
4156
4530
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
4157
4531
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
4158
4532
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
4159
|
-
return
|
|
4533
|
+
return text17(lines.join("\n"));
|
|
4160
4534
|
}
|
|
4161
4535
|
case "save_baseline": {
|
|
4162
4536
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
4163
|
-
return
|
|
4537
|
+
return text17(
|
|
4164
4538
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
4165
4539
|
);
|
|
4166
4540
|
}
|
|
4167
4541
|
case "clear_baseline": {
|
|
4168
4542
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
4169
|
-
return
|
|
4543
|
+
return text17("Baseline cleared.");
|
|
4170
4544
|
}
|
|
4171
4545
|
case "clear_records": {
|
|
4172
4546
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
4173
|
-
return
|
|
4547
|
+
return text17(result?.message ?? "Inventory cleared.");
|
|
4174
4548
|
}
|
|
4175
4549
|
}
|
|
4176
4550
|
} catch (e) {
|
|
4177
|
-
return
|
|
4551
|
+
return fail19(errMsg18(e));
|
|
4178
4552
|
}
|
|
4179
4553
|
}
|
|
4180
4554
|
);
|
|
4181
4555
|
}
|
|
4182
4556
|
|
|
4183
4557
|
// src/tools/packs/reload.ts
|
|
4184
|
-
import { z as
|
|
4558
|
+
import { z as z20 } from "zod";
|
|
4185
4559
|
|
|
4186
4560
|
// src/capture/devServerReload.ts
|
|
4187
4561
|
import { WebSocket } from "ws";
|
|
@@ -4244,8 +4618,8 @@ var DEFAULT_WAIT_MS = 3e4;
|
|
|
4244
4618
|
var TEARDOWN_GRACE_MS = 1200;
|
|
4245
4619
|
var PROBE_TIMEOUT_MS = 1500;
|
|
4246
4620
|
var PROBE_INTERVAL_MS = 400;
|
|
4247
|
-
var
|
|
4248
|
-
var
|
|
4621
|
+
var text18 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4622
|
+
var fail20 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
4249
4623
|
var errMsg19 = (e) => e instanceof Error ? e.message : String(e);
|
|
4250
4624
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4251
4625
|
async function ping(sync, deviceId, timeoutMs) {
|
|
@@ -4285,12 +4659,12 @@ function registerReloadTools(server, sync) {
|
|
|
4285
4659
|
{
|
|
4286
4660
|
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.",
|
|
4287
4661
|
inputSchema: {
|
|
4288
|
-
strategy:
|
|
4662
|
+
strategy: z20.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4289
4663
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
4290
4664
|
),
|
|
4291
|
-
wait:
|
|
4292
|
-
timeoutMs:
|
|
4293
|
-
deviceId:
|
|
4665
|
+
wait: z20.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
4666
|
+
timeoutMs: z20.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
4667
|
+
deviceId: z20.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
4294
4668
|
}
|
|
4295
4669
|
},
|
|
4296
4670
|
async (args) => {
|
|
@@ -4332,28 +4706,28 @@ function registerReloadTools(server, sync) {
|
|
|
4332
4706
|
}
|
|
4333
4707
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
4334
4708
|
if (args.wait === false) {
|
|
4335
|
-
return
|
|
4709
|
+
return text18(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
4336
4710
|
}
|
|
4337
4711
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
4338
4712
|
if (elapsed == null) {
|
|
4339
|
-
return
|
|
4713
|
+
return fail20(
|
|
4340
4714
|
`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.`
|
|
4341
4715
|
);
|
|
4342
4716
|
}
|
|
4343
|
-
return
|
|
4717
|
+
return text18(
|
|
4344
4718
|
`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.`
|
|
4345
4719
|
);
|
|
4346
4720
|
} catch (e) {
|
|
4347
|
-
return
|
|
4721
|
+
return fail20(errMsg19(e));
|
|
4348
4722
|
}
|
|
4349
4723
|
}
|
|
4350
4724
|
);
|
|
4351
4725
|
}
|
|
4352
4726
|
|
|
4353
4727
|
// src/tools/packs/triage.ts
|
|
4354
|
-
import { z as
|
|
4355
|
-
var
|
|
4356
|
-
var
|
|
4728
|
+
import { z as z21 } from "zod";
|
|
4729
|
+
var text19 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4730
|
+
var fail21 = (message) => ({
|
|
4357
4731
|
isError: true,
|
|
4358
4732
|
content: [{ type: "text", text: message }]
|
|
4359
4733
|
});
|
|
@@ -4388,9 +4762,9 @@ async function otherBackends(sync, target, opts) {
|
|
|
4388
4762
|
return notes.length ? `
|
|
4389
4763
|
${notes.join(" \xB7 ")} \u2014 read with get_storage` : "";
|
|
4390
4764
|
}
|
|
4391
|
-
async function section(title,
|
|
4765
|
+
async function section(title, run2) {
|
|
4392
4766
|
try {
|
|
4393
|
-
const body = await
|
|
4767
|
+
const body = await run2();
|
|
4394
4768
|
return body ? `${title}
|
|
4395
4769
|
${body}` : null;
|
|
4396
4770
|
} catch (e) {
|
|
@@ -4409,7 +4783,7 @@ function registerTriageTools(server, sync) {
|
|
|
4409
4783
|
{
|
|
4410
4784
|
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.",
|
|
4411
4785
|
inputSchema: {
|
|
4412
|
-
deviceId:
|
|
4786
|
+
deviceId: z21.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4413
4787
|
}
|
|
4414
4788
|
},
|
|
4415
4789
|
async (args) => {
|
|
@@ -4516,7 +4890,7 @@ _Values: get_storage includeValues:true_`;
|
|
|
4516
4890
|
|
|
4517
4891
|
\u{1F6D1} ${sync.unresponsiveNote(target)}` : "";
|
|
4518
4892
|
const body = [consoleSec, netSec, storeSec, screenSec].filter(Boolean).join("\n\n");
|
|
4519
|
-
return
|
|
4893
|
+
return text19(
|
|
4520
4894
|
`${head}${banner}
|
|
4521
4895
|
|
|
4522
4896
|
${body}
|
|
@@ -4524,18 +4898,18 @@ ${body}
|
|
|
4524
4898
|
_Drill in with get_console, get_network_requests (includeBodies:true for response bodies), get_storage (includeValues:true), describe_screen._`
|
|
4525
4899
|
);
|
|
4526
4900
|
} catch (e) {
|
|
4527
|
-
return
|
|
4901
|
+
return fail21(errMsg20(e));
|
|
4528
4902
|
}
|
|
4529
4903
|
}
|
|
4530
4904
|
);
|
|
4531
4905
|
}
|
|
4532
4906
|
|
|
4533
4907
|
// src/tools/packs/flow.ts
|
|
4534
|
-
import { z as
|
|
4908
|
+
import { z as z22 } from "zod";
|
|
4535
4909
|
var UI_TOOL_ID = "highlight-updates";
|
|
4536
4910
|
var APP_TOOL_ID2 = "app";
|
|
4537
|
-
var
|
|
4538
|
-
var
|
|
4911
|
+
var text20 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
4912
|
+
var fail22 = (message) => ({
|
|
4539
4913
|
isError: true,
|
|
4540
4914
|
content: [{ type: "text", text: message }]
|
|
4541
4915
|
});
|
|
@@ -4556,19 +4930,19 @@ function formatElement2(e) {
|
|
|
4556
4930
|
const display = e.text ?? e.label;
|
|
4557
4931
|
return `${e.interactive ? "\u2022 " : " "}${e.name}${display ? ` "${display}"` : ""}${e.testID ? ` [testID=${e.testID}]` : ""}`;
|
|
4558
4932
|
}
|
|
4559
|
-
var stepSchema =
|
|
4560
|
-
tap:
|
|
4561
|
-
longPress:
|
|
4562
|
-
type:
|
|
4563
|
-
into:
|
|
4564
|
-
set:
|
|
4565
|
-
reload:
|
|
4566
|
-
waitMs:
|
|
4567
|
-
waitFor:
|
|
4568
|
-
waitGone:
|
|
4569
|
-
expect:
|
|
4570
|
-
expectGone:
|
|
4571
|
-
timeoutMs:
|
|
4933
|
+
var stepSchema = z22.object({
|
|
4934
|
+
tap: z22.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
4935
|
+
longPress: z22.boolean().optional().describe("Make `tap` a long-press instead."),
|
|
4936
|
+
type: z22.string().optional().describe("Text to type (needs `into`)."),
|
|
4937
|
+
into: z22.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
4938
|
+
set: z22.union([z22.number(), z22.boolean()]).optional().describe("Set a toggle (true/false) or slider (number) \u2014 pair with `tap` as the target."),
|
|
4939
|
+
reload: z22.boolean().optional().describe("Reload the JS bundle before this step's action."),
|
|
4940
|
+
waitMs: z22.number().min(0).max(3e4).optional().describe("Fixed pause after the action, in ms (max 30000)."),
|
|
4941
|
+
waitFor: z22.string().optional().describe("Wait until an element matching this appears (default 5s)."),
|
|
4942
|
+
waitGone: z22.string().optional().describe("Wait until no element matches this (default 5s)."),
|
|
4943
|
+
expect: z22.string().optional().describe("Assert an element matching this IS on screen (waits up to 2s for it); fails the flow if not."),
|
|
4944
|
+
expectGone: z22.string().optional().describe("Assert nothing matches this (waits up to 2s for it to go); fails the flow if something does."),
|
|
4945
|
+
timeoutMs: z22.number().min(0).max(6e4).optional().describe("Budget for this step's waitFor/waitGone/expect, max 60000. Default 5000 (2000 for expect).")
|
|
4572
4946
|
}).describe("One flow step. Order within a step: reload \u2192 tap/type/set \u2192 waitMs \u2192 waitFor/waitGone \u2192 expect.");
|
|
4573
4947
|
function registerFlowTools(server, sync) {
|
|
4574
4948
|
server.registerTool(
|
|
@@ -4576,10 +4950,10 @@ function registerFlowTools(server, sync) {
|
|
|
4576
4950
|
{
|
|
4577
4951
|
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.",
|
|
4578
4952
|
inputSchema: {
|
|
4579
|
-
steps:
|
|
4580
|
-
include:
|
|
4581
|
-
continueOnFailure:
|
|
4582
|
-
deviceId:
|
|
4953
|
+
steps: z22.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
4954
|
+
include: z22.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."),
|
|
4955
|
+
continueOnFailure: z22.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
4956
|
+
deviceId: z22.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4583
4957
|
}
|
|
4584
4958
|
},
|
|
4585
4959
|
async (args) => {
|
|
@@ -4803,9 +5177,9 @@ ${shown.map(formatElement2).join("\n")}`
|
|
|
4803
5177
|
}
|
|
4804
5178
|
}
|
|
4805
5179
|
}
|
|
4806
|
-
return
|
|
5180
|
+
return text20(parts.join("\n\n"));
|
|
4807
5181
|
} catch (e) {
|
|
4808
|
-
return
|
|
5182
|
+
return fail22(errMsg21(e));
|
|
4809
5183
|
}
|
|
4810
5184
|
}
|
|
4811
5185
|
);
|
|
@@ -4826,10 +5200,10 @@ var cap2 = (s, n = 160) => s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
|
4826
5200
|
var firstLines = (s, n = 3) => s.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, n).join(" \u23CE ");
|
|
4827
5201
|
|
|
4828
5202
|
// src/tools/packs/renders.ts
|
|
4829
|
-
import { z as
|
|
5203
|
+
import { z as z23 } from "zod";
|
|
4830
5204
|
var UI_TOOL_ID2 = "highlight-updates";
|
|
4831
|
-
var
|
|
4832
|
-
var
|
|
5205
|
+
var text21 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
5206
|
+
var fail23 = (message) => ({
|
|
4833
5207
|
isError: true,
|
|
4834
5208
|
content: [{ type: "text", text: message }]
|
|
4835
5209
|
});
|
|
@@ -4839,12 +5213,12 @@ var DEFAULT_IDLE_MS = 3e3;
|
|
|
4839
5213
|
var SETTLE_MS = 400;
|
|
4840
5214
|
var DEFAULT_TOP = 12;
|
|
4841
5215
|
var lastCapture = /* @__PURE__ */ new Map();
|
|
4842
|
-
var stepSchema2 =
|
|
4843
|
-
tap:
|
|
4844
|
-
type:
|
|
4845
|
-
into:
|
|
4846
|
-
waitMs:
|
|
4847
|
-
repeat:
|
|
5216
|
+
var stepSchema2 = z23.object({
|
|
5217
|
+
tap: z23.string().optional().describe("Tap this element: exact testID, or fuzzy visible text."),
|
|
5218
|
+
type: z23.string().optional().describe("Text to type (needs `into`)."),
|
|
5219
|
+
into: z23.string().optional().describe("Target text input for `type`: testID or fuzzy text."),
|
|
5220
|
+
waitMs: z23.number().min(0).max(1e4).optional().describe("Pause after the action, in ms."),
|
|
5221
|
+
repeat: z23.number().min(1).max(20).optional().describe("Do this step N times (default 1). Useful for 'tap it five times'.")
|
|
4848
5222
|
}).describe("One interaction to measure. Keep it to the thing you're asking about.");
|
|
4849
5223
|
function selector2(needle) {
|
|
4850
5224
|
return /\s/.test(needle) ? { query: needle } : { testID: needle, query: needle };
|
|
@@ -4937,14 +5311,14 @@ function registerRenderTools(server, sync) {
|
|
|
4937
5311
|
{
|
|
4938
5312
|
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.",
|
|
4939
5313
|
inputSchema: {
|
|
4940
|
-
steps:
|
|
5314
|
+
steps: z23.array(stepSchema2).max(20).optional().describe(
|
|
4941
5315
|
"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."
|
|
4942
5316
|
),
|
|
4943
|
-
idleMs:
|
|
4944
|
-
label:
|
|
4945
|
-
compareToPrevious:
|
|
4946
|
-
top:
|
|
4947
|
-
deviceId:
|
|
5317
|
+
idleMs: z23.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5318
|
+
label: z23.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5319
|
+
compareToPrevious: z23.boolean().optional().describe("Diff against the previous measure_renders on this device. Use after an edit to prove the fix."),
|
|
5320
|
+
top: z23.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5321
|
+
deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4948
5322
|
}
|
|
4949
5323
|
},
|
|
4950
5324
|
async (args) => {
|
|
@@ -4953,18 +5327,18 @@ function registerRenderTools(server, sync) {
|
|
|
4953
5327
|
const device = sync.connectedDevices().find((d) => d.deviceId === target);
|
|
4954
5328
|
const uiTool = (device?.tools ?? []).find((t) => t.toolId === UI_TOOL_ID2);
|
|
4955
5329
|
if (!uiTool) {
|
|
4956
|
-
return
|
|
5330
|
+
return fail23(
|
|
4957
5331
|
"This app doesn't expose the highlight-updates tool, so renders can't be measured. Install @buoy-gg/highlight-updates."
|
|
4958
5332
|
);
|
|
4959
5333
|
}
|
|
4960
5334
|
if (!(uiTool.actions ?? []).includes("beginMeasurement")) {
|
|
4961
|
-
return
|
|
5335
|
+
return fail23(
|
|
4962
5336
|
"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."
|
|
4963
5337
|
);
|
|
4964
5338
|
}
|
|
4965
5339
|
const began = await sync.callAction(UI_TOOL_ID2, "beginMeasurement", {}, target);
|
|
4966
5340
|
if (began && began.ok === false) {
|
|
4967
|
-
return
|
|
5341
|
+
return fail23(`Can't measure renders: ${began.reason ?? "capture unavailable"}`);
|
|
4968
5342
|
}
|
|
4969
5343
|
await delay5(SETTLE_MS);
|
|
4970
5344
|
const startedAt = Date.now();
|
|
@@ -5009,7 +5383,7 @@ function registerRenderTools(server, sync) {
|
|
|
5009
5383
|
const ended = await sync.callAction(UI_TOOL_ID2, "endMeasurement", {}, target);
|
|
5010
5384
|
const summary = ended?.summary;
|
|
5011
5385
|
if (!summary) {
|
|
5012
|
-
return
|
|
5386
|
+
return fail23(
|
|
5013
5387
|
"The capture window closed without returning anything. If the app reloaded mid-measurement, try again."
|
|
5014
5388
|
);
|
|
5015
5389
|
}
|
|
@@ -5034,9 +5408,9 @@ function registerRenderTools(server, sync) {
|
|
|
5034
5408
|
} else if (!args.compareToPrevious) {
|
|
5035
5409
|
parts.push("", "_Fixed something? Call this again with the same steps and `compareToPrevious:true` to prove it landed._");
|
|
5036
5410
|
}
|
|
5037
|
-
return
|
|
5411
|
+
return text21(parts.join("\n"));
|
|
5038
5412
|
} catch (e) {
|
|
5039
|
-
return
|
|
5413
|
+
return fail23(errMsg22(e));
|
|
5040
5414
|
}
|
|
5041
5415
|
}
|
|
5042
5416
|
);
|
|
@@ -5044,7 +5418,7 @@ function registerRenderTools(server, sync) {
|
|
|
5044
5418
|
|
|
5045
5419
|
// src/verify/editWatcher.ts
|
|
5046
5420
|
import fs3 from "fs";
|
|
5047
|
-
import
|
|
5421
|
+
import path4 from "path";
|
|
5048
5422
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5049
5423
|
".ts",
|
|
5050
5424
|
".tsx",
|
|
@@ -5093,8 +5467,8 @@ var EditWatcher = class {
|
|
|
5093
5467
|
(_event, filename) => {
|
|
5094
5468
|
if (!filename) return;
|
|
5095
5469
|
const rel2 = String(filename);
|
|
5096
|
-
if (rel2.split(
|
|
5097
|
-
if (!WATCHED_EXT.has(
|
|
5470
|
+
if (rel2.split(path4.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
5471
|
+
if (!WATCHED_EXT.has(path4.extname(rel2))) return;
|
|
5098
5472
|
this.noteEdit(rel2);
|
|
5099
5473
|
}
|
|
5100
5474
|
);
|
|
@@ -5194,6 +5568,19 @@ requests the DEVELOPER kept: those are snapshots that outlive Clear, the 500-req
|
|
|
5194
5568
|
restarts, so treat a pinned request as "a human flagged this as the problem". \`network_action\`
|
|
5195
5569
|
pins/saves one \u2014 pin the failing call you want a human to see, or keep one before the log rolls over.
|
|
5196
5570
|
|
|
5571
|
+
Testing anything that opens a CAMERA \u2014 a QR/barcode scanner, document capture, an avatar
|
|
5572
|
+
picker? The iOS Simulator has no camera, so those screens normally cannot be exercised at
|
|
5573
|
+
all. \`camera_source\` gives it one: \`barcode\` generates the code from text you supply
|
|
5574
|
+
(pdf417 is what a driver's licence carries), \`image\`/\`video\` play a file, \`screen\` points
|
|
5575
|
+
the phone at this Mac. \`camera_devices\` lists the booted simulators and their apps when
|
|
5576
|
+
you need a bundle id; \`camera_inputs\` lists this Mac's cameras and windows when you need
|
|
5577
|
+
one of those ids. Then \`camera_launch\` to relaunch the app with it attached \u2014 a camera
|
|
5578
|
+
can only be attached at process start \u2014 or \`camera_zero_setup\` when something else starts
|
|
5579
|
+
the app. \`camera_status\` shows what is being decoded right now, which separates "the
|
|
5580
|
+
scanner is broken" from "it never saw the code", \`camera_diagnose\` says why when nothing
|
|
5581
|
+
works, and \`camera_stop\` turns it off when you are done. These need no device connection
|
|
5582
|
+
and no Buoy integration in the app.
|
|
5583
|
+
|
|
5197
5584
|
Need to SEE a component (verify a UI change, check styling/layout)? Use \`screenshot_component\` with
|
|
5198
5585
|
a testID/name \u2014 it locates the live component on-device and returns a cropped screenshot inline.
|
|
5199
5586
|
iOS Simulator only, and only works when this MCP server runs on the same Mac as the booted Simulator.
|
|
@@ -5310,6 +5697,7 @@ function createMcpServer(sync) {
|
|
|
5310
5697
|
registerTimeMachineTools(server, sync);
|
|
5311
5698
|
registerScenariosTools(server, sync);
|
|
5312
5699
|
registerScreenshotTools(server, sync);
|
|
5700
|
+
registerCameraTools(server);
|
|
5313
5701
|
registerInteractTools(server, sync);
|
|
5314
5702
|
registerJsThreadTools(server, sync);
|
|
5315
5703
|
registerImagesTools(server, sync);
|
|
@@ -5318,7 +5706,7 @@ function createMcpServer(sync) {
|
|
|
5318
5706
|
return server;
|
|
5319
5707
|
}
|
|
5320
5708
|
var deviceIdField = {
|
|
5321
|
-
deviceId:
|
|
5709
|
+
deviceId: z24.string().optional().describe(
|
|
5322
5710
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
5323
5711
|
)
|
|
5324
5712
|
};
|
|
@@ -5326,8 +5714,8 @@ var deviceIdField = {
|
|
|
5326
5714
|
// src/init.ts
|
|
5327
5715
|
import { spawnSync } from "child_process";
|
|
5328
5716
|
import fs4 from "fs";
|
|
5329
|
-
import
|
|
5330
|
-
import
|
|
5717
|
+
import os4 from "os";
|
|
5718
|
+
import path5 from "path";
|
|
5331
5719
|
function defaultEnv() {
|
|
5332
5720
|
return { BUOY_VERIFY: "auto" };
|
|
5333
5721
|
}
|
|
@@ -5335,19 +5723,19 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5335
5723
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5336
5724
|
function runInit(opts = {}) {
|
|
5337
5725
|
const cwd = opts.cwd ?? process.cwd();
|
|
5338
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
5726
|
+
const skillSrcDir = opts.skillSrcDir ?? path5.resolve(__dirname, "../skill");
|
|
5339
5727
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5340
5728
|
const entry = resolveEntry(opts, log);
|
|
5341
5729
|
const done = [];
|
|
5342
|
-
upsertMcpConfig(
|
|
5343
|
-
fs4.mkdirSync(
|
|
5344
|
-
upsertMcpConfig(
|
|
5345
|
-
if (fs4.existsSync(
|
|
5346
|
-
upsertMcpConfig(
|
|
5730
|
+
upsertMcpConfig(path5.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
5731
|
+
fs4.mkdirSync(path5.join(cwd, ".cursor"), { recursive: true });
|
|
5732
|
+
upsertMcpConfig(path5.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
5733
|
+
if (fs4.existsSync(path5.join(cwd, ".vscode"))) {
|
|
5734
|
+
upsertMcpConfig(path5.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5347
5735
|
}
|
|
5348
5736
|
installSkills(skillSrcDir, cwd, done);
|
|
5349
|
-
appendDebuggingGuide(
|
|
5350
|
-
appendDebuggingGuide(
|
|
5737
|
+
appendDebuggingGuide(path5.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5738
|
+
appendDebuggingGuide(path5.join(cwd, "AGENTS.md"), cwd, done);
|
|
5351
5739
|
log("Buoy MCP installed:");
|
|
5352
5740
|
for (const line of done) log(" \u2022 " + line);
|
|
5353
5741
|
log("");
|
|
@@ -5389,11 +5777,11 @@ function npxEntry() {
|
|
|
5389
5777
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5390
5778
|
}
|
|
5391
5779
|
function localEntry(opts, log) {
|
|
5392
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
5780
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? path5.join(os4.homedir(), ".buoy-mcp");
|
|
5393
5781
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
5394
|
-
const
|
|
5395
|
-
if (
|
|
5396
|
-
return { type: "stdio", command: "node", args: [
|
|
5782
|
+
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
5783
|
+
if (cliPath2) {
|
|
5784
|
+
return { type: "stdio", command: "node", args: [cliPath2], env: defaultEnv() };
|
|
5397
5785
|
}
|
|
5398
5786
|
log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
|
|
5399
5787
|
return npxEntry();
|
|
@@ -5423,12 +5811,12 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
5423
5811
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
5424
5812
|
return null;
|
|
5425
5813
|
}
|
|
5426
|
-
const
|
|
5427
|
-
if (!fs4.existsSync(
|
|
5428
|
-
log(`\u26A0 install finished but ${
|
|
5814
|
+
const cliPath2 = path5.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
5815
|
+
if (!fs4.existsSync(cliPath2)) {
|
|
5816
|
+
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
5429
5817
|
return null;
|
|
5430
5818
|
}
|
|
5431
|
-
return
|
|
5819
|
+
return cliPath2;
|
|
5432
5820
|
}
|
|
5433
5821
|
function probeRegistry() {
|
|
5434
5822
|
const registry = effectiveRegistry();
|
|
@@ -5485,7 +5873,7 @@ function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
|
5485
5873
|
json3 = {};
|
|
5486
5874
|
}
|
|
5487
5875
|
} else {
|
|
5488
|
-
fs4.mkdirSync(
|
|
5876
|
+
fs4.mkdirSync(path5.dirname(file), { recursive: true });
|
|
5489
5877
|
}
|
|
5490
5878
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
5491
5879
|
const existed = "buoy" in servers;
|
|
@@ -5576,29 +5964,29 @@ function installSkills(skillSrcDir, cwd, done) {
|
|
|
5576
5964
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
5577
5965
|
return;
|
|
5578
5966
|
}
|
|
5579
|
-
const skills = fs4.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs4.existsSync(
|
|
5967
|
+
const skills = fs4.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && fs4.existsSync(path5.join(skillSrcDir, e.name, "SKILL.md")));
|
|
5580
5968
|
if (skills.length === 0) {
|
|
5581
5969
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
5582
5970
|
return;
|
|
5583
5971
|
}
|
|
5584
5972
|
for (const skill of skills) {
|
|
5585
|
-
const from =
|
|
5586
|
-
const to =
|
|
5973
|
+
const from = path5.join(skillSrcDir, skill.name);
|
|
5974
|
+
const to = path5.join(cwd, ".claude", "skills", skill.name);
|
|
5587
5975
|
copyDir(from, to);
|
|
5588
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
5976
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(path5.join(to, "SKILL.md"), cwd)}`);
|
|
5589
5977
|
}
|
|
5590
5978
|
}
|
|
5591
5979
|
function copyDir(from, to) {
|
|
5592
5980
|
fs4.mkdirSync(to, { recursive: true });
|
|
5593
5981
|
for (const entry of fs4.readdirSync(from, { withFileTypes: true })) {
|
|
5594
|
-
const s =
|
|
5595
|
-
const d =
|
|
5982
|
+
const s = path5.join(from, entry.name);
|
|
5983
|
+
const d = path5.join(to, entry.name);
|
|
5596
5984
|
if (entry.isDirectory()) copyDir(s, d);
|
|
5597
5985
|
else fs4.copyFileSync(s, d);
|
|
5598
5986
|
}
|
|
5599
5987
|
}
|
|
5600
5988
|
function rel(file, cwd) {
|
|
5601
|
-
const r =
|
|
5989
|
+
const r = path5.relative(cwd, file);
|
|
5602
5990
|
return r && !r.startsWith("..") ? r : file;
|
|
5603
5991
|
}
|
|
5604
5992
|
|