@buoy-gg/mcp 7.0.26 → 7.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NPMUQLCZ.mjs → chunk-SFG6ZYH6.mjs} +193 -52
- package/dist/cli.js +191 -52
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +191 -52
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,33 @@ var DEVICE_RETENTION_MS = 15 * 6e4;
|
|
|
46
46
|
var DASHBOARD_NAME = "Dashboard";
|
|
47
47
|
var ALL_DEVICES = "All";
|
|
48
48
|
|
|
49
|
+
// src/sync/actionTimeout.ts
|
|
50
|
+
function isRecoveryAction(toolId, action) {
|
|
51
|
+
return toolId === "app" && (action === "ping" || action === "reloadApp");
|
|
52
|
+
}
|
|
53
|
+
function isUnresponsive(consecutiveTimeouts) {
|
|
54
|
+
return consecutiveTimeouts >= 2;
|
|
55
|
+
}
|
|
56
|
+
function actionTimeoutMs(input) {
|
|
57
|
+
if (isRecoveryAction(input.toolId, input.action)) {
|
|
58
|
+
return { timeoutMs: input.fullMs, usedFullBudget: false };
|
|
59
|
+
}
|
|
60
|
+
if (!isUnresponsive(input.consecutiveTimeouts)) {
|
|
61
|
+
return { timeoutMs: input.fullMs, usedFullBudget: false };
|
|
62
|
+
}
|
|
63
|
+
if (input.now - input.lastFullBudgetAt > input.recoveryProbeMs) {
|
|
64
|
+
return { timeoutMs: input.fullMs, usedFullBudget: true };
|
|
65
|
+
}
|
|
66
|
+
return { timeoutMs: input.shortMs, usedFullBudget: false };
|
|
67
|
+
}
|
|
68
|
+
function diagnoseCrashed(opts) {
|
|
69
|
+
if (opts.consecutiveTimeouts >= 2) return true;
|
|
70
|
+
if (opts.lastOkAt > 0 && opts.now - opts.lastOkAt > opts.unresponsiveAfterMs) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
// src/sync/SyncClient.ts
|
|
50
77
|
var SyncClient = class {
|
|
51
78
|
constructor(opts = {}) {
|
|
@@ -314,27 +341,34 @@ var SyncClient = class {
|
|
|
314
341
|
noteTimeout(deviceId) {
|
|
315
342
|
this.healthOf(deviceId).consecutiveTimeouts += 1;
|
|
316
343
|
}
|
|
317
|
-
/** True once a device has missed
|
|
344
|
+
/** True once a device has missed TWO calls and hasn't answered since. */
|
|
318
345
|
isUnresponsive(deviceId) {
|
|
319
|
-
return this.healthOf(deviceId).consecutiveTimeouts
|
|
346
|
+
return isUnresponsive(this.healthOf(deviceId).consecutiveTimeouts);
|
|
320
347
|
}
|
|
321
348
|
/**
|
|
322
349
|
* Full timeout normally; a short one for a device already not answering.
|
|
323
350
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
351
|
+
* A single missed call does not trigger the short budget — one slow
|
|
352
|
+
* `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
|
|
353
|
+
* the full budget (they are the recovery path).
|
|
354
|
+
*
|
|
355
|
+
* Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
|
|
356
|
+
* budget again so the short timeout cannot become self-sustaining.
|
|
329
357
|
*/
|
|
330
|
-
timeoutFor(deviceId) {
|
|
331
|
-
if (!this.isUnresponsive(deviceId)) return this.actionTimeoutMs;
|
|
358
|
+
timeoutFor(deviceId, toolId, action) {
|
|
332
359
|
const h = this.healthOf(deviceId);
|
|
333
|
-
|
|
334
|
-
h.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
360
|
+
const { timeoutMs, usedFullBudget } = actionTimeoutMs({
|
|
361
|
+
consecutiveTimeouts: h.consecutiveTimeouts,
|
|
362
|
+
lastFullBudgetAt: h.lastFullBudgetAt,
|
|
363
|
+
now: Date.now(),
|
|
364
|
+
fullMs: this.actionTimeoutMs,
|
|
365
|
+
shortMs: UNRESPONSIVE_ACTION_TIMEOUT_MS,
|
|
366
|
+
recoveryProbeMs: RECOVERY_PROBE_MS,
|
|
367
|
+
toolId,
|
|
368
|
+
action
|
|
369
|
+
});
|
|
370
|
+
if (usedFullBudget) h.lastFullBudgetAt = Date.now();
|
|
371
|
+
return timeoutMs;
|
|
338
372
|
}
|
|
339
373
|
/**
|
|
340
374
|
* Plain-language diagnosis for a device that isn't answering, including the
|
|
@@ -347,7 +381,12 @@ var SyncClient = class {
|
|
|
347
381
|
unresponsiveNote(deviceId) {
|
|
348
382
|
const h = this.healthOf(deviceId);
|
|
349
383
|
const since = h.lastOkAt ? Math.round((Date.now() - h.lastOkAt) / 1e3) : null;
|
|
350
|
-
const crashed =
|
|
384
|
+
const crashed = diagnoseCrashed({
|
|
385
|
+
consecutiveTimeouts: h.consecutiveTimeouts,
|
|
386
|
+
lastOkAt: h.lastOkAt,
|
|
387
|
+
now: Date.now(),
|
|
388
|
+
unresponsiveAfterMs: UNRESPONSIVE_AFTER_MS
|
|
389
|
+
});
|
|
351
390
|
const lines = [
|
|
352
391
|
`The app on "${deviceId}" is not responding (${h.consecutiveTimeouts} call${h.consecutiveTimeouts === 1 ? "" : "s"} unanswered${since != null ? `, last answered ${since}s ago` : ""}).`,
|
|
353
392
|
crashed ? "It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded." : "It may be busy, mid-reload, or crashing."
|
|
@@ -549,7 +588,7 @@ var SyncClient = class {
|
|
|
549
588
|
const target = deviceId === ALL_DEVICES ? ALL_DEVICES : this.resolveDevice(deviceId);
|
|
550
589
|
if (!this.socket) throw new Error("SyncClient is not connected");
|
|
551
590
|
const requestId = (0, import_node_crypto.randomUUID)();
|
|
552
|
-
const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target);
|
|
591
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target, toolId, action);
|
|
553
592
|
const result = await new Promise((resolve, reject) => {
|
|
554
593
|
const timer = setTimeout(() => {
|
|
555
594
|
this.pendingActions.delete(requestId);
|
|
@@ -612,7 +651,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
612
651
|
var import_node_path = __toESM(require("path"));
|
|
613
652
|
|
|
614
653
|
// src/version.ts
|
|
615
|
-
var VERSION = "7.0.
|
|
654
|
+
var VERSION = "7.0.28" ? "7.0.28" : "0.0.0-dev";
|
|
616
655
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
617
656
|
|
|
618
657
|
// src/update/checkForUpdate.ts
|
|
@@ -1638,7 +1677,14 @@ function statusIcon(e) {
|
|
|
1638
1677
|
return "\u2705";
|
|
1639
1678
|
}
|
|
1640
1679
|
function statusLabel(e) {
|
|
1641
|
-
if (e.
|
|
1680
|
+
if (e.status !== void 0 && e.status > 0) return String(e.status);
|
|
1681
|
+
if (e.error) {
|
|
1682
|
+
const cleaned = e.error.replace(/^HTTP\s+\d+:\s*/i, "").trim();
|
|
1683
|
+
if (!cleaned || cleaned === "undefined" || cleaned === "no error") {
|
|
1684
|
+
return "ERR";
|
|
1685
|
+
}
|
|
1686
|
+
return `ERR ${e.error}`;
|
|
1687
|
+
}
|
|
1642
1688
|
if (e.status === void 0) return "pending";
|
|
1643
1689
|
return String(e.status);
|
|
1644
1690
|
}
|
|
@@ -2098,10 +2144,20 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2098
2144
|
|
|
2099
2145
|
// src/tools/packs/reactQuery.ts
|
|
2100
2146
|
var import_zod5 = require("zod");
|
|
2101
|
-
var RQ_TOOL_ID = "
|
|
2147
|
+
var RQ_TOOL_ID = "query";
|
|
2148
|
+
var RQ_TOOL_ID_LEGACY = "react-query";
|
|
2102
2149
|
var text4 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2103
2150
|
var fail5 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2104
2151
|
var errMsg5 = (e) => e instanceof Error ? e.message : String(e);
|
|
2152
|
+
async function callQueryAction(sync, action, params, target) {
|
|
2153
|
+
try {
|
|
2154
|
+
return await sync.callAction(RQ_TOOL_ID, action, params, target);
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
const msg = errMsg5(e);
|
|
2157
|
+
if (!/unknown tool/i.test(msg)) throw e;
|
|
2158
|
+
return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2105
2161
|
var STATUS_ICON = {
|
|
2106
2162
|
fresh: "\u{1F7E2}",
|
|
2107
2163
|
stale: "\u{1F7E1}",
|
|
@@ -2134,8 +2190,8 @@ function registerReactQueryTools(server, sync) {
|
|
|
2134
2190
|
async (args) => {
|
|
2135
2191
|
try {
|
|
2136
2192
|
const target = sync.requireProDevice(args.deviceId);
|
|
2137
|
-
const res = await
|
|
2138
|
-
|
|
2193
|
+
const res = await callQueryAction(
|
|
2194
|
+
sync,
|
|
2139
2195
|
"listQueries",
|
|
2140
2196
|
{ includeData: args.includeData ?? false, staleOnly: args.staleOnly ?? false, limit: args.limit ?? 25 },
|
|
2141
2197
|
target
|
|
@@ -2181,13 +2237,13 @@ ${lines.join("\n")}${note}`);
|
|
|
2181
2237
|
try {
|
|
2182
2238
|
const target = sync.requireProDevice(args.deviceId);
|
|
2183
2239
|
if (args.action === "clear") {
|
|
2184
|
-
await sync
|
|
2240
|
+
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
2185
2241
|
return text4("Cleared the entire React Query cache.");
|
|
2186
2242
|
}
|
|
2187
2243
|
if (!args.queryHash) {
|
|
2188
2244
|
return fail5(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
2189
2245
|
}
|
|
2190
|
-
await sync
|
|
2246
|
+
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
2191
2247
|
return text4(`Ran ${args.action} on query \`${args.queryHash}\`.`);
|
|
2192
2248
|
} catch (e) {
|
|
2193
2249
|
return fail5(errMsg5(e));
|
|
@@ -2614,17 +2670,23 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
|
|
|
2614
2670
|
server.registerTool(
|
|
2615
2671
|
"navigate",
|
|
2616
2672
|
{
|
|
2617
|
-
description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths.",
|
|
2673
|
+
description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",
|
|
2618
2674
|
inputSchema: {
|
|
2619
2675
|
path: import_zod10.z.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
|
|
2676
|
+
replace: import_zod10.z.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
|
|
2620
2677
|
deviceId: import_zod10.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2621
2678
|
}
|
|
2622
2679
|
},
|
|
2623
2680
|
async (args) => {
|
|
2624
2681
|
try {
|
|
2625
2682
|
const target = sync.requireProDevice(args.deviceId);
|
|
2626
|
-
await sync.callAction(
|
|
2627
|
-
|
|
2683
|
+
await sync.callAction(
|
|
2684
|
+
ROUTES_TOOL_ID,
|
|
2685
|
+
"navigate",
|
|
2686
|
+
{ path: args.path, replace: args.replace === true },
|
|
2687
|
+
target
|
|
2688
|
+
);
|
|
2689
|
+
return text9(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
|
|
2628
2690
|
} catch (e) {
|
|
2629
2691
|
return fail10(errMsg10(e));
|
|
2630
2692
|
}
|
|
@@ -2924,6 +2986,40 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
2924
2986
|
}
|
|
2925
2987
|
}
|
|
2926
2988
|
|
|
2989
|
+
// src/tools/packs/screenshotLocate.ts
|
|
2990
|
+
function pickDescribeScreenMatch(elements, query, nativeTag) {
|
|
2991
|
+
if (nativeTag != null) {
|
|
2992
|
+
const exact = elements.find((e) => e.nativeTag === nativeTag);
|
|
2993
|
+
if (exact) return exact;
|
|
2994
|
+
}
|
|
2995
|
+
const q = (query ?? "").trim().toLowerCase();
|
|
2996
|
+
if (!q) return void 0;
|
|
2997
|
+
let best;
|
|
2998
|
+
for (const el of elements) {
|
|
2999
|
+
const fields = [el.testID, el.label, el.text, el.name].filter(Boolean).map((s) => s.toLowerCase());
|
|
3000
|
+
let score = 0;
|
|
3001
|
+
for (const f of fields) {
|
|
3002
|
+
if (f === q) score = Math.max(score, 100);
|
|
3003
|
+
else if (f.startsWith(q)) score = Math.max(score, 50);
|
|
3004
|
+
else if (f.includes(q)) score = Math.max(score, 25);
|
|
3005
|
+
}
|
|
3006
|
+
const tap = el.tap;
|
|
3007
|
+
if (tap && tap.x >= 0 && tap.x <= 1 && tap.y >= 0 && tap.y <= 1) score += 20;
|
|
3008
|
+
else if (tap) score -= 40;
|
|
3009
|
+
if (score <= 0) continue;
|
|
3010
|
+
if (!best || score > best.score) best = { el, score };
|
|
3011
|
+
}
|
|
3012
|
+
return best?.el;
|
|
3013
|
+
}
|
|
3014
|
+
function frameToRect(frame, screen) {
|
|
3015
|
+
return {
|
|
3016
|
+
x: frame.x * screen.width,
|
|
3017
|
+
y: frame.y * screen.height,
|
|
3018
|
+
width: frame.width * screen.width,
|
|
3019
|
+
height: frame.height * screen.height
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
|
|
2927
3023
|
// src/tools/packs/screenshot.ts
|
|
2928
3024
|
var TOOL_ID = "highlight-updates";
|
|
2929
3025
|
var MARGIN_PT = 12;
|
|
@@ -2933,7 +3029,7 @@ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
|
2933
3029
|
function reasonMessage(res, query) {
|
|
2934
3030
|
switch (res.reason) {
|
|
2935
3031
|
case "no-renders":
|
|
2936
|
-
return "No components
|
|
3032
|
+
return "No components matched via highlight tracking or the live fiber walk. Pass a testID / name from describe_screen, then retry.";
|
|
2937
3033
|
case "no-match":
|
|
2938
3034
|
return query ? `Nothing matched "${query}".` : "No query or nativeTag given \u2014 pass one, or a testID/name to search for.";
|
|
2939
3035
|
case "no-measurement":
|
|
@@ -2957,7 +3053,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2957
3053
|
server.registerTool(
|
|
2958
3054
|
"screenshot_component",
|
|
2959
3055
|
{
|
|
2960
|
-
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.
|
|
3056
|
+
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.",
|
|
2961
3057
|
inputSchema: {
|
|
2962
3058
|
query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2963
3059
|
nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
@@ -2981,15 +3077,37 @@ function registerScreenshotTools(server, sync) {
|
|
|
2981
3077
|
},
|
|
2982
3078
|
target
|
|
2983
3079
|
);
|
|
2984
|
-
|
|
2985
|
-
|
|
3080
|
+
let located = res;
|
|
3081
|
+
if (!located.matched && (located.reason === "no-renders" || located.reason === "no-match")) {
|
|
3082
|
+
const screen = await sync.callAction(TOOL_ID, "describeScreen", {}, target).catch(() => null);
|
|
3083
|
+
const match = screen?.elements ? pickDescribeScreenMatch(screen.elements, args.query, args.nativeTag) : void 0;
|
|
3084
|
+
if (match?.frame && screen?.screen) {
|
|
3085
|
+
located = {
|
|
3086
|
+
matched: true,
|
|
3087
|
+
live: true,
|
|
3088
|
+
rect: frameToRect(match.frame, screen.screen),
|
|
3089
|
+
screen: screen.screen,
|
|
3090
|
+
nativeTag: match.nativeTag ?? void 0,
|
|
3091
|
+
componentName: match.name,
|
|
3092
|
+
testID: match.testID
|
|
3093
|
+
};
|
|
3094
|
+
} else if (located.reason === "no-renders") {
|
|
3095
|
+
located = {
|
|
3096
|
+
matched: false,
|
|
3097
|
+
reason: "no-match",
|
|
3098
|
+
candidates: located.candidates
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (!located.matched || !located.rect || !located.screen) {
|
|
3103
|
+
return fail13(reasonMessage(located, args.query) + candidatesText(located.candidates));
|
|
2986
3104
|
}
|
|
2987
3105
|
const shot = await captureIosFramebuffer();
|
|
2988
3106
|
if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
|
|
2989
3107
|
return fail13(shot.error ?? "Screenshot capture failed.");
|
|
2990
3108
|
}
|
|
2991
|
-
const pxScale = shot.width /
|
|
2992
|
-
const r =
|
|
3109
|
+
const pxScale = shot.width / located.screen.width;
|
|
3110
|
+
const r = located.rect;
|
|
2993
3111
|
const pad = CAPTURE_PADDING_PT;
|
|
2994
3112
|
const crop = await cropToBuffer(
|
|
2995
3113
|
shot.srcPath,
|
|
@@ -3004,9 +3122,9 @@ function registerScreenshotTools(server, sync) {
|
|
|
3004
3122
|
if (!crop.ok || !crop.buffer) {
|
|
3005
3123
|
return fail13(crop.error ?? "Crop failed.");
|
|
3006
3124
|
}
|
|
3007
|
-
const label =
|
|
3008
|
-
const idPart =
|
|
3009
|
-
const warn =
|
|
3125
|
+
const label = located.componentName || "component";
|
|
3126
|
+
const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
|
|
3127
|
+
const warn = located.inView === false ? "\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge." : "";
|
|
3010
3128
|
const summary = `Captured "${label}"${idPart} \u2014 ${crop.width}\xD7${crop.height}px.${warn}`;
|
|
3011
3129
|
return {
|
|
3012
3130
|
content: [
|
|
@@ -3570,6 +3688,19 @@ ${JSON.stringify(slim)}
|
|
|
3570
3688
|
|
|
3571
3689
|
// src/tools/packs/assets.ts
|
|
3572
3690
|
var import_zod17 = require("zod");
|
|
3691
|
+
|
|
3692
|
+
// src/tools/packs/assetsGraphNote.ts
|
|
3693
|
+
function graphUnavailableNote(ss) {
|
|
3694
|
+
if (ss.graphSupported && ss.graphCount != null) return null;
|
|
3695
|
+
const err = (ss.graphError ?? "").trim();
|
|
3696
|
+
const looksLikeDev = ss.dev === true || !!err;
|
|
3697
|
+
if (looksLikeDev) {
|
|
3698
|
+
return err ? `\u26A0 Metro bundle graph unavailable (${err}) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.` : "\u26A0 Metro bundle graph unavailable \u2014 showing runtime-registered assets only; never-loaded detection unavailable.";
|
|
3699
|
+
}
|
|
3700
|
+
return "\u26A0 No Metro bundle graph (release build, or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.";
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// src/tools/packs/assets.ts
|
|
3573
3704
|
var ASSETS_TOOL_ID = "assets";
|
|
3574
3705
|
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3575
3706
|
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
@@ -3612,11 +3743,8 @@ function formatListMarkdown2(list, limit) {
|
|
|
3612
3743
|
if (ss.fontFamilies.length > 0) {
|
|
3613
3744
|
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
3614
3745
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
"\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
|
|
3618
|
-
);
|
|
3619
|
-
}
|
|
3746
|
+
const graphNote = graphUnavailableNote(ss);
|
|
3747
|
+
if (graphNote) lines.push(graphNote);
|
|
3620
3748
|
if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
|
|
3621
3749
|
const ins = list.insights;
|
|
3622
3750
|
const chips = [];
|
|
@@ -3905,16 +4033,14 @@ function registerReloadTools(server, sync) {
|
|
|
3905
4033
|
const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
|
|
3906
4034
|
let ack = null;
|
|
3907
4035
|
let recovered = false;
|
|
3908
|
-
|
|
3909
|
-
ack = await sync.callAction(
|
|
3910
|
-
APP_TOOL_ID,
|
|
3911
|
-
RELOAD_ACTION,
|
|
3912
|
-
{ strategy: args.strategy ?? "auto" },
|
|
3913
|
-
target
|
|
3914
|
-
);
|
|
3915
|
-
} catch (inAppError) {
|
|
4036
|
+
const tryDevServer = async (inAppError) => {
|
|
3916
4037
|
const origin = sync.devServerOrigin(target);
|
|
3917
|
-
if (!origin)
|
|
4038
|
+
if (!origin) {
|
|
4039
|
+
if (inAppError) throw inAppError;
|
|
4040
|
+
throw new Error(
|
|
4041
|
+
"The app is not answering and no dev-server origin is known (it was never learned from a successful ping). Reload the app by hand."
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
3918
4044
|
const res = await reloadViaDevServer(origin);
|
|
3919
4045
|
if (!res.ok) {
|
|
3920
4046
|
throw new Error(
|
|
@@ -3922,6 +4048,20 @@ function registerReloadTools(server, sync) {
|
|
|
3922
4048
|
);
|
|
3923
4049
|
}
|
|
3924
4050
|
recovered = true;
|
|
4051
|
+
};
|
|
4052
|
+
if (sync.isUnresponsive(target) && sync.devServerOrigin(target)) {
|
|
4053
|
+
await tryDevServer();
|
|
4054
|
+
} else {
|
|
4055
|
+
try {
|
|
4056
|
+
ack = await sync.callAction(
|
|
4057
|
+
APP_TOOL_ID,
|
|
4058
|
+
RELOAD_ACTION,
|
|
4059
|
+
{ strategy: args.strategy ?? "auto" },
|
|
4060
|
+
target
|
|
4061
|
+
);
|
|
4062
|
+
} catch (inAppError) {
|
|
4063
|
+
await tryDevServer(inAppError);
|
|
4064
|
+
}
|
|
3925
4065
|
}
|
|
3926
4066
|
const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3927
4067
|
if (args.wait === false) {
|
|
@@ -4931,9 +5071,8 @@ function runInit(opts = {}) {
|
|
|
4931
5071
|
const entry = resolveEntry(opts, log);
|
|
4932
5072
|
const done = [];
|
|
4933
5073
|
upsertMcpConfig(import_node_path4.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
}
|
|
5074
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.join(cwd, ".cursor"), { recursive: true });
|
|
5075
|
+
upsertMcpConfig(import_node_path4.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
4937
5076
|
if (import_node_fs4.default.existsSync(import_node_path4.default.join(cwd, ".vscode"))) {
|
|
4938
5077
|
upsertMcpConfig(import_node_path4.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
4939
5078
|
}
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -127,16 +127,17 @@ declare class SyncClient {
|
|
|
127
127
|
* first miss we stop paying the full timeout on every subsequent call.
|
|
128
128
|
*/
|
|
129
129
|
private noteTimeout;
|
|
130
|
-
/** True once a device has missed
|
|
130
|
+
/** True once a device has missed TWO calls and hasn't answered since. */
|
|
131
131
|
isUnresponsive(deviceId: string): boolean;
|
|
132
132
|
/**
|
|
133
133
|
* Full timeout normally; a short one for a device already not answering.
|
|
134
134
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
135
|
+
* A single missed call does not trigger the short budget — one slow
|
|
136
|
+
* `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
|
|
137
|
+
* the full budget (they are the recovery path).
|
|
138
|
+
*
|
|
139
|
+
* Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
|
|
140
|
+
* budget again so the short timeout cannot become self-sustaining.
|
|
140
141
|
*/
|
|
141
142
|
private timeoutFor;
|
|
142
143
|
/**
|
|
@@ -244,8 +245,9 @@ declare function createMcpServer(sync: SyncClient): McpServer;
|
|
|
244
245
|
* project, so a user doesn't have to hand-edit any config.
|
|
245
246
|
*
|
|
246
247
|
* - Registers the `buoy` server in the editor MCP config(s): `.mcp.json`
|
|
247
|
-
* (Claude Code, always),
|
|
248
|
-
*
|
|
248
|
+
* (Claude Code, always), `.cursor/mcp.json` (always — Cursor is a first-class
|
|
249
|
+
* client; we create `.cursor/` if needed), and `.vscode/mcp.json` when
|
|
250
|
+
* `.vscode/` already exists.
|
|
249
251
|
* - Copies the `buoy-optimize` skill into `.claude/skills/`.
|
|
250
252
|
*
|
|
251
253
|
* Idempotent + non-destructive: merges into existing configs (never clobbers
|
package/dist/index.d.ts
CHANGED
|
@@ -127,16 +127,17 @@ declare class SyncClient {
|
|
|
127
127
|
* first miss we stop paying the full timeout on every subsequent call.
|
|
128
128
|
*/
|
|
129
129
|
private noteTimeout;
|
|
130
|
-
/** True once a device has missed
|
|
130
|
+
/** True once a device has missed TWO calls and hasn't answered since. */
|
|
131
131
|
isUnresponsive(deviceId: string): boolean;
|
|
132
132
|
/**
|
|
133
133
|
* Full timeout normally; a short one for a device already not answering.
|
|
134
134
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
135
|
+
* A single missed call does not trigger the short budget — one slow
|
|
136
|
+
* `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
|
|
137
|
+
* the full budget (they are the recovery path).
|
|
138
|
+
*
|
|
139
|
+
* Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
|
|
140
|
+
* budget again so the short timeout cannot become self-sustaining.
|
|
140
141
|
*/
|
|
141
142
|
private timeoutFor;
|
|
142
143
|
/**
|
|
@@ -244,8 +245,9 @@ declare function createMcpServer(sync: SyncClient): McpServer;
|
|
|
244
245
|
* project, so a user doesn't have to hand-edit any config.
|
|
245
246
|
*
|
|
246
247
|
* - Registers the `buoy` server in the editor MCP config(s): `.mcp.json`
|
|
247
|
-
* (Claude Code, always),
|
|
248
|
-
*
|
|
248
|
+
* (Claude Code, always), `.cursor/mcp.json` (always — Cursor is a first-class
|
|
249
|
+
* client; we create `.cursor/` if needed), and `.vscode/mcp.json` when
|
|
250
|
+
* `.vscode/` already exists.
|
|
249
251
|
* - Copies the `buoy-optimize` skill into `.claude/skills/`.
|
|
250
252
|
*
|
|
251
253
|
* Idempotent + non-destructive: merges into existing configs (never clobbers
|