@buoy-gg/mcp 7.0.36 → 7.0.39
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 +29 -19
- package/dist/{chunk-QIINQPNK.mjs → chunk-UXLRYEUQ.mjs} +528 -280
- package/dist/cli.js +580 -347
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +580 -347
- package/dist/index.mjs +1 -1
- package/package.json +4 -3
- package/skill/buoy-optimize/SKILL.md +42 -69
|
@@ -12,6 +12,189 @@ var DEVICE_RETENTION_MS = 15 * 6e4;
|
|
|
12
12
|
var DASHBOARD_NAME = "Dashboard";
|
|
13
13
|
var ALL_DEVICES = "All";
|
|
14
14
|
|
|
15
|
+
// src/sync/NodeAccount.ts
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
17
|
+
import { dirname, join, resolve } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import { createHash } from "crypto";
|
|
20
|
+
import {
|
|
21
|
+
validateLicense,
|
|
22
|
+
isAcceptedValidation,
|
|
23
|
+
isTransientLicenseError,
|
|
24
|
+
resolveTier,
|
|
25
|
+
isRecognizedAccount,
|
|
26
|
+
isBuoyToken,
|
|
27
|
+
verifyFreeToken,
|
|
28
|
+
resolveAccountAccess,
|
|
29
|
+
signCache,
|
|
30
|
+
verifyCacheSignature,
|
|
31
|
+
LICENSE_LAUNCH_VALIDATION_TTL,
|
|
32
|
+
LICENSE_REVALIDATION_INTERVAL,
|
|
33
|
+
LICENSE_TRANSIENT_RETRY_DELAYS,
|
|
34
|
+
KEYGEN_CONFIG
|
|
35
|
+
} from "@buoy-gg/license/headless";
|
|
36
|
+
var KEY_NAMES = ["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY"];
|
|
37
|
+
function readProjectKey(projectDir, env = process.env) {
|
|
38
|
+
for (const name of KEY_NAMES) if (env[name] !== void 0) return env[name].trim().toUpperCase() || null;
|
|
39
|
+
for (const file of [".env.development.local", ".env.local", ".env"]) {
|
|
40
|
+
let contents;
|
|
41
|
+
try {
|
|
42
|
+
contents = readFileSync(join(projectDir, file), "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const values = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
48
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);
|
|
49
|
+
if (!match) continue;
|
|
50
|
+
const raw = match[2].trim();
|
|
51
|
+
const quoted = raw.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);
|
|
52
|
+
values.set(match[1], quoted ? quoted[2] : raw.split("#")[0].trim());
|
|
53
|
+
}
|
|
54
|
+
for (const name of KEY_NAMES) if (values.has(name)) return values.get(name).trim().toUpperCase() || null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
var NodeAccount = class {
|
|
59
|
+
constructor(changed, opts = {}) {
|
|
60
|
+
this.changed = changed;
|
|
61
|
+
this.key = null;
|
|
62
|
+
this.cache = null;
|
|
63
|
+
this.timer = null;
|
|
64
|
+
this.generation = 0;
|
|
65
|
+
this.busy = false;
|
|
66
|
+
this.nextCheck = 0;
|
|
67
|
+
this.retry = 0;
|
|
68
|
+
this.lastAdmission = false;
|
|
69
|
+
this.hasAccess = () => resolveAccountAccess({
|
|
70
|
+
initialized: true,
|
|
71
|
+
validating: false,
|
|
72
|
+
key: this.key,
|
|
73
|
+
tierSource: this.cache?.tier ? "key" : "none",
|
|
74
|
+
verifiedAt: this.cache?.cachedAt ?? null
|
|
75
|
+
}).status === "admitted";
|
|
76
|
+
this.projectDir = resolve(opts.projectDir ?? process.env.BUOY_PROJECT_DIR ?? process.cwd());
|
|
77
|
+
const project = createHash("sha256").update(this.projectDir).digest("hex").slice(0, 20);
|
|
78
|
+
this.cacheFile = opts.cacheFile ?? join(homedir(), ".buoy", `account-${project}.json`);
|
|
79
|
+
}
|
|
80
|
+
start() {
|
|
81
|
+
if (this.timer) return;
|
|
82
|
+
try {
|
|
83
|
+
const cached = JSON.parse(readFileSync(this.cacheFile, "utf8"));
|
|
84
|
+
if (cached.valid && verifyCacheSignature(cached)) this.cache = cached;
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
void this.tick();
|
|
88
|
+
this.timer = setInterval(() => {
|
|
89
|
+
void this.tick();
|
|
90
|
+
}, 1e3);
|
|
91
|
+
this.timer.unref();
|
|
92
|
+
}
|
|
93
|
+
stop() {
|
|
94
|
+
++this.generation;
|
|
95
|
+
if (this.timer) clearInterval(this.timer);
|
|
96
|
+
this.timer = null;
|
|
97
|
+
this.key = null;
|
|
98
|
+
this.cache = null;
|
|
99
|
+
this.notify();
|
|
100
|
+
}
|
|
101
|
+
notify() {
|
|
102
|
+
const admitted = this.hasAccess();
|
|
103
|
+
if (admitted === this.lastAdmission) return;
|
|
104
|
+
this.lastAdmission = admitted;
|
|
105
|
+
this.changed();
|
|
106
|
+
}
|
|
107
|
+
async tick() {
|
|
108
|
+
const selected = readProjectKey(this.projectDir);
|
|
109
|
+
if (selected !== this.key) {
|
|
110
|
+
++this.generation;
|
|
111
|
+
this.key = selected;
|
|
112
|
+
if (this.cache?.licenseKey !== selected) this.cache = null;
|
|
113
|
+
this.nextCheck = 0;
|
|
114
|
+
this.retry = 0;
|
|
115
|
+
if (!selected) this.removeCache();
|
|
116
|
+
}
|
|
117
|
+
this.notify();
|
|
118
|
+
if (!selected || this.busy || Date.now() < this.nextCheck) return;
|
|
119
|
+
if (this.cache && this.hasAccess() && Date.now() - this.cache.lastValidatedAt < LICENSE_LAUNCH_VALIDATION_TTL && this.nextCheck === 0) {
|
|
120
|
+
this.nextCheck = this.cache.lastValidatedAt + LICENSE_LAUNCH_VALIDATION_TTL;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.busy = true;
|
|
124
|
+
const startedAt = this.generation;
|
|
125
|
+
try {
|
|
126
|
+
if (isBuoyToken(selected)) {
|
|
127
|
+
const claims = verifyFreeToken(selected);
|
|
128
|
+
if (!claims) {
|
|
129
|
+
this.cache = null;
|
|
130
|
+
this.removeCache();
|
|
131
|
+
} else {
|
|
132
|
+
const now2 = Date.now();
|
|
133
|
+
this.cache = signCache({
|
|
134
|
+
licenseKey: selected,
|
|
135
|
+
licenseId: `token:${claims.accountHash}`,
|
|
136
|
+
valid: true,
|
|
137
|
+
tier: "free",
|
|
138
|
+
isPro: false,
|
|
139
|
+
expiresAt: null,
|
|
140
|
+
cachedAt: now2,
|
|
141
|
+
lastValidatedAt: now2
|
|
142
|
+
});
|
|
143
|
+
this.persistCache();
|
|
144
|
+
}
|
|
145
|
+
this.nextCheck = Date.now() + LICENSE_REVALIDATION_INTERVAL;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const response = await validateLicense(selected);
|
|
149
|
+
if (startedAt !== this.generation || readProjectKey(this.projectDir) !== selected) return;
|
|
150
|
+
const product = response.data?.relationships?.product?.data?.id;
|
|
151
|
+
const expired = response.meta.code === "EXPIRED" && (isRecognizedAccount(response.data) || this.cache?.licenseKey === selected);
|
|
152
|
+
if (product && product !== KEYGEN_CONFIG.productId || !expired && !isAcceptedValidation(response)) {
|
|
153
|
+
this.cache = null;
|
|
154
|
+
this.removeCache();
|
|
155
|
+
this.nextCheck = Date.now() + LICENSE_REVALIDATION_INTERVAL;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const tier = expired ? "free" : resolveTier(response.data);
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
this.cache = signCache({
|
|
161
|
+
licenseKey: selected,
|
|
162
|
+
licenseId: response.data?.id ?? "",
|
|
163
|
+
valid: true,
|
|
164
|
+
tier,
|
|
165
|
+
isPro: tier === "pro",
|
|
166
|
+
expiresAt: expired ? null : response.data.attributes.expiry,
|
|
167
|
+
cachedAt: now,
|
|
168
|
+
lastValidatedAt: now
|
|
169
|
+
});
|
|
170
|
+
this.persistCache();
|
|
171
|
+
this.retry = 0;
|
|
172
|
+
this.nextCheck = now + LICENSE_REVALIDATION_INTERVAL;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (startedAt !== this.generation) return;
|
|
175
|
+
const delay6 = isTransientLicenseError(error) ? LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++] : void 0;
|
|
176
|
+
this.nextCheck = Date.now() + (delay6 ?? LICENSE_REVALIDATION_INTERVAL);
|
|
177
|
+
} finally {
|
|
178
|
+
this.busy = false;
|
|
179
|
+
this.notify();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
persistCache() {
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(dirname(this.cacheFile), { recursive: true, mode: 448 });
|
|
185
|
+
writeFileSync(`${this.cacheFile}.tmp`, JSON.stringify(this.cache), { mode: 384 });
|
|
186
|
+
renameSync(`${this.cacheFile}.tmp`, this.cacheFile);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
removeCache() {
|
|
191
|
+
try {
|
|
192
|
+
unlinkSync(this.cacheFile);
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
15
198
|
// src/sync/SyncClient.ts
|
|
16
199
|
import http from "http";
|
|
17
200
|
import { randomUUID } from "crypto";
|
|
@@ -51,6 +234,9 @@ function diagnoseCrashed(opts) {
|
|
|
51
234
|
var SyncClient = class {
|
|
52
235
|
constructor(opts = {}) {
|
|
53
236
|
this.socket = null;
|
|
237
|
+
this.admitted = false;
|
|
238
|
+
this.lockedDevices = /* @__PURE__ */ new Set();
|
|
239
|
+
this.hostAccount = null;
|
|
54
240
|
this.broker = null;
|
|
55
241
|
/** Latest capabilities announce per deviceId. */
|
|
56
242
|
this.capabilities = /* @__PURE__ */ new Map();
|
|
@@ -99,7 +285,10 @@ var SyncClient = class {
|
|
|
99
285
|
);
|
|
100
286
|
}
|
|
101
287
|
this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);
|
|
288
|
+
this.hostAccount = new NodeAccount(() => this.broker?.refreshAccess());
|
|
289
|
+
this.hostAccount.start();
|
|
102
290
|
this.broker = createBroker({
|
|
291
|
+
canUseDesktop: this.hostAccount.hasAccess,
|
|
103
292
|
port: this.port,
|
|
104
293
|
onError: (e) => this.log(`broker error: ${e.code ?? e.message}`)
|
|
105
294
|
});
|
|
@@ -112,7 +301,7 @@ var SyncClient = class {
|
|
|
112
301
|
return mode;
|
|
113
302
|
}
|
|
114
303
|
connectSocket() {
|
|
115
|
-
return new Promise((
|
|
304
|
+
return new Promise((resolve2, reject) => {
|
|
116
305
|
const socket = ioClient(this.url, {
|
|
117
306
|
transports: ["websocket"],
|
|
118
307
|
query: { deviceName: DASHBOARD_NAME },
|
|
@@ -122,31 +311,59 @@ var SyncClient = class {
|
|
|
122
311
|
});
|
|
123
312
|
this.socket = socket;
|
|
124
313
|
let settled = false;
|
|
125
|
-
const
|
|
314
|
+
const onReady = () => {
|
|
126
315
|
if (settled) return;
|
|
127
316
|
settled = true;
|
|
128
317
|
this.log("connected to broker");
|
|
129
|
-
|
|
318
|
+
resolve2();
|
|
130
319
|
};
|
|
131
|
-
|
|
132
|
-
|
|
320
|
+
const readinessTimeout = setTimeout(() => {
|
|
321
|
+
if (settled) return;
|
|
322
|
+
settled = true;
|
|
323
|
+
reject(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."));
|
|
324
|
+
}, 5e3);
|
|
325
|
+
socket.on("buoy:access", (message) => {
|
|
326
|
+
const ready = !!message && typeof message === "object" && message.protocol === 1 && message.ready === true;
|
|
327
|
+
this.setAdmission(ready);
|
|
328
|
+
if (ready) {
|
|
329
|
+
clearTimeout(readinessTimeout);
|
|
330
|
+
this.rearmWatches();
|
|
331
|
+
onReady();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
socket.on("connect", () => {
|
|
335
|
+
socket.emit("buoy:access:request");
|
|
336
|
+
});
|
|
337
|
+
socket.on("disconnect", () => this.setAdmission(false, true));
|
|
133
338
|
socket.once("connect_error", (err) => {
|
|
134
339
|
if (settled) return;
|
|
135
340
|
settled = true;
|
|
341
|
+
clearTimeout(readinessTimeout);
|
|
136
342
|
reject(new Error(`failed to connect to broker at ${this.url}: ${err.message}`));
|
|
137
343
|
});
|
|
138
344
|
socket.on("devtool-capabilities", (msg) => {
|
|
345
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
139
346
|
this.capabilities.set(msg.persistentDeviceId, msg);
|
|
140
347
|
void this.learnDevServerOrigin(msg.persistentDeviceId);
|
|
141
348
|
});
|
|
142
349
|
socket.on("all-devices-update", (list) => {
|
|
350
|
+
if (!this.admitted) return;
|
|
143
351
|
if (!Array.isArray(list)) return;
|
|
352
|
+
let deviceReadmitted = false;
|
|
353
|
+
for (const device of list) {
|
|
354
|
+
if (device.deviceId && device.tier === "locked") this.lockDevice(String(device.deviceId));
|
|
355
|
+
else if (device.deviceId && (device.tier === "free" || device.tier === "pro")) {
|
|
356
|
+
deviceReadmitted = this.lockedDevices.delete(String(device.deviceId)) || deviceReadmitted;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
144
359
|
this.devices = list;
|
|
145
360
|
const now = Date.now();
|
|
146
361
|
for (const d of list) if (d.deviceId) this.lastSeen.set(d.deviceId, now);
|
|
147
362
|
this.pruneStaleDevices();
|
|
363
|
+
if (deviceReadmitted) this.rearmWatches();
|
|
148
364
|
});
|
|
149
365
|
socket.on("devtool-sync", (msg) => {
|
|
366
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
150
367
|
const key = snapKey(msg.persistentDeviceId, msg.toolId);
|
|
151
368
|
this.snapshots.set(key, msg.payload);
|
|
152
369
|
this.snapshotAt.set(key, Date.now());
|
|
@@ -157,6 +374,7 @@ var SyncClient = class {
|
|
|
157
374
|
}
|
|
158
375
|
});
|
|
159
376
|
socket.on("devtool-action-result", (msg) => {
|
|
377
|
+
if (!this.admitted) return;
|
|
160
378
|
const pending = this.pendingActions.get(msg.requestId);
|
|
161
379
|
if (!pending) return;
|
|
162
380
|
this.pendingActions.delete(msg.requestId);
|
|
@@ -174,6 +392,55 @@ var SyncClient = class {
|
|
|
174
392
|
* with every simulator erase and fresh dev client, and each one can hold
|
|
175
393
|
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
176
394
|
*/
|
|
395
|
+
requireAdmission() {
|
|
396
|
+
if (!this.admitted) throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.");
|
|
397
|
+
}
|
|
398
|
+
setAdmission(ready, preserveWatchIntent = false) {
|
|
399
|
+
this.admitted = ready;
|
|
400
|
+
if (ready) return;
|
|
401
|
+
this.devices = [];
|
|
402
|
+
this.lockedDevices.clear();
|
|
403
|
+
this.capabilities.clear();
|
|
404
|
+
this.snapshots.clear();
|
|
405
|
+
this.snapshotAt.clear();
|
|
406
|
+
this.devServerOrigins.clear();
|
|
407
|
+
this.originProbed.clear();
|
|
408
|
+
this.health.clear();
|
|
409
|
+
this.lastSeen.clear();
|
|
410
|
+
if (!preserveWatchIntent) this.watching.clear();
|
|
411
|
+
for (const pending of this.pendingActions.values()) {
|
|
412
|
+
clearTimeout(pending.timer);
|
|
413
|
+
pending.reject(new Error("Buoy account access ended while the action was pending."));
|
|
414
|
+
}
|
|
415
|
+
this.pendingActions.clear();
|
|
416
|
+
for (const waiters of this.snapshotWaiters.values()) for (const waiter of waiters) waiter(void 0);
|
|
417
|
+
this.snapshotWaiters.clear();
|
|
418
|
+
if (this.socket) this.socket.sendBuffer = [];
|
|
419
|
+
}
|
|
420
|
+
requireDeviceAccount(deviceId) {
|
|
421
|
+
if (this.lockedDevices.has(deviceId)) throw new Error("The device's Buoy account access ended. Sign in and verify its key.");
|
|
422
|
+
}
|
|
423
|
+
lockDevice(deviceId) {
|
|
424
|
+
this.lockedDevices.add(deviceId);
|
|
425
|
+
this.capabilities.delete(deviceId);
|
|
426
|
+
this.health.delete(deviceId);
|
|
427
|
+
this.devServerOrigins.delete(deviceId);
|
|
428
|
+
this.originProbed.delete(deviceId);
|
|
429
|
+
const prefix = `${deviceId}::`;
|
|
430
|
+
for (const key of this.snapshots.keys()) if (key.startsWith(prefix)) {
|
|
431
|
+
this.snapshots.delete(key);
|
|
432
|
+
this.snapshotAt.delete(key);
|
|
433
|
+
}
|
|
434
|
+
for (const [key, waiters] of this.snapshotWaiters) if (key.startsWith(prefix)) {
|
|
435
|
+
this.snapshotWaiters.delete(key);
|
|
436
|
+
for (const waiter of waiters) waiter(void 0);
|
|
437
|
+
}
|
|
438
|
+
for (const [id, pending] of this.pendingActions) if (pending.target === deviceId || pending.target === ALL_DEVICES) {
|
|
439
|
+
clearTimeout(pending.timer);
|
|
440
|
+
this.pendingActions.delete(id);
|
|
441
|
+
pending.reject(new Error("Device account access ended while the action was pending."));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
177
444
|
pruneStaleDevices() {
|
|
178
445
|
const cutoff = Date.now() - DEVICE_RETENTION_MS;
|
|
179
446
|
for (const [deviceId, seen] of this.lastSeen) {
|
|
@@ -198,6 +465,9 @@ var SyncClient = class {
|
|
|
198
465
|
}
|
|
199
466
|
/** Disconnect and tear down a spawned broker, if any. */
|
|
200
467
|
async stop() {
|
|
468
|
+
this.setAdmission(false);
|
|
469
|
+
this.hostAccount?.stop();
|
|
470
|
+
this.hostAccount = null;
|
|
201
471
|
for (const [, p] of this.pendingActions) {
|
|
202
472
|
clearTimeout(p.timer);
|
|
203
473
|
p.reject(new Error("SyncClient stopped"));
|
|
@@ -217,6 +487,7 @@ var SyncClient = class {
|
|
|
217
487
|
// ----------------------------------------------------------------
|
|
218
488
|
/** All devices the broker knows about, merged with their advertised tools. */
|
|
219
489
|
listDevices() {
|
|
490
|
+
if (!this.admitted) return [];
|
|
220
491
|
return this.devices.filter((d) => d.deviceName !== DASHBOARD_NAME && d.deviceId).map((d) => {
|
|
221
492
|
const caps = this.capabilities.get(d.deviceId);
|
|
222
493
|
return {
|
|
@@ -225,7 +496,7 @@ var SyncClient = class {
|
|
|
225
496
|
platform: d.platform,
|
|
226
497
|
isConnected: d.isConnected !== false,
|
|
227
498
|
isPro: d.isPro === true,
|
|
228
|
-
tier: d.tier
|
|
499
|
+
tier: d.tier === "free" || d.tier === "pro" ? d.tier : d.tier === void 0 && d.isPro === true ? "pro" : "locked",
|
|
229
500
|
collision: d.collision === true,
|
|
230
501
|
tools: caps?.tools ?? []
|
|
231
502
|
};
|
|
@@ -248,9 +519,19 @@ var SyncClient = class {
|
|
|
248
519
|
* or many, an informative error is thrown (callers can pass "All" explicitly).
|
|
249
520
|
*/
|
|
250
521
|
resolveDevice(deviceId) {
|
|
251
|
-
|
|
252
|
-
|
|
522
|
+
this.requireAdmission();
|
|
523
|
+
if (deviceId) {
|
|
524
|
+
this.requireDeviceAccount(deviceId);
|
|
525
|
+
return deviceId;
|
|
526
|
+
}
|
|
527
|
+
const allConnected = this.connectedDevices();
|
|
528
|
+
const connected = allConnected.filter((d) => !this.lockedDevices.has(d.deviceId));
|
|
253
529
|
if (connected.length === 0) {
|
|
530
|
+
if (allConnected.length > 0) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`Devices are connected (${allConnected.map((d) => d.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
254
535
|
throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.");
|
|
255
536
|
}
|
|
256
537
|
if (connected.length === 1) return connected[0].deviceId;
|
|
@@ -276,9 +557,10 @@ var SyncClient = class {
|
|
|
276
557
|
* is blocked.
|
|
277
558
|
*/
|
|
278
559
|
requireProDevice(deviceId) {
|
|
560
|
+
this.requireAdmission();
|
|
279
561
|
const tierNote = (dev2) => {
|
|
280
562
|
if (!dev2) return "";
|
|
281
|
-
return dev2.tier === "
|
|
563
|
+
return dev2.tier === "locked" ? ` The broker has not confirmed account access for "${dev2.deviceName}"; a locked status does not establish whether a key is configured.` : ` The connected app "${dev2.deviceName}" is on the ${dev2.tier} tier.`;
|
|
282
564
|
};
|
|
283
565
|
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
284
566
|
dev2
|
|
@@ -393,6 +675,7 @@ var SyncClient = class {
|
|
|
393
675
|
}
|
|
394
676
|
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
395
677
|
snapshotAgeMs(toolId, deviceId) {
|
|
678
|
+
this.requireAdmission();
|
|
396
679
|
const at = this.snapshotAt.get(snapKey(deviceId, toolId));
|
|
397
680
|
return at ? Date.now() - at : null;
|
|
398
681
|
}
|
|
@@ -428,6 +711,7 @@ var SyncClient = class {
|
|
|
428
711
|
}
|
|
429
712
|
/** The dev-server origin last reported by this device, if any. */
|
|
430
713
|
devServerOrigin(deviceId) {
|
|
714
|
+
this.requireAdmission();
|
|
431
715
|
const known = this.devServerOrigins.get(deviceId);
|
|
432
716
|
if (known) return known;
|
|
433
717
|
const snap = this.snapshots.get(snapKey(deviceId, "app"));
|
|
@@ -461,6 +745,7 @@ var SyncClient = class {
|
|
|
461
745
|
this.sendWatch(toolId, targetDeviceId);
|
|
462
746
|
}
|
|
463
747
|
sendWatch(toolId, targetDeviceId) {
|
|
748
|
+
if (!this.admitted || !this.socket?.connected || this.lockedDevices.has(targetDeviceId)) return;
|
|
464
749
|
this.socket?.emit("devtool-watch", {
|
|
465
750
|
toolId,
|
|
466
751
|
targetDeviceId,
|
|
@@ -486,10 +771,12 @@ var SyncClient = class {
|
|
|
486
771
|
unwatch(toolId, targetDeviceId) {
|
|
487
772
|
const key = snapKey(targetDeviceId, toolId);
|
|
488
773
|
if (!this.watching.delete(key)) return;
|
|
489
|
-
this.socket?.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
774
|
+
if (this.admitted && this.socket?.connected) this.socket.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
490
775
|
}
|
|
491
776
|
/** Last snapshot received for a tool/device, if any. */
|
|
492
777
|
cachedSnapshot(toolId, deviceId) {
|
|
778
|
+
this.requireAdmission();
|
|
779
|
+
this.requireDeviceAccount(deviceId);
|
|
493
780
|
return this.snapshots.get(snapKey(deviceId, toolId));
|
|
494
781
|
}
|
|
495
782
|
/**
|
|
@@ -517,6 +804,8 @@ var SyncClient = class {
|
|
|
517
804
|
const next = this.waitForSnapshot(key, budget);
|
|
518
805
|
this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
|
|
519
806
|
const payload = await next;
|
|
807
|
+
this.requireAdmission();
|
|
808
|
+
this.requireDeviceAccount(target);
|
|
520
809
|
if (payload !== void 0) {
|
|
521
810
|
if (!opts.silent) this.noteOk(target);
|
|
522
811
|
return { deviceId: target, payload };
|
|
@@ -537,16 +826,16 @@ var SyncClient = class {
|
|
|
537
826
|
return `\u26A0\uFE0F Served from the last sync (${ageText}) \u2014 ${this.unresponsiveNote(deviceId)}`;
|
|
538
827
|
}
|
|
539
828
|
waitForSnapshot(key, timeoutMs) {
|
|
540
|
-
return new Promise((
|
|
829
|
+
return new Promise((resolve2) => {
|
|
541
830
|
const waiters = this.snapshotWaiters.get(key) ?? [];
|
|
542
831
|
const timer = setTimeout(() => {
|
|
543
832
|
const remaining = (this.snapshotWaiters.get(key) ?? []).filter((w) => w !== onSnap);
|
|
544
833
|
this.snapshotWaiters.set(key, remaining);
|
|
545
|
-
|
|
834
|
+
resolve2(void 0);
|
|
546
835
|
}, timeoutMs);
|
|
547
836
|
const onSnap = (payload) => {
|
|
548
837
|
clearTimeout(timer);
|
|
549
|
-
|
|
838
|
+
resolve2(payload);
|
|
550
839
|
};
|
|
551
840
|
waiters.push(onSnap);
|
|
552
841
|
this.snapshotWaiters.set(key, waiters);
|
|
@@ -560,11 +849,12 @@ var SyncClient = class {
|
|
|
560
849
|
* target one device, or "All" to broadcast (resolves with the first responder).
|
|
561
850
|
*/
|
|
562
851
|
async callAction(toolId, action, params, deviceId, opts = {}) {
|
|
852
|
+
this.requireAdmission();
|
|
563
853
|
const target = deviceId === ALL_DEVICES ? ALL_DEVICES : this.resolveDevice(deviceId);
|
|
564
854
|
if (!this.socket) throw new Error("SyncClient is not connected");
|
|
565
855
|
const requestId = randomUUID();
|
|
566
856
|
const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target, toolId, action);
|
|
567
|
-
const result = await new Promise((
|
|
857
|
+
const result = await new Promise((resolve2, reject) => {
|
|
568
858
|
const timer = setTimeout(() => {
|
|
569
859
|
this.pendingActions.delete(requestId);
|
|
570
860
|
if (target !== ALL_DEVICES && !opts.silent) this.noteTimeout(target);
|
|
@@ -574,7 +864,7 @@ var SyncClient = class {
|
|
|
574
864
|
)
|
|
575
865
|
);
|
|
576
866
|
}, timeoutMs);
|
|
577
|
-
this.pendingActions.set(requestId, { resolve, reject, timer });
|
|
867
|
+
this.pendingActions.set(requestId, { resolve: resolve2, reject, timer, target });
|
|
578
868
|
this.socket.emit("devtool-action", {
|
|
579
869
|
requestId,
|
|
580
870
|
toolId,
|
|
@@ -592,16 +882,16 @@ var SyncClient = class {
|
|
|
592
882
|
};
|
|
593
883
|
var snapKey = (deviceId, toolId) => `${deviceId}::${toolId}`;
|
|
594
884
|
function probeBroker(url, timeoutMs = 800) {
|
|
595
|
-
return new Promise((
|
|
885
|
+
return new Promise((resolve2) => {
|
|
596
886
|
const req = http.get(url, (res) => {
|
|
597
887
|
res.resume();
|
|
598
|
-
|
|
888
|
+
resolve2((res.statusCode ?? 0) > 0);
|
|
599
889
|
});
|
|
600
890
|
req.setTimeout(timeoutMs, () => {
|
|
601
891
|
req.destroy();
|
|
602
|
-
|
|
892
|
+
resolve2(false);
|
|
603
893
|
});
|
|
604
|
-
req.on("error", () =>
|
|
894
|
+
req.on("error", () => resolve2(false));
|
|
605
895
|
});
|
|
606
896
|
}
|
|
607
897
|
async function waitForBroker(url, attempts = 20) {
|
|
@@ -614,7 +904,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
614
904
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
615
905
|
|
|
616
906
|
// src/version.ts
|
|
617
|
-
var VERSION = "7.0.
|
|
907
|
+
var VERSION = "7.0.39" ? "7.0.39" : "0.0.0-dev";
|
|
618
908
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
619
909
|
|
|
620
910
|
// src/update/checkForUpdate.ts
|
|
@@ -711,7 +1001,7 @@ function registerGenericTools(server, sync) {
|
|
|
711
1001
|
}
|
|
712
1002
|
const connected = devices.filter((d) => d.isConnected);
|
|
713
1003
|
const anyPro = connected.some((d) => d.isPro);
|
|
714
|
-
const anySignedIn = connected.some((d) => d.tier !== "
|
|
1004
|
+
const anySignedIn = connected.some((d) => d.tier !== "locked");
|
|
715
1005
|
const collided = connected.filter((d) => d.collision);
|
|
716
1006
|
return json({
|
|
717
1007
|
devices,
|
|
@@ -721,7 +1011,7 @@ function registerGenericTools(server, sync) {
|
|
|
721
1011
|
...anyPro ? {} : {
|
|
722
1012
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
723
1013
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
724
|
-
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless." : "The
|
|
1014
|
+
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless." : "The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."
|
|
725
1015
|
},
|
|
726
1016
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
727
1017
|
});
|
|
@@ -1037,7 +1327,7 @@ function formatRankingMarkdown(ranking) {
|
|
|
1037
1327
|
].filter(Boolean).join(" \xB7 ");
|
|
1038
1328
|
if (leaderLine) lines.push(`**Per-metric best:** ${leaderLine}`);
|
|
1039
1329
|
lines.push(
|
|
1040
|
-
ranking.balanced.length ? `**
|
|
1330
|
+
ranking.balanced.length ? `**No configured risk flags:** ${ranking.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.` : `**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`
|
|
1041
1331
|
);
|
|
1042
1332
|
if (ranking.flagged.length) {
|
|
1043
1333
|
lines.push(
|
|
@@ -1263,7 +1553,7 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1263
1553
|
server.registerTool(
|
|
1264
1554
|
"run_benchmark_batch",
|
|
1265
1555
|
{
|
|
1266
|
-
description: "Run a perf-monitor benchmark batch and return
|
|
1556
|
+
description: "Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",
|
|
1267
1557
|
inputSchema: {
|
|
1268
1558
|
targetRoute: z3.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1269
1559
|
cases: z3.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
@@ -1274,10 +1564,10 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1274
1564
|
coolDownMs: z3.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1275
1565
|
navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1276
1566
|
discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1277
|
-
shuffleCases: z3.boolean().optional().describe("Interleave
|
|
1567
|
+
shuffleCases: z3.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),
|
|
1278
1568
|
reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1279
1569
|
captureRenders: z3.boolean().optional().describe(
|
|
1280
|
-
"Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds
|
|
1570
|
+
"Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."
|
|
1281
1571
|
),
|
|
1282
1572
|
deviceId: z3.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1283
1573
|
maxWaitMs: z3.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
@@ -1471,7 +1761,7 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1471
1761
|
server.registerTool(
|
|
1472
1762
|
"compare_reports",
|
|
1473
1763
|
{
|
|
1474
|
-
description: "Compare two saved benchmark runs head-to-head: avg + p95 for FPS/CPU/memory, jank, AND render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use
|
|
1764
|
+
description: "Compare two saved benchmark runs head-to-head: avg + p95 for FPS/CPU/memory, jank, AND render counts \u2014 total, commits, and wasted (parent cascades + identity-only prop churn) \u2014 each with deltas. Use comparable workloads and inspect available metrics together. A wasted-render delta alone does not prove a fix.",
|
|
1475
1765
|
inputSchema: {
|
|
1476
1766
|
reportIdA: z3.string().describe("First (baseline) report id."),
|
|
1477
1767
|
reportIdB: z3.string().describe("Second report id to compare against the baseline."),
|
|
@@ -1540,7 +1830,7 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1540
1830
|
navTimeoutMs: z3.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1541
1831
|
runsPerCase: z3.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1542
1832
|
discardWarmupRuns: z3.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1543
|
-
shuffleCases: z3.boolean().optional().describe("Interleave
|
|
1833
|
+
shuffleCases: z3.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),
|
|
1544
1834
|
reloadBetweenCases: z3.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1545
1835
|
captureRenders: z3.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1546
1836
|
deviceId: deviceIdInput
|
|
@@ -1605,7 +1895,7 @@ function registerEventsTools(server, sync) {
|
|
|
1605
1895
|
server.registerTool(
|
|
1606
1896
|
"get_events",
|
|
1607
1897
|
{
|
|
1608
|
-
description: "
|
|
1898
|
+
description: "Read retained events from connected Buoy sources. Filter with sources, such as ['network'], ['redux'], ['react-query'], ['storage'] or ['route']. Returns compact summaries by default; use includeData:true for available payload details. Coverage depends on integrations and retention. An empty timeline does not prove an event never happened. Use get_network_requests for request IDs and get_snapshot for available current tool state.",
|
|
1609
1899
|
inputSchema: {
|
|
1610
1900
|
limit: z4.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1611
1901
|
sources: z4.array(z4.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
|
|
@@ -1682,7 +1972,7 @@ function registerReactQueryTools(server, sync) {
|
|
|
1682
1972
|
server.registerTool(
|
|
1683
1973
|
"get_react_query",
|
|
1684
1974
|
{
|
|
1685
|
-
description: "Read the
|
|
1975
|
+
description: "Read queries reported by the connected React Query integration, including available status, observer counts, timestamps and errors. includeData:true requests cached payloads; staleOnly:true filters stale queries. Use each returned hash with react_query_action. History depends on retained events.",
|
|
1686
1976
|
inputSchema: {
|
|
1687
1977
|
includeData: z5.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
1688
1978
|
staleOnly: z5.boolean().optional().describe("Only stale queries. Default false."),
|
|
@@ -1741,13 +2031,13 @@ ${lines.join("\n")}${note}`);
|
|
|
1741
2031
|
const target = sync.requireProDevice(args.deviceId);
|
|
1742
2032
|
if (args.action === "clear") {
|
|
1743
2033
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
1744
|
-
return text("
|
|
2034
|
+
return text("Requested clearing of the connected React Query cache. Read it again to confirm.");
|
|
1745
2035
|
}
|
|
1746
2036
|
if (!args.queryHash) {
|
|
1747
2037
|
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
1748
2038
|
}
|
|
1749
2039
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
1750
|
-
return text(`
|
|
2040
|
+
return text(`Requested ${args.action} on query \`${args.queryHash}\`.`);
|
|
1751
2041
|
} catch (e) {
|
|
1752
2042
|
return fail2(errMsg2(e));
|
|
1753
2043
|
}
|
|
@@ -1762,7 +2052,7 @@ function registerZustandTools(server, sync) {
|
|
|
1762
2052
|
server.registerTool(
|
|
1763
2053
|
"get_zustand_state",
|
|
1764
2054
|
{
|
|
1765
|
-
description:
|
|
2055
|
+
description: 'Read registered Zustand stores, available state keys, change counts and persistence metadata. includeValues:true requests values. Use get_events with sources:["zustand"] for retained changes.',
|
|
1766
2056
|
inputSchema: {
|
|
1767
2057
|
includeValues: z6.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
1768
2058
|
limit: z6.number().optional().describe("First N stores. Default all."),
|
|
@@ -1814,7 +2104,7 @@ ${lines.join("\n")}${note}`);
|
|
|
1814
2104
|
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
1815
2105
|
target
|
|
1816
2106
|
);
|
|
1817
|
-
return text(`
|
|
2107
|
+
return text(`Requested state update on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
1818
2108
|
})
|
|
1819
2109
|
);
|
|
1820
2110
|
}
|
|
@@ -1826,7 +2116,7 @@ function registerJotaiTools(server, sync) {
|
|
|
1826
2116
|
server.registerTool(
|
|
1827
2117
|
"get_jotai_state",
|
|
1828
2118
|
{
|
|
1829
|
-
description:
|
|
2119
|
+
description: 'Read watched Jotai atoms and their available labels, change counts and writable status. includeValues:true requests values. Use get_events with sources:["jotai"] for retained changes.',
|
|
1830
2120
|
inputSchema: {
|
|
1831
2121
|
includeValues: z7.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
1832
2122
|
limit: z7.number().optional().describe("First N atoms. Default all."),
|
|
@@ -1872,7 +2162,7 @@ ${lines.join("\n")}${note}`);
|
|
|
1872
2162
|
},
|
|
1873
2163
|
withDevice(sync, async (args, target) => {
|
|
1874
2164
|
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
1875
|
-
return text(`
|
|
2165
|
+
return text(`Requested update of Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
1876
2166
|
})
|
|
1877
2167
|
);
|
|
1878
2168
|
}
|
|
@@ -1896,7 +2186,7 @@ function registerReduxTools(server, sync) {
|
|
|
1896
2186
|
server.registerTool(
|
|
1897
2187
|
"get_redux_state",
|
|
1898
2188
|
{
|
|
1899
|
-
description:
|
|
2189
|
+
description: 'Read available state from the connected Redux integration. Lists top-level slices by default; includeValues:true requests state values. Use get_events with sources:["redux"] for retained action history.',
|
|
1900
2190
|
inputSchema: {
|
|
1901
2191
|
includeValues: z8.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
1902
2192
|
deviceId: deviceIdInput
|
|
@@ -2045,9 +2335,9 @@ ${lines.join("\n")}`);
|
|
|
2045
2335
|
server.registerTool(
|
|
2046
2336
|
"storage_action",
|
|
2047
2337
|
{
|
|
2048
|
-
description: "Modify the app's AsyncStorage: set a key, remove a key, or clear
|
|
2338
|
+
description: "Modify the app's AsyncStorage: set a key, remove a key, or clear supported app AsyncStorage keys (Buoy's own devtool keys are preserved). Values are stored as strings \u2014 JSON-encode objects yourself.",
|
|
2049
2339
|
inputSchema: {
|
|
2050
|
-
action: z9.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear
|
|
2340
|
+
action: z9.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),
|
|
2051
2341
|
key: z9.string().optional().describe("The key (required for set and remove)."),
|
|
2052
2342
|
value: z9.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2053
2343
|
deviceId: deviceIdInput
|
|
@@ -2058,20 +2348,20 @@ ${lines.join("\n")}`);
|
|
|
2058
2348
|
const target = sync.requireProDevice(args.deviceId);
|
|
2059
2349
|
if (args.action === "clear") {
|
|
2060
2350
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2061
|
-
return text("
|
|
2351
|
+
return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.");
|
|
2062
2352
|
}
|
|
2063
2353
|
if (!args.key) {
|
|
2064
2354
|
return fail2(`'${args.action}' needs a key.`);
|
|
2065
2355
|
}
|
|
2066
2356
|
if (args.action === "remove") {
|
|
2067
2357
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2068
|
-
return text(`
|
|
2358
|
+
return text(`Requested removal of storage key "${args.key}".`);
|
|
2069
2359
|
}
|
|
2070
2360
|
if (args.value === void 0) {
|
|
2071
2361
|
return fail2("'set' needs a value (a string).");
|
|
2072
2362
|
}
|
|
2073
2363
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2074
|
-
return text(`
|
|
2364
|
+
return text(`Requested update of storage key "${args.key}".`);
|
|
2075
2365
|
} catch (e) {
|
|
2076
2366
|
return fail2(errMsg2(e));
|
|
2077
2367
|
}
|
|
@@ -2093,9 +2383,9 @@ function registerRoutesTools(server, sync) {
|
|
|
2093
2383
|
server.registerTool(
|
|
2094
2384
|
"get_routes",
|
|
2095
2385
|
{
|
|
2096
|
-
description:
|
|
2386
|
+
description: 'Read reported current navigation state and stack. includeSitemap:true includes available configured route paths; it does not establish complete application route coverage. Use get_events with sources:["route"] for retained navigation history.',
|
|
2097
2387
|
inputSchema: {
|
|
2098
|
-
includeSitemap: z10.boolean().optional().describe("Also list
|
|
2388
|
+
includeSitemap: z10.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),
|
|
2099
2389
|
deviceId: deviceIdInput
|
|
2100
2390
|
}
|
|
2101
2391
|
},
|
|
@@ -2176,7 +2466,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2176
2466
|
server.registerTool(
|
|
2177
2467
|
"get_console",
|
|
2178
2468
|
{
|
|
2179
|
-
description: "Read
|
|
2469
|
+
description: "Read retained captured console output as a compact tail of timestamps, levels and messages. Filter by minimum level or a case-insensitive message substring. Fatal JavaScript errors may be included when supported hooks captured them; post-crash availability is not guaranteed. Missing entries do not establish that no error occurred. Use get_snapshot for available full entry details. Requires configured console capture and Buoy Pro.",
|
|
2180
2470
|
inputSchema: {
|
|
2181
2471
|
level: z11.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2182
2472
|
limit: z11.number().optional().describe("Most-recent N entries. Default 50."),
|
|
@@ -2209,7 +2499,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2209
2499
|
${lines.join("\n")}
|
|
2210
2500
|
\`\`\`
|
|
2211
2501
|
|
|
2212
|
-
_Tail of recent logs. Use get_snapshot
|
|
2502
|
+
_Tail of recent logs. Use get_snapshot for available console entry details._`);
|
|
2213
2503
|
})
|
|
2214
2504
|
);
|
|
2215
2505
|
}
|
|
@@ -2358,11 +2648,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
2358
2648
|
server.registerTool(
|
|
2359
2649
|
"screenshot_component",
|
|
2360
2650
|
{
|
|
2361
|
-
description: "
|
|
2651
|
+
description: "Locate a supported React Native component and attempt a cropped iOS Simulator screenshot. Select by query or an exact nativeTag. Uses measured bounds or an available fiber-walk fallback and can attempt scrolling. Inspect the match and crop before treating it as visual evidence. Requires Buoy Pro, a compatible highlight-updates integration, and the MCP server on the same Mac as the booted iOS Simulator.",
|
|
2362
2652
|
inputSchema: {
|
|
2363
2653
|
query: z12.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
2364
2654
|
nativeTag: z12.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
2365
|
-
scrollIntoView: z12.boolean().optional().describe("
|
|
2655
|
+
scrollIntoView: z12.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),
|
|
2366
2656
|
deviceId: deviceIdInput
|
|
2367
2657
|
}
|
|
2368
2658
|
},
|
|
@@ -2534,12 +2824,12 @@ async function captureDiagnosis(sync, target) {
|
|
|
2534
2824
|
target
|
|
2535
2825
|
);
|
|
2536
2826
|
} catch {
|
|
2537
|
-
return "
|
|
2827
|
+
return "Capture status is unavailable. An empty result does not establish whether requests occurred.";
|
|
2538
2828
|
}
|
|
2539
2829
|
if (!status || status.capturing !== false) {
|
|
2540
|
-
return "
|
|
2830
|
+
return "No requests are available in this result. Check capture setup, supported transports and hook timing before concluding that no request occurred.";
|
|
2541
2831
|
}
|
|
2542
|
-
return "
|
|
2832
|
+
return "Capture is disabled. Open the Network panel on the device or in Buoy Desktop, trigger a supported request and read again. An empty result does not establish whether requests occurred. Capture depends on installed hooks, supported transports and recording timing.";
|
|
2543
2833
|
}
|
|
2544
2834
|
function skippedNote(skipped) {
|
|
2545
2835
|
return skipped > 0 ? `
|
|
@@ -2571,15 +2861,33 @@ function formatRow(index, e, markers) {
|
|
|
2571
2861
|
\u2514\u2500 ${bits.join(" \xB7 ")}`;
|
|
2572
2862
|
}
|
|
2573
2863
|
function registerNetworkTools(server, sync) {
|
|
2864
|
+
server.registerTool("network_conditions", {
|
|
2865
|
+
description: "Read or set the device's network conditions. Offline blocks new intercepted HTTP requests; slow adds 500 ms and verySlow adds 2000 ms before dispatch. Normal clears conditions. State resets on JS reload. Check app and server observations to verify the effect.",
|
|
2866
|
+
inputSchema: {
|
|
2867
|
+
action: z13.enum(["get", "set"]).describe("Read the effective profile or select one."),
|
|
2868
|
+
profile: z13.enum(["normal", "offline", "slow", "verySlow"]).optional().describe("Required for set."),
|
|
2869
|
+
deviceId: deviceIdInput
|
|
2870
|
+
}
|
|
2871
|
+
}, async (args) => {
|
|
2872
|
+
try {
|
|
2873
|
+
if (args.action === "set" && !args.profile) return fail2("Set requires a profile.");
|
|
2874
|
+
const target = sync.resolveDevice(args.deviceId);
|
|
2875
|
+
const result = await sync.callAction(NETWORK_TOOL_ID, args.action === "get" ? "getNetworkConditions" : "setNetworkConditions", args.action === "set" ? { profile: args.profile } : void 0, target);
|
|
2876
|
+
if (!result || typeof result !== "object" || !("profile" in result)) return fail2("The device did not report its effective network conditions.");
|
|
2877
|
+
return text(JSON.stringify(result));
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
return fail2(errMsg2(error));
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2574
2882
|
server.registerTool(
|
|
2575
2883
|
"get_network_requests",
|
|
2576
2884
|
{
|
|
2577
|
-
description: "List captured HTTP requests
|
|
2885
|
+
description: "List captured HTTP requests with IDs, available status/timing/size fields and pinned or saved markers. Capture depends on supported transports, installed hooks and recording timing; requests made before capture starts may be absent. Use IDs for network actions and flagged to inspect retained records. A flag does not establish who created it or the cause of a problem. Filter by status or URL pattern. includeBodies:true requests available truncated bodies, which can be missing or withheld. Requires the network integration and Buoy Pro.",
|
|
2578
2886
|
inputSchema: {
|
|
2579
2887
|
limit: z13.number().optional().describe("Most-recent N requests. Default 25."),
|
|
2580
2888
|
pattern: z13.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
2581
2889
|
status: z13.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
2582
|
-
flagged: z13.enum(["pinned", "saved", "any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These
|
|
2890
|
+
flagged: z13.enum(["pinned", "saved", "any"]).optional().describe("Read KEPT requests instead of the live list: 'pinned' (held at the top of the developer's list), 'saved' (their favorites), 'any' (either). These are separate from the live list; restart retention depends on successful persistence."),
|
|
2583
2891
|
includeBodies: z13.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
2584
2892
|
deviceId: deviceIdInput
|
|
2585
2893
|
}
|
|
@@ -2658,7 +2966,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2658
2966
|
server.registerTool(
|
|
2659
2967
|
"network_action",
|
|
2660
2968
|
{
|
|
2661
|
-
description: "
|
|
2969
|
+
description: "Pin or save captured requests as retained records, or remove records and clear lists. Bodies may be truncated or omitted by storage budgets. Records are kept separately from the live list; availability after restart depends on successful device persistence. Get request IDs from get_network_requests. remove uses a retained-record key from flagged results. An action acknowledgement does not confirm a durable storage write.",
|
|
2662
2970
|
inputSchema: {
|
|
2663
2971
|
action: z13.enum(["pin", "unpin", "save", "unsave", "remove", "clearPinned", "clearSaved", "clearRequests"]).describe("pin/unpin/save/unsave act on one request by `id`. remove drops one kept record by `key`. clearPinned/clearSaved drop all of that kind. clearRequests wipes the live capture list (kept requests survive)."),
|
|
2664
2972
|
id: z13.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
@@ -2714,7 +3022,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2714
3022
|
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
2715
3023
|
case "clearRequests":
|
|
2716
3024
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
2717
|
-
return text(`
|
|
3025
|
+
return text(`Clear request sent to ${target}. Re-list to confirm the live capture list; this action does not clear retained records.`);
|
|
2718
3026
|
}
|
|
2719
3027
|
} catch (e) {
|
|
2720
3028
|
return fail2(errMsg2(e));
|
|
@@ -2724,7 +3032,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2724
3032
|
server.registerTool(
|
|
2725
3033
|
"network_override",
|
|
2726
3034
|
{
|
|
2727
|
-
description: "
|
|
3035
|
+
description: "Configure response, failure or delay rules for supported requests intercepted by the app integration. Coverage depends on transport, hooks and rule-loading timing; early startup requests may bypass a rule. Inspect existing rules before editing, trigger the intended request and check observed results. Simulated offline behavior does not disconnect the device. Remove rules created for this task afterward or restore prior settings; preserve unrelated rules. Requires Buoy Pro and network override support.",
|
|
2728
3036
|
inputSchema: {
|
|
2729
3037
|
action: z13.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
2730
3038
|
"add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
|
|
@@ -2762,7 +3070,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2762
3070
|
),
|
|
2763
3071
|
name: z13.string().optional().describe("Label shown in the developer's UI."),
|
|
2764
3072
|
enabled: z13.boolean().optional().describe(
|
|
2765
|
-
"For `add`:
|
|
3073
|
+
"For `add`: whether the rule is enabled. Default true; pass false to stage it switched off. Useful for staging a rule you'll arm later with enable({id})."
|
|
2766
3074
|
),
|
|
2767
3075
|
id: z13.string().optional().describe(
|
|
2768
3076
|
"Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
|
|
@@ -2833,7 +3141,7 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2833
3141
|
}
|
|
2834
3142
|
case "clear":
|
|
2835
3143
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2836
|
-
return text(`Removed all override rules on ${target}.
|
|
3144
|
+
return text(`Removed all override rules on ${target}. Other interception or simulation settings may still apply.`);
|
|
2837
3145
|
case "enable":
|
|
2838
3146
|
case "disable": {
|
|
2839
3147
|
const enabled = args.action === "enable";
|
|
@@ -2845,12 +3153,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2845
3153
|
target
|
|
2846
3154
|
);
|
|
2847
3155
|
return text(
|
|
2848
|
-
`Rule \`${args.id}\`
|
|
3156
|
+
`Rule \`${args.id}\` was requested ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2849
3157
|
);
|
|
2850
3158
|
}
|
|
2851
3159
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2852
3160
|
return text(
|
|
2853
|
-
enabled ? `Overrides
|
|
3161
|
+
enabled ? `Overrides enabled in settings for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2854
3162
|
);
|
|
2855
3163
|
}
|
|
2856
3164
|
case "list": {
|
|
@@ -2880,7 +3188,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2880
3188
|
return text(
|
|
2881
3189
|
`No override rules on ${target}.
|
|
2882
3190
|
|
|
2883
|
-
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
3191
|
+
Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`
|
|
2884
3192
|
);
|
|
2885
3193
|
}
|
|
2886
3194
|
const rows = rules.map((rule, i) => {
|
|
@@ -2998,7 +3306,7 @@ function registerSentryTools(server, sync) {
|
|
|
2998
3306
|
server.registerTool(
|
|
2999
3307
|
"get_sentry_envelopes",
|
|
3000
3308
|
{
|
|
3001
|
-
description: "
|
|
3309
|
+
description: "Inspect retained captured Sentry envelopes and available item types, sizes, spans and payload details. Coverage depends on supported hooks. Local capture does not prove successful delivery, ingestion or billing impact. Filter by item type to investigate the available evidence.",
|
|
3002
3310
|
inputSchema: {
|
|
3003
3311
|
type: z14.string().optional().describe(
|
|
3004
3312
|
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
@@ -3048,7 +3356,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
3048
3356
|
server.registerTool(
|
|
3049
3357
|
"get_sentry_cost",
|
|
3050
3358
|
{
|
|
3051
|
-
description: "
|
|
3359
|
+
description: "Inspect captured session usage and the tool's configured monthly extrapolation and quota assumptions. These estimates do not establish Sentry ingestion, current plan limits or an actual bill. Check the account's current terms and usage before making cost decisions.",
|
|
3052
3360
|
inputSchema: {
|
|
3053
3361
|
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3054
3362
|
}
|
|
@@ -3064,7 +3372,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
3064
3372
|
const usage = p.usage;
|
|
3065
3373
|
if (!usage?.session) {
|
|
3066
3374
|
return text2(
|
|
3067
|
-
`${staleHeader2}No usage
|
|
3375
|
+
`${staleHeader2}No usage is available in this capture.${statusNote(p.status)}`
|
|
3068
3376
|
);
|
|
3069
3377
|
}
|
|
3070
3378
|
const session = usage.session;
|
|
@@ -3077,7 +3385,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
3077
3385
|
const month = projected[key];
|
|
3078
3386
|
const quota = FREE_QUOTA[key];
|
|
3079
3387
|
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
3080
|
-
const pctNote = pct === null ? " (
|
|
3388
|
+
const pctNote = pct === null ? " (no configured reference quota)" : ` \u2014 ${pct}% of the configured reference quota of ${units(quota, format)}`;
|
|
3081
3389
|
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
3082
3390
|
});
|
|
3083
3391
|
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
@@ -3087,7 +3395,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
3087
3395
|
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
3088
3396
|
const caveat = projected ? "\n\n_The monthly figure extrapolates one session on one device \u2014 it catches a config that is orders of magnitude off, it is not a forecast._" : "\n\n_Session too short to project a month from._";
|
|
3089
3397
|
return text2(
|
|
3090
|
-
`${staleHeader2}**Sentry
|
|
3398
|
+
`${staleHeader2}**Sentry usage estimate \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
3091
3399
|
|
|
3092
3400
|
${rows.join("\n")}${contributors}${caveat}`
|
|
3093
3401
|
);
|
|
@@ -3117,9 +3425,9 @@ ${rows.join("\n")}${contributors}${caveat}`
|
|
|
3117
3425
|
const recent = p.drops?.recent ?? [];
|
|
3118
3426
|
if (summary.length === 0) {
|
|
3119
3427
|
return text2(
|
|
3120
|
-
`${staleHeader2}
|
|
3428
|
+
`${staleHeader2}No drops are reported in the available capture.${statusNote(p.status)}
|
|
3121
3429
|
|
|
3122
|
-
_If an event is
|
|
3430
|
+
_If an event is missing from sentry.io, check capture coverage, transport errors, the DSN/project, inbound filters and quota. Local capture does not confirm delivery._`
|
|
3123
3431
|
);
|
|
3124
3432
|
}
|
|
3125
3433
|
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
@@ -3198,7 +3506,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
3198
3506
|
server.registerTool(
|
|
3199
3507
|
"get_time_machine",
|
|
3200
3508
|
{
|
|
3201
|
-
description: "List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore
|
|
3509
|
+
description: "List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Each snapshot also records the route it was captured on (shown as `route`), which a restore can navigate back to. Use time_machine_action to capture a snapshot before driving a flow, then restore supported sources to repeat the flow. Uncaptured and backend state can still differ.",
|
|
3202
3510
|
inputSchema: {
|
|
3203
3511
|
deviceId: deviceIdInput
|
|
3204
3512
|
}
|
|
@@ -3221,7 +3529,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
3221
3529
|
}
|
|
3222
3530
|
const snapLines = snapshots.map((s) => {
|
|
3223
3531
|
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
3224
|
-
const desc = s.baseline ? "baseline
|
|
3532
|
+
const desc = s.baseline ? "baseline: clear supported providers and request reload" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
3225
3533
|
const route = s.route ? ` \xB7 route ${s.route.href}` : "";
|
|
3226
3534
|
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${route}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
3227
3535
|
});
|
|
@@ -3242,7 +3550,7 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3242
3550
|
server.registerTool(
|
|
3243
3551
|
"time_machine_action",
|
|
3244
3552
|
{
|
|
3245
|
-
description: "Manage
|
|
3553
|
+
description: "Manage snapshots of supported registered state sources: capture, preview, restore, capture_baseline, delete or rename. Preview affected items before restoring. Live mode replaces supported state in place; reload mode restores supported persisted storage and requests a JS reload. Baselines clear providers that support clearing and request a reload; they do not reset backend state or reproduce a complete reinstall. restoreRoute:true requests the captured route when available. Inspect per-source applied/skipped results and reload outcomes.",
|
|
3246
3554
|
inputSchema: {
|
|
3247
3555
|
action: z15.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3248
3556
|
id: z15.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
@@ -3389,7 +3697,7 @@ function registerScenariosTools(server, sync) {
|
|
|
3389
3697
|
server.registerTool(
|
|
3390
3698
|
"get_scenarios",
|
|
3391
3699
|
{
|
|
3392
|
-
description: "List
|
|
3700
|
+
description: "List saved scenarios, configured steps, available durability labels and the reported active scenario. Inspect these settings when app behavior may be simulated. A scenario covers its supported steps, not every source of app or backend state.",
|
|
3393
3701
|
inputSchema: {
|
|
3394
3702
|
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3395
3703
|
}
|
|
@@ -3430,7 +3738,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3430
3738
|
server.registerTool(
|
|
3431
3739
|
"scenario_action",
|
|
3432
3740
|
{
|
|
3433
|
-
description:
|
|
3741
|
+
description: "Preview, run, deactivate, save, organize or delete a scenario. Preview unfamiliar scenarios before running; inspect applied steps, failures and unrestored effects afterward. Storage changes need suitable undo steps. Saved scenarios are drafts requiring acceptance on the device before running. Reuse existing folders when appropriate. A completed scenario does not guarantee a deterministic app or backend state; overrides and cache edits have different coverage and lifetimes.",
|
|
3434
3742
|
inputSchema: {
|
|
3435
3743
|
action: z16.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3436
3744
|
folder: z16.string().optional().describe(
|
|
@@ -3520,7 +3828,7 @@ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
|
3520
3828
|
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3521
3829
|
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3522
3830
|
lines.push(
|
|
3523
|
-
r.ok ? "
|
|
3831
|
+
r.ok ? "Pre-flight checks passed. Review the intended effects before running." : `\u274C **Cannot run:**
|
|
3524
3832
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3525
3833
|
);
|
|
3526
3834
|
return text3(lines.join("\n\n"));
|
|
@@ -3555,9 +3863,9 @@ The device is now in a partial state. Deactivate to reverse what landed, or fix
|
|
|
3555
3863
|
[
|
|
3556
3864
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3557
3865
|
steps,
|
|
3558
|
-
`**
|
|
3866
|
+
`**Reported effects:**
|
|
3559
3867
|
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3560
|
-
"The device shows a SIMULATED banner while this is active. Deactivate when done,
|
|
3868
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, and account for these effects when interpreting observations."
|
|
3561
3869
|
].join("\n\n")
|
|
3562
3870
|
);
|
|
3563
3871
|
}
|
|
@@ -3622,7 +3930,7 @@ import { z as z17 } from "zod";
|
|
|
3622
3930
|
// src/capture/camera.ts
|
|
3623
3931
|
import { execFile as execFile2 } from "child_process";
|
|
3624
3932
|
import { promisify as promisify2 } from "util";
|
|
3625
|
-
import { existsSync, readFileSync } from "fs";
|
|
3933
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
3626
3934
|
import os3 from "os";
|
|
3627
3935
|
import path3 from "path";
|
|
3628
3936
|
var execFileAsync2 = promisify2(execFile2);
|
|
@@ -3754,20 +4062,20 @@ function readEntitlement() {
|
|
|
3754
4062
|
for (const p of entitlementPaths()) {
|
|
3755
4063
|
try {
|
|
3756
4064
|
if (!existsSync(p)) continue;
|
|
3757
|
-
const parsed = JSON.parse(
|
|
4065
|
+
const parsed = JSON.parse(readFileSync2(p, "utf8"));
|
|
3758
4066
|
return {
|
|
3759
|
-
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "
|
|
3760
|
-
isPro: parsed.isPro === true
|
|
4067
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "locked",
|
|
4068
|
+
isPro: parsed.isPro === true && parsed.hasDesktopAccess === true && typeof parsed.validUntil === "number" && Number.isFinite(parsed.validUntil) && Date.now() < parsed.validUntil && typeof parsed.proValidUntil === "number" && Number.isFinite(parsed.proValidUntil) && Date.now() < parsed.proValidUntil
|
|
3761
4069
|
};
|
|
3762
4070
|
} catch {
|
|
3763
4071
|
}
|
|
3764
4072
|
}
|
|
3765
|
-
return { tier: "
|
|
4073
|
+
return { tier: "locked", isPro: false };
|
|
3766
4074
|
}
|
|
3767
4075
|
function proRequired(tool) {
|
|
3768
4076
|
return `\`${tool}\` is a Buoy Pro feature.
|
|
3769
4077
|
|
|
3770
|
-
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel
|
|
4078
|
+
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel with a Free account. Pro adds the screen region, the non-QR symbologies (PDF417, Aztec, DataMatrix, Code 128), and driving any of it from an agent.
|
|
3771
4079
|
|
|
3772
4080
|
_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._`;
|
|
3773
4081
|
}
|
|
@@ -4014,7 +4322,7 @@ function registerInteractTools(server, sync) {
|
|
|
4014
4322
|
server.registerTool(
|
|
4015
4323
|
"describe_screen",
|
|
4016
4324
|
{
|
|
4017
|
-
description: "List
|
|
4325
|
+
description: "List elements exposed by the app React-fiber inspection integration, with available names, text, roles, testIDs, positions and native tags. This is not a complete native accessibility tree or a visual screenshot. Use the returned controls to select a target for tap_element; refresh after navigation or state changes. Empty results can reflect unavailable inspection. Requires Buoy Pro and the highlight-updates interaction integration.",
|
|
4018
4326
|
inputSchema: {
|
|
4019
4327
|
interactiveOnly: z18.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4020
4328
|
includeBuoy: z18.boolean().optional().describe(
|
|
@@ -4052,7 +4360,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4052
4360
|
server.registerTool(
|
|
4053
4361
|
"tap_element",
|
|
4054
4362
|
{
|
|
4055
|
-
description: "
|
|
4363
|
+
description: "Invoke a supported element handler in JavaScript: onPress/onLongPress, onValueChange or onChangeText. This does not reproduce native hit testing, keyboard entry or a physical gesture. Select an exposed element by nativeTag, testID or fuzzy query; inspect the matched target and resulting behavior. value sets a supported toggle or slider, text invokes a text-input handler, and longPress selects onLongPress. Can attempt to scroll an off-screen target into view. Use describe_screen for current controls and run_flow for sequences. Requires Buoy Pro and the app interaction integration.",
|
|
4056
4364
|
inputSchema: {
|
|
4057
4365
|
nativeTag: z18.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4058
4366
|
testID: z18.string().optional().describe("Exact testID of the element."),
|
|
@@ -4060,7 +4368,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4060
4368
|
value: z18.union([z18.number(), z18.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
4061
4369
|
text: z18.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4062
4370
|
longPress: z18.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
4063
|
-
scrollIntoView: z18.boolean().optional().describe("
|
|
4371
|
+
scrollIntoView: z18.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),
|
|
4064
4372
|
includeBuoy: z18.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),
|
|
4065
4373
|
deviceId: deviceIdInput
|
|
4066
4374
|
}
|
|
@@ -4184,11 +4492,11 @@ function registerJsThreadTools(server, sync) {
|
|
|
4184
4492
|
server.registerTool(
|
|
4185
4493
|
"get_js_thread_top",
|
|
4186
4494
|
{
|
|
4187
|
-
description:
|
|
4495
|
+
description: "Sample instrumented JavaScript task origins and return available timing, busy percentage and freeze attribution. Coverage depends on installed hooks; unattributed work can remain. Requires @buoy-gg/js-top and Buoy Pro. The call waits for the sample: arrange the interaction during that window using an available concurrent driver or a person, rather than starting it after this call returns. clearFirst resets tracked statistics before sampling. Use get_js_thread_origin_detail for a returned row key and measure_renders for component render diagnostics.",
|
|
4188
4496
|
inputSchema: {
|
|
4189
4497
|
deviceId: deviceIdInput,
|
|
4190
|
-
durationMs: z19.number().optional().describe("How long to sample before reading the table. Default 3000
|
|
4191
|
-
clearFirst: z19.boolean().optional().describe("Reset
|
|
4498
|
+
durationMs: z19.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),
|
|
4499
|
+
clearFirst: z19.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")
|
|
4192
4500
|
}
|
|
4193
4501
|
},
|
|
4194
4502
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -4372,7 +4680,7 @@ function registerImagesTools(server, sync) {
|
|
|
4372
4680
|
server.registerTool(
|
|
4373
4681
|
"get_images",
|
|
4374
4682
|
{
|
|
4375
|
-
description: "
|
|
4683
|
+
description: "Inspect image loads reported through the Buoy image integration, including available cache origin, load timing, size estimates, errors and simulation overrides. Coverage depends on instrumented components and platform support. Requires @buoy-gg/images on the device.",
|
|
4376
4684
|
inputSchema: {
|
|
4377
4685
|
deviceId: deviceIdInput,
|
|
4378
4686
|
limit: z20.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
@@ -4429,7 +4737,7 @@ ${JSON.stringify(slim)}
|
|
|
4429
4737
|
server.registerTool(
|
|
4430
4738
|
"image_action",
|
|
4431
4739
|
{
|
|
4432
|
-
description: "
|
|
4740
|
+
description: "Request an action for a registered image ID: hard_reload, retry, evict_disk, flash, prove_savings or a supported simulation override. swap_url requires url; restore removes the override. Support depends on the component and platform. prove_savings attempts a WebP conversion at displayed size and reports available byte comparisons and a device-local URI; inspect visual quality and originalBytesSource before using the result. Other actions return an acknowledgement and, when available, a later record snapshot; that snapshot may precede load completion. A device-local URI is not necessarily accessible from the editor host.",
|
|
4433
4741
|
inputSchema: {
|
|
4434
4742
|
deviceId: deviceIdInput,
|
|
4435
4743
|
id: z20.number().describe("Record id from get_images."),
|
|
@@ -4499,12 +4807,12 @@ ${JSON.stringify(slim)}
|
|
|
4499
4807
|
server.registerTool(
|
|
4500
4808
|
"set_image_simulation",
|
|
4501
4809
|
{
|
|
4502
|
-
description: "
|
|
4810
|
+
description: "Configure supported instrumented image loads: offline simulates network-load failures while bundled assets can still load; cold requests cache bypass; normal resets the network mode. Reload or remount existing images as needed. blankImages removes sources from integrated images; massAction targets registered mounted images. Can also clear the registry or supported Expo caches. These modes do not change device connectivity.",
|
|
4503
4811
|
inputSchema: {
|
|
4504
4812
|
deviceId: deviceIdInput,
|
|
4505
4813
|
network: z20.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4506
|
-
blankImages: z20.boolean().optional().describe("Render
|
|
4507
|
-
massAction: z20.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to
|
|
4814
|
+
blankImages: z20.boolean().optional().describe("Render integrated images without a source."),
|
|
4815
|
+
massAction: z20.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to registered mounted images: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
|
|
4508
4816
|
clearRegistry: z20.boolean().optional().describe("Clear the captured image log."),
|
|
4509
4817
|
clearExpoCaches: z20.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4510
4818
|
}
|
|
@@ -4643,7 +4951,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4643
4951
|
server.registerTool(
|
|
4644
4952
|
"get_assets",
|
|
4645
4953
|
{
|
|
4646
|
-
description: "
|
|
4954
|
+
description: "Inspect assets reported by the configured manifest, runtime registry and available Metro bundle graph. Includes available dimensions, scale coverage, byte sizes, observed load history, audit findings and saved-baseline differences. Size measurement and coverage depend on setup; not observed loading does not mean unused. Use get_images for instrumented runtime image loads. Requires @buoy-gg/assets on the device.",
|
|
4647
4955
|
inputSchema: {
|
|
4648
4956
|
deviceId: deviceIdInput,
|
|
4649
4957
|
limit: z21.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
@@ -4776,12 +5084,12 @@ function messageSocketUrl(origin) {
|
|
|
4776
5084
|
return `${origin.replace(/^http/, "ws").replace(/\/+$/, "")}/message`;
|
|
4777
5085
|
}
|
|
4778
5086
|
function reloadViaDevServer(origin) {
|
|
4779
|
-
return new Promise((
|
|
5087
|
+
return new Promise((resolve2) => {
|
|
4780
5088
|
let settled = false;
|
|
4781
5089
|
const done = (result) => {
|
|
4782
5090
|
if (settled) return;
|
|
4783
5091
|
settled = true;
|
|
4784
|
-
|
|
5092
|
+
resolve2(result);
|
|
4785
5093
|
};
|
|
4786
5094
|
let socket;
|
|
4787
5095
|
try {
|
|
@@ -4866,7 +5174,7 @@ function registerReloadTools(server, sync) {
|
|
|
4866
5174
|
server.registerTool(
|
|
4867
5175
|
"reload_app",
|
|
4868
5176
|
{
|
|
4869
|
-
description: "
|
|
5177
|
+
description: "Request a JS reload for a supported app using DevSettings in development or installed expo-updates where available. For an unresponsive app, can try its known dev server when reachable. Recovery is not guaranteed. Reload can discard in-memory state and evidence; collect needed observations first when possible. By default waits for reconnection up to timeoutMs. Requires configured @buoy-gg/core support and Buoy Pro.",
|
|
4870
5178
|
inputSchema: {
|
|
4871
5179
|
strategy: z22.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4872
5180
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
@@ -4990,7 +5298,7 @@ function registerTriageTools(server, sync) {
|
|
|
4990
5298
|
server.registerTool(
|
|
4991
5299
|
"get_triage",
|
|
4992
5300
|
{
|
|
4993
|
-
description: "
|
|
5301
|
+
description: "Summarize available runtime observations for a connected app: captured console errors, recent network requests, configured storage and screen information. Use the returned sections and errors to choose follow-up tools. Missing data or an unresponsive app does not prove a crash; crash output may be unavailable. Requires Buoy Pro.",
|
|
4994
5302
|
inputSchema: {
|
|
4995
5303
|
deviceId: z23.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
4996
5304
|
}
|
|
@@ -5157,10 +5465,10 @@ function registerFlowTools(server, sync) {
|
|
|
5157
5465
|
server.registerTool(
|
|
5158
5466
|
"run_flow",
|
|
5159
5467
|
{
|
|
5160
|
-
description: "Run
|
|
5468
|
+
description: "Run supported UI actions and element checks in sequence, with per-step results. Stops on the first failed step unless continueOnFailure is true. Depending on include, attempts to return the final element tree and newly observed captured network/console entries. Background activity can appear in that output; it does not establish causation. Actions use exposed app handlers, not physical gestures. Review failures and check the expected behavior. Requires Buoy Pro and the app interaction integration.",
|
|
5161
5469
|
inputSchema: {
|
|
5162
5470
|
steps: z24.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5163
|
-
include: z24.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final
|
|
5471
|
+
include: z24.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = available final element tree + newly observed captured network/console entries; 'screen' = available final element tree only; 'none' = step results only."),
|
|
5164
5472
|
continueOnFailure: z24.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5165
5473
|
deviceId: deviceIdInput
|
|
5166
5474
|
}
|
|
@@ -5482,7 +5790,7 @@ function formatCapture(c, top) {
|
|
|
5482
5790
|
}
|
|
5483
5791
|
lines.push("");
|
|
5484
5792
|
lines.push(
|
|
5485
|
-
"`wasted`
|
|
5793
|
+
"`wasted` classifies parent cascades and identity-only prop changes. Investigate whether these renders are needed before changing them. Rows are sorted by **self ms**. Compare timing and render counts for the measured workload, and check behavior before deciding whether memoization helps."
|
|
5486
5794
|
);
|
|
5487
5795
|
return lines.join("\n");
|
|
5488
5796
|
}
|
|
@@ -5518,14 +5826,14 @@ function registerRenderTools(server, sync) {
|
|
|
5518
5826
|
server.registerTool(
|
|
5519
5827
|
"measure_renders",
|
|
5520
5828
|
{
|
|
5521
|
-
description: "
|
|
5829
|
+
description: "Measure captured component renders during supplied interaction steps or an idle window. Returns available timings, render counts, reported reasons and changed props. The wasted count is a diagnostic classification, not proof a render can be removed. Check interaction warnings before comparing runs. Use the same steps and comparable conditions with compareToPrevious:true after editing; the comparison alone does not prove correctness or overall performance. Requires Buoy Pro and a highlight-updates integration exposing beginMeasurement. Use run_benchmark_batch to compare route-based variants.",
|
|
5522
5830
|
inputSchema: {
|
|
5523
5831
|
steps: z25.array(stepSchema2).max(20).optional().describe(
|
|
5524
5832
|
"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."
|
|
5525
5833
|
),
|
|
5526
5834
|
idleMs: z25.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5527
5835
|
label: z25.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5528
|
-
compareToPrevious: z25.boolean().optional().describe("
|
|
5836
|
+
compareToPrevious: z25.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),
|
|
5529
5837
|
top: z25.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5530
5838
|
deviceId: deviceIdInput
|
|
5531
5839
|
}
|
|
@@ -5615,7 +5923,7 @@ function registerRenderTools(server, sync) {
|
|
|
5615
5923
|
"_Nothing re-rendered in this window. If you expected renders, check the app is on the screen you meant and that the steps actually hit something._"
|
|
5616
5924
|
);
|
|
5617
5925
|
} else if (!args.compareToPrevious) {
|
|
5618
|
-
parts.push("", "
|
|
5926
|
+
parts.push("", "_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._");
|
|
5619
5927
|
}
|
|
5620
5928
|
return text8(parts.join("\n"));
|
|
5621
5929
|
} catch (e) {
|
|
@@ -5728,129 +6036,89 @@ var EditWatcher = class {
|
|
|
5728
6036
|
|
|
5729
6037
|
---
|
|
5730
6038
|
\u26A0\uFE0F **${files.length} source file${files.length === 1 ? "" : "s"} changed since anything was last checked on the device** (${shown}${more}).
|
|
5731
|
-
|
|
6039
|
+
For a behavior change, repeat the affected interaction and check the expected result. Use \`measure_renders\` with the same steps and \`compareToPrevious: true\` for render comparisons, or \`run_flow\` for supported behavior checks. Report what was checked and any remaining limits. This reminder tracks edits and tool calls; clearing it does not prove the change is correct.`;
|
|
5732
6040
|
}
|
|
5733
6041
|
};
|
|
5734
6042
|
var editWatcher = new EditWatcher();
|
|
5735
6043
|
|
|
5736
6044
|
// src/server.ts
|
|
5737
|
-
var SERVER_INSTRUCTIONS = `Buoy MCP
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
-
|
|
5750
|
-
|
|
5751
|
-
-
|
|
5752
|
-
|
|
5753
|
-
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
and
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
\`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
|
|
5815
|
-
freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
|
|
5816
|
-
Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
|
|
5817
|
-
This is about TASKS on the thread \u2014 for React components re-rendering too often, it's
|
|
5818
|
-
\`measure_renders\`, not this.
|
|
5819
|
-
|
|
5820
|
-
Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
|
|
5821
|
-
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
5822
|
-
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
5823
|
-
|
|
5824
|
-
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
5825
|
-
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
5826
|
-
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
5827
|
-
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
5828
|
-
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
5829
|
-
|
|
5830
|
-
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
5831
|
-
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
5832
|
-
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
5833
|
-
re-read anything you cached.
|
|
5834
|
-
|
|
5835
|
-
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
5836
|
-
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
5837
|
-
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
5838
|
-
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
5839
|
-
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
5840
|
-
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
5841
|
-
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
5842
|
-
|
|
5843
|
-
For everything else:
|
|
5844
|
-
- \`list_tool_actions\` to see what a tool can do,
|
|
5845
|
-
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
5846
|
-
- \`call_action\` to invoke any tool action generically.
|
|
5847
|
-
|
|
5848
|
-
Higher-level tools (e.g. perf-monitor's \`run_benchmark_batch\`) wrap these for common workflows.
|
|
5849
|
-
If no device is connected, open the app with Buoy devtools running and try again.
|
|
5850
|
-
|
|
5851
|
-
The Buoy MCP is a Pro feature: \`list_devices\` is always available, but data/action tools
|
|
5852
|
-
require the connected app to have an active Buoy Pro license. If they error with "Buoy Pro
|
|
5853
|
-
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
6045
|
+
var SERVER_INSTRUCTIONS = `Buoy MCP connects your editor to supported Buoy tools through the configured broker.
|
|
6046
|
+
|
|
6047
|
+
Start with account and connection setup. Use \`list_devices\` to select an app and inspect
|
|
6048
|
+
its available tools. App data and action tools require Buoy Pro. If access fails, explain
|
|
6049
|
+
the reported requirement and setup steps. Camera tools use a separate local setup and
|
|
6050
|
+
account gate; \`camera_diagnose\` can report setup problems without an app connection.
|
|
6051
|
+
|
|
6052
|
+
For a runtime bug, \`get_triage\` summarizes available console errors, network activity,
|
|
6053
|
+
storage and screen information. Follow up on the relevant section and read source when
|
|
6054
|
+
needed. Missing data can reflect missing integration, retention limits or a disconnected
|
|
6055
|
+
app. An unresponsive app is not proof of a crash, and post-crash evidence may be unavailable.
|
|
6056
|
+
|
|
6057
|
+
Choose follow-up tools by the symptom:
|
|
6058
|
+
- Persisted values: \`get_storage\` with \`includeValues:true\` for configured storage sources.
|
|
6059
|
+
- Requests or API responses: \`get_network_requests\` with \`includeBodies:true\` for captured
|
|
6060
|
+
bodies. Capture depends on supported transports, installed hooks and recording timing.
|
|
6061
|
+
- Errors: \`get_console\` for retained captured output. A stopped app cannot provide new data.
|
|
6062
|
+
- Cache or store state: \`get_react_query\`, \`get_zustand_state\`, \`get_redux_state\` or
|
|
6063
|
+
\`get_jotai_state\` for the integrations registered in the app.
|
|
6064
|
+
|
|
6065
|
+
Use \`get_events\` with \`sources\` to read retained activity from connected event sources.
|
|
6066
|
+
It returns compact summaries by default; request \`includeData:true\` when details are needed.
|
|
6067
|
+
For request IDs and network actions, use \`get_network_requests\`. Its \`flagged:'any'\` filter
|
|
6068
|
+
finds pinned or saved requests. A flag is a reason to inspect a request, not proof of its
|
|
6069
|
+
cause or who flagged it. Use \`network_action\` to retain relevant evidence within task scope.
|
|
6070
|
+
|
|
6071
|
+
For supported UI interactions, \`run_flow\` runs steps such as tap, type, set, reload,
|
|
6072
|
+
waitFor and expect. Review step results, failures and available captured output. Use
|
|
6073
|
+
\`describe_screen\` to discover exposed elements and \`tap_element\` for individual actions.
|
|
6074
|
+
A completed tool call alone does not establish that the user's expected behavior occurred.
|
|
6075
|
+
|
|
6076
|
+
For render-heavy interactions, use \`measure_renders\` with representative \`steps\`.
|
|
6077
|
+
Repeat the same steps after a change with \`compareToPrevious:true\`. Interpret available
|
|
6078
|
+
render counts, reasons and wasted-render diagnostics alongside behavior and performance;
|
|
6079
|
+
these diagnostics do not prove that a render was unnecessary. For instrumented JavaScript
|
|
6080
|
+
tasks, use \`get_js_thread_top\` and \`get_js_thread_origin_detail\`. Their coverage depends
|
|
6081
|
+
on the installed hooks. Keep measurement conditions comparable.
|
|
6082
|
+
|
|
6083
|
+
\`get_images\` inspects instrumented image loads and available cache, timing, size and error
|
|
6084
|
+
data. Use \`image_action\` for supported per-image actions and \`set_image_simulation\` for
|
|
6085
|
+
integrated image behavior. Simulation does not disconnect the device's network.
|
|
6086
|
+
\`get_assets\` inspects the configured manifest, runtime registry and available bundle graph.
|
|
6087
|
+
Reported load history and size estimates do not prove complete shipped-asset coverage or
|
|
6088
|
+
that an unobserved asset is unused. \`asset_action\` can refresh data and manage baselines.
|
|
6089
|
+
|
|
6090
|
+
\`reload_app\` requests a JS reload using supported app mechanisms and can wait for
|
|
6091
|
+
reconnection. It can try a dev-server fallback for an unresponsive app when that server
|
|
6092
|
+
is known and reachable. Recovery is not guaranteed. Reload can discard in-memory evidence;
|
|
6093
|
+
collect needed observations first when possible and read state again after reconnecting.
|
|
6094
|
+
|
|
6095
|
+
\`get_time_machine\` lists snapshots and provider capabilities. \`time_machine_action\`
|
|
6096
|
+
captures or restores supported registered state sources. Preview the affected state before
|
|
6097
|
+
restoring. Live restore changes supported state in place; reload mode restores supported
|
|
6098
|
+
persisted sources and requests a reload. A baseline clears providers that support clearing
|
|
6099
|
+
and requests a reload. This does not reset backend state or reproduce a complete reinstall.
|
|
6100
|
+
Check per-source applied/skipped outcomes and any reload or route-restoration failures.
|
|
6101
|
+
|
|
6102
|
+
For iOS Simulator camera input, inspect setup with \`camera_diagnose\`. Use \`camera_source\`
|
|
6103
|
+
for supported barcode, file or screen input, \`camera_devices\` for simulators/apps and
|
|
6104
|
+
\`camera_inputs\` for local sources. \`camera_launch\` attaches input when launching the app;
|
|
6105
|
+
\`camera_zero_setup\` configures attachment for another launcher. \`camera_status\` reports
|
|
6106
|
+
available source/decoder status, which does not establish the app's scanner result.
|
|
6107
|
+
Use \`camera_stop\` when finished. These tools operate locally without an in-app Buoy
|
|
6108
|
+
integration, but require their supported host, simulator and account setup.
|
|
6109
|
+
|
|
6110
|
+
\`screenshot_component\` locates a supported component by testID/name and attempts a crop.
|
|
6111
|
+
It requires the MCP server on the same Mac as the booted iOS Simulator and the component
|
|
6112
|
+
inspection setup. Inspect the result before using it as visual evidence.
|
|
6113
|
+
|
|
6114
|
+
For other supported operations, use \`list_tool_actions\`, \`get_snapshot\` or \`call_action\`.
|
|
6115
|
+
Snapshots describe available current state and can be large; request only needed data.
|
|
6116
|
+
|
|
6117
|
+
After a behavior change, repeat the affected interaction and check the expected result.
|
|
6118
|
+
Include restart or persistence checks when relevant to the change. Use appropriate static
|
|
6119
|
+
checks for documentation-only work. Report actual checks, observations and remaining limits.
|
|
6120
|
+
BUOY_VERIFY reminders track selected edits and tool calls; clearing a reminder is not a
|
|
6121
|
+
test result. Stay within the user's authorized task and preserve existing project work.`;
|
|
5854
6122
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
5855
6123
|
"measure_renders",
|
|
5856
6124
|
"scenario_action",
|
|
@@ -5946,21 +6214,21 @@ function runInit(opts = {}) {
|
|
|
5946
6214
|
installSkills(skillSrcDir, cwd, done);
|
|
5947
6215
|
appendDebuggingGuide(path5.join(cwd, "CLAUDE.md"), cwd, done);
|
|
5948
6216
|
appendDebuggingGuide(path5.join(cwd, "AGENTS.md"), cwd, done);
|
|
5949
|
-
log("Buoy MCP
|
|
6217
|
+
log("Buoy MCP configuration written:");
|
|
5950
6218
|
for (const line of done) log(" \u2022 " + line);
|
|
5951
6219
|
log("");
|
|
5952
6220
|
log("Next steps:");
|
|
5953
6221
|
log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
|
|
5954
|
-
log(" 2.
|
|
5955
|
-
log(
|
|
6222
|
+
log(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");
|
|
6223
|
+
log(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");
|
|
5956
6224
|
if (entry.command === "npx") {
|
|
5957
6225
|
log("");
|
|
5958
|
-
log(`
|
|
5959
|
-
log(`
|
|
6226
|
+
log(`The editor launches the server with \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
|
|
6227
|
+
log(`Registry access may be needed. Re-run \`npx -y ${PACKAGE_NAME}@latest init\` to refresh configuration.`);
|
|
5960
6228
|
} else if (!opts.cliPath) {
|
|
5961
6229
|
log("");
|
|
5962
6230
|
log(`This uses a pinned local install (no network on startup). To update it later,`);
|
|
5963
|
-
log(`
|
|
6231
|
+
log(`To update the local package, run: npx -y ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}@latest init --local`);
|
|
5964
6232
|
}
|
|
5965
6233
|
}
|
|
5966
6234
|
function resolveEntry(opts, log) {
|
|
@@ -6095,57 +6363,37 @@ function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
|
6095
6363
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
6096
6364
|
var DEBUGGING_GUIDE = `${GUIDE_HEADING}
|
|
6097
6365
|
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
- \`run_flow
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
- \`
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
- \`
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
Saying "fixed" is a claim about how the app behaves, so check it the same way
|
|
6131
|
-
you'd check anything else about the app \u2014 by looking at it. Re-run the exact
|
|
6132
|
-
interaction that was broken and report what the numbers did:
|
|
6133
|
-
|
|
6134
|
-
- \`measure_renders\` with the same \`steps\` and \`compareToPrevious: true\` for a
|
|
6135
|
-
performance change \u2014 it prints the before/after delta and flags a change that
|
|
6136
|
-
made things worse.
|
|
6137
|
-
- \`run_flow\` re-driving the broken screen for a behaviour change.
|
|
6138
|
-
|
|
6139
|
-
Two things this catches that reading cannot: a fix that is right but incomplete
|
|
6140
|
-
(the value was wrong in three places and you corrected one), and a fix that
|
|
6141
|
-
moved the cost instead of removing it \u2014 the usual failure mode for re-render
|
|
6142
|
-
work, where a page gets faster to type in and slower to tap.
|
|
6143
|
-
|
|
6144
|
-
If the app changed since anything was last checked, Buoy will say so in a tool
|
|
6145
|
-
result. Whether that's worth acting on is your call \u2014 a doc edit isn't worth a
|
|
6146
|
-
device check, a state-management change is. Set \`BUOY_VERIFY\` in the MCP config
|
|
6147
|
-
to \`always\` (repeat until checked) or \`never\` (silent) to change how often it
|
|
6148
|
-
mentions it.
|
|
6366
|
+
Use Buoy to inspect a connected app when runtime evidence helps the task.
|
|
6367
|
+
Complete account, tool integration and connection setup first. MCP data and
|
|
6368
|
+
action access requires Pro. Follow the user's scope and repository instructions.
|
|
6369
|
+
|
|
6370
|
+
- \`get_triage\`: summarize available device and tool observations, then choose
|
|
6371
|
+
a relevant tool. Missing data is not proof that no problem occurred.
|
|
6372
|
+
- \`run_flow\`: run supported tap, type, wait and assertion steps to reproduce
|
|
6373
|
+
a failure or check a change. Review the flow before running it.
|
|
6374
|
+
- \`get_events\`: inspect retained events from connected tools.
|
|
6375
|
+
- \`get_network_requests\`: inspect captured requests and responses. Capture
|
|
6376
|
+
depends on installed hooks, transport and when recording began.
|
|
6377
|
+
- \`network_conditions\`: take the app offline or add latency to check error
|
|
6378
|
+
and retry paths. Development builds only. Restore \`normal\` when finished.
|
|
6379
|
+
- \`get_console\`: inspect retained console and error output. A terminated or
|
|
6380
|
+
disconnected app may not provide new data.
|
|
6381
|
+
- \`get_storage\`: inspect registered storage backends and available keys.
|
|
6382
|
+
- \`reload_app\`: reload where supported. Confirm the device remains connected;
|
|
6383
|
+
a reload does not guarantee recovery from a crash.
|
|
6384
|
+
- \`get_snapshot\`: inspect the available state of a specific tool.
|
|
6385
|
+
|
|
6386
|
+
After a behavior change, repeat the failing interaction when the environment
|
|
6387
|
+
allows it. Use relevant tests and report what was actually checked. For a
|
|
6388
|
+
performance change, compare \`measure_renders\` runs with the same steps and
|
|
6389
|
+
\`compareToPrevious: true\`; inspect the measurements and their limitations.
|
|
6390
|
+
Use \`run_flow\` for supported UI checks. Do not claim a fix was verified when
|
|
6391
|
+
only source review or a build was performed.
|
|
6392
|
+
|
|
6393
|
+
\`BUOY_VERIFY\` controls verification reminders: \`auto\` uses edit-based
|
|
6394
|
+
reminders, \`always\` repeats until checked, and \`never\` disables them.
|
|
6395
|
+
A reminder is not a test result. Choose checks appropriate to the change;
|
|
6396
|
+
prose-only edits do not require running device actions.
|
|
6149
6397
|
`;
|
|
6150
6398
|
function appendDebuggingGuide(file, cwd, done) {
|
|
6151
6399
|
let existing = "";
|
|
@@ -6157,7 +6405,7 @@ function appendDebuggingGuide(file, cwd, done) {
|
|
|
6157
6405
|
return;
|
|
6158
6406
|
}
|
|
6159
6407
|
if (existing.includes(GUIDE_HEADING)) {
|
|
6160
|
-
done.push(`${rel(file, cwd)} already has the "${GUIDE_HEADING}" block
|
|
6408
|
+
done.push(`${rel(file, cwd)} already has the "${GUIDE_HEADING}" block; preserved existing text. Review it against the current bundled guidance before relying on older instructions.`);
|
|
6161
6409
|
return;
|
|
6162
6410
|
}
|
|
6163
6411
|
}
|