@buoy-gg/mcp 7.0.35 → 7.0.38
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-QL2DJGPP.mjs → chunk-PQOVQPCW.mjs} +552 -284
- package/dist/cli.js +604 -351
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.js +604 -351
- package/dist/index.mjs +1 -1
- package/package.json +4 -3
- package/skill/buoy-optimize/SKILL.md +42 -69
package/dist/index.js
CHANGED
|
@@ -61,9 +61,177 @@ __export(src_exports, {
|
|
|
61
61
|
});
|
|
62
62
|
module.exports = __toCommonJS(src_exports);
|
|
63
63
|
|
|
64
|
+
// src/sync/NodeAccount.ts
|
|
65
|
+
var import_node_fs = require("fs");
|
|
66
|
+
var import_node_path = require("path");
|
|
67
|
+
var import_node_os = require("os");
|
|
68
|
+
var import_node_crypto = require("crypto");
|
|
69
|
+
var import_headless = require("@buoy-gg/license/headless");
|
|
70
|
+
var KEY_NAMES = ["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY"];
|
|
71
|
+
function readProjectKey(projectDir, env = process.env) {
|
|
72
|
+
for (const name of KEY_NAMES) if (env[name] !== void 0) return env[name].trim().toUpperCase() || null;
|
|
73
|
+
for (const file of [".env.development.local", ".env.local", ".env"]) {
|
|
74
|
+
let contents;
|
|
75
|
+
try {
|
|
76
|
+
contents = (0, import_node_fs.readFileSync)((0, import_node_path.join)(projectDir, file), "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const values = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
82
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);
|
|
83
|
+
if (!match) continue;
|
|
84
|
+
const raw = match[2].trim();
|
|
85
|
+
const quoted = raw.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);
|
|
86
|
+
values.set(match[1], quoted ? quoted[2] : raw.split("#")[0].trim());
|
|
87
|
+
}
|
|
88
|
+
for (const name of KEY_NAMES) if (values.has(name)) return values.get(name).trim().toUpperCase() || null;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
var NodeAccount = class {
|
|
93
|
+
constructor(changed, opts = {}) {
|
|
94
|
+
this.changed = changed;
|
|
95
|
+
this.key = null;
|
|
96
|
+
this.cache = null;
|
|
97
|
+
this.timer = null;
|
|
98
|
+
this.generation = 0;
|
|
99
|
+
this.busy = false;
|
|
100
|
+
this.nextCheck = 0;
|
|
101
|
+
this.retry = 0;
|
|
102
|
+
this.lastAdmission = false;
|
|
103
|
+
this.hasAccess = () => (0, import_headless.resolveAccountAccess)({
|
|
104
|
+
initialized: true,
|
|
105
|
+
validating: false,
|
|
106
|
+
key: this.key,
|
|
107
|
+
tierSource: this.cache?.tier ? "key" : "none",
|
|
108
|
+
verifiedAt: this.cache?.cachedAt ?? null
|
|
109
|
+
}).status === "admitted";
|
|
110
|
+
this.projectDir = (0, import_node_path.resolve)(opts.projectDir ?? process.env.BUOY_PROJECT_DIR ?? process.cwd());
|
|
111
|
+
const project = (0, import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0, 20);
|
|
112
|
+
this.cacheFile = opts.cacheFile ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".buoy", `account-${project}.json`);
|
|
113
|
+
}
|
|
114
|
+
start() {
|
|
115
|
+
if (this.timer) return;
|
|
116
|
+
try {
|
|
117
|
+
const cached = JSON.parse((0, import_node_fs.readFileSync)(this.cacheFile, "utf8"));
|
|
118
|
+
if (cached.valid && (0, import_headless.verifyCacheSignature)(cached)) this.cache = cached;
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
void this.tick();
|
|
122
|
+
this.timer = setInterval(() => {
|
|
123
|
+
void this.tick();
|
|
124
|
+
}, 1e3);
|
|
125
|
+
this.timer.unref();
|
|
126
|
+
}
|
|
127
|
+
stop() {
|
|
128
|
+
++this.generation;
|
|
129
|
+
if (this.timer) clearInterval(this.timer);
|
|
130
|
+
this.timer = null;
|
|
131
|
+
this.key = null;
|
|
132
|
+
this.cache = null;
|
|
133
|
+
this.notify();
|
|
134
|
+
}
|
|
135
|
+
notify() {
|
|
136
|
+
const admitted = this.hasAccess();
|
|
137
|
+
if (admitted === this.lastAdmission) return;
|
|
138
|
+
this.lastAdmission = admitted;
|
|
139
|
+
this.changed();
|
|
140
|
+
}
|
|
141
|
+
async tick() {
|
|
142
|
+
const selected = readProjectKey(this.projectDir);
|
|
143
|
+
if (selected !== this.key) {
|
|
144
|
+
++this.generation;
|
|
145
|
+
this.key = selected;
|
|
146
|
+
if (this.cache?.licenseKey !== selected) this.cache = null;
|
|
147
|
+
this.nextCheck = 0;
|
|
148
|
+
this.retry = 0;
|
|
149
|
+
if (!selected) this.removeCache();
|
|
150
|
+
}
|
|
151
|
+
this.notify();
|
|
152
|
+
if (!selected || this.busy || Date.now() < this.nextCheck) return;
|
|
153
|
+
if (this.cache && this.hasAccess() && Date.now() - this.cache.lastValidatedAt < import_headless.LICENSE_LAUNCH_VALIDATION_TTL && this.nextCheck === 0) {
|
|
154
|
+
this.nextCheck = this.cache.lastValidatedAt + import_headless.LICENSE_LAUNCH_VALIDATION_TTL;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.busy = true;
|
|
158
|
+
const startedAt = this.generation;
|
|
159
|
+
try {
|
|
160
|
+
if ((0, import_headless.isBuoyToken)(selected)) {
|
|
161
|
+
const claims = (0, import_headless.verifyFreeToken)(selected);
|
|
162
|
+
if (!claims) {
|
|
163
|
+
this.cache = null;
|
|
164
|
+
this.removeCache();
|
|
165
|
+
} else {
|
|
166
|
+
const now2 = Date.now();
|
|
167
|
+
this.cache = (0, import_headless.signCache)({
|
|
168
|
+
licenseKey: selected,
|
|
169
|
+
licenseId: `token:${claims.accountHash}`,
|
|
170
|
+
valid: true,
|
|
171
|
+
tier: "free",
|
|
172
|
+
isPro: false,
|
|
173
|
+
expiresAt: null,
|
|
174
|
+
cachedAt: now2,
|
|
175
|
+
lastValidatedAt: now2
|
|
176
|
+
});
|
|
177
|
+
this.persistCache();
|
|
178
|
+
}
|
|
179
|
+
this.nextCheck = Date.now() + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const response = await (0, import_headless.validateLicense)(selected);
|
|
183
|
+
if (startedAt !== this.generation || readProjectKey(this.projectDir) !== selected) return;
|
|
184
|
+
const product = response.data?.relationships?.product?.data?.id;
|
|
185
|
+
const expired = response.meta.code === "EXPIRED" && ((0, import_headless.isRecognizedAccount)(response.data) || this.cache?.licenseKey === selected);
|
|
186
|
+
if (product && product !== import_headless.KEYGEN_CONFIG.productId || !expired && !(0, import_headless.isAcceptedValidation)(response)) {
|
|
187
|
+
this.cache = null;
|
|
188
|
+
this.removeCache();
|
|
189
|
+
this.nextCheck = Date.now() + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const tier = expired ? "free" : (0, import_headless.resolveTier)(response.data);
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
this.cache = (0, import_headless.signCache)({
|
|
195
|
+
licenseKey: selected,
|
|
196
|
+
licenseId: response.data?.id ?? "",
|
|
197
|
+
valid: true,
|
|
198
|
+
tier,
|
|
199
|
+
isPro: tier === "pro",
|
|
200
|
+
expiresAt: expired ? null : response.data.attributes.expiry,
|
|
201
|
+
cachedAt: now,
|
|
202
|
+
lastValidatedAt: now
|
|
203
|
+
});
|
|
204
|
+
this.persistCache();
|
|
205
|
+
this.retry = 0;
|
|
206
|
+
this.nextCheck = now + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (startedAt !== this.generation) return;
|
|
209
|
+
const delay6 = (0, import_headless.isTransientLicenseError)(error) ? import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++] : void 0;
|
|
210
|
+
this.nextCheck = Date.now() + (delay6 ?? import_headless.LICENSE_REVALIDATION_INTERVAL);
|
|
211
|
+
} finally {
|
|
212
|
+
this.busy = false;
|
|
213
|
+
this.notify();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
persistCache() {
|
|
217
|
+
try {
|
|
218
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(this.cacheFile), { recursive: true, mode: 448 });
|
|
219
|
+
(0, import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`, JSON.stringify(this.cache), { mode: 384 });
|
|
220
|
+
(0, import_node_fs.renameSync)(`${this.cacheFile}.tmp`, this.cacheFile);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
removeCache() {
|
|
225
|
+
try {
|
|
226
|
+
(0, import_node_fs.unlinkSync)(this.cacheFile);
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
64
232
|
// src/sync/SyncClient.ts
|
|
65
233
|
var import_node_http = __toESM(require("http"));
|
|
66
|
-
var
|
|
234
|
+
var import_node_crypto2 = require("crypto");
|
|
67
235
|
var import_socket = require("socket.io-client");
|
|
68
236
|
var import_sync_broker2 = require("@buoy-gg/sync-broker");
|
|
69
237
|
|
|
@@ -112,6 +280,9 @@ function diagnoseCrashed(opts) {
|
|
|
112
280
|
var SyncClient = class {
|
|
113
281
|
constructor(opts = {}) {
|
|
114
282
|
this.socket = null;
|
|
283
|
+
this.admitted = false;
|
|
284
|
+
this.lockedDevices = /* @__PURE__ */ new Set();
|
|
285
|
+
this.hostAccount = null;
|
|
115
286
|
this.broker = null;
|
|
116
287
|
/** Latest capabilities announce per deviceId. */
|
|
117
288
|
this.capabilities = /* @__PURE__ */ new Map();
|
|
@@ -160,7 +331,10 @@ var SyncClient = class {
|
|
|
160
331
|
);
|
|
161
332
|
}
|
|
162
333
|
this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);
|
|
334
|
+
this.hostAccount = new NodeAccount(() => this.broker?.refreshAccess());
|
|
335
|
+
this.hostAccount.start();
|
|
163
336
|
this.broker = (0, import_sync_broker2.createBroker)({
|
|
337
|
+
canUseDesktop: this.hostAccount.hasAccess,
|
|
164
338
|
port: this.port,
|
|
165
339
|
onError: (e) => this.log(`broker error: ${e.code ?? e.message}`)
|
|
166
340
|
});
|
|
@@ -173,7 +347,7 @@ var SyncClient = class {
|
|
|
173
347
|
return mode;
|
|
174
348
|
}
|
|
175
349
|
connectSocket() {
|
|
176
|
-
return new Promise((
|
|
350
|
+
return new Promise((resolve2, reject) => {
|
|
177
351
|
const socket = (0, import_socket.io)(this.url, {
|
|
178
352
|
transports: ["websocket"],
|
|
179
353
|
query: { deviceName: DASHBOARD_NAME },
|
|
@@ -183,31 +357,59 @@ var SyncClient = class {
|
|
|
183
357
|
});
|
|
184
358
|
this.socket = socket;
|
|
185
359
|
let settled = false;
|
|
186
|
-
const
|
|
360
|
+
const onReady = () => {
|
|
187
361
|
if (settled) return;
|
|
188
362
|
settled = true;
|
|
189
363
|
this.log("connected to broker");
|
|
190
|
-
|
|
364
|
+
resolve2();
|
|
191
365
|
};
|
|
192
|
-
|
|
193
|
-
|
|
366
|
+
const readinessTimeout = setTimeout(() => {
|
|
367
|
+
if (settled) return;
|
|
368
|
+
settled = true;
|
|
369
|
+
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."));
|
|
370
|
+
}, 5e3);
|
|
371
|
+
socket.on("buoy:access", (message) => {
|
|
372
|
+
const ready = !!message && typeof message === "object" && message.protocol === 1 && message.ready === true;
|
|
373
|
+
this.setAdmission(ready);
|
|
374
|
+
if (ready) {
|
|
375
|
+
clearTimeout(readinessTimeout);
|
|
376
|
+
this.rearmWatches();
|
|
377
|
+
onReady();
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
socket.on("connect", () => {
|
|
381
|
+
socket.emit("buoy:access:request");
|
|
382
|
+
});
|
|
383
|
+
socket.on("disconnect", () => this.setAdmission(false, true));
|
|
194
384
|
socket.once("connect_error", (err) => {
|
|
195
385
|
if (settled) return;
|
|
196
386
|
settled = true;
|
|
387
|
+
clearTimeout(readinessTimeout);
|
|
197
388
|
reject(new Error(`failed to connect to broker at ${this.url}: ${err.message}`));
|
|
198
389
|
});
|
|
199
390
|
socket.on("devtool-capabilities", (msg) => {
|
|
391
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
200
392
|
this.capabilities.set(msg.persistentDeviceId, msg);
|
|
201
393
|
void this.learnDevServerOrigin(msg.persistentDeviceId);
|
|
202
394
|
});
|
|
203
395
|
socket.on("all-devices-update", (list) => {
|
|
396
|
+
if (!this.admitted) return;
|
|
204
397
|
if (!Array.isArray(list)) return;
|
|
398
|
+
let deviceReadmitted = false;
|
|
399
|
+
for (const device of list) {
|
|
400
|
+
if (device.deviceId && device.tier === "locked") this.lockDevice(String(device.deviceId));
|
|
401
|
+
else if (device.deviceId && (device.tier === "free" || device.tier === "pro")) {
|
|
402
|
+
deviceReadmitted = this.lockedDevices.delete(String(device.deviceId)) || deviceReadmitted;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
205
405
|
this.devices = list;
|
|
206
406
|
const now = Date.now();
|
|
207
407
|
for (const d of list) if (d.deviceId) this.lastSeen.set(d.deviceId, now);
|
|
208
408
|
this.pruneStaleDevices();
|
|
409
|
+
if (deviceReadmitted) this.rearmWatches();
|
|
209
410
|
});
|
|
210
411
|
socket.on("devtool-sync", (msg) => {
|
|
412
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
211
413
|
const key = snapKey(msg.persistentDeviceId, msg.toolId);
|
|
212
414
|
this.snapshots.set(key, msg.payload);
|
|
213
415
|
this.snapshotAt.set(key, Date.now());
|
|
@@ -218,6 +420,7 @@ var SyncClient = class {
|
|
|
218
420
|
}
|
|
219
421
|
});
|
|
220
422
|
socket.on("devtool-action-result", (msg) => {
|
|
423
|
+
if (!this.admitted) return;
|
|
221
424
|
const pending = this.pendingActions.get(msg.requestId);
|
|
222
425
|
if (!pending) return;
|
|
223
426
|
this.pendingActions.delete(msg.requestId);
|
|
@@ -235,6 +438,55 @@ var SyncClient = class {
|
|
|
235
438
|
* with every simulator erase and fresh dev client, and each one can hold
|
|
236
439
|
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
237
440
|
*/
|
|
441
|
+
requireAdmission() {
|
|
442
|
+
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.");
|
|
443
|
+
}
|
|
444
|
+
setAdmission(ready, preserveWatchIntent = false) {
|
|
445
|
+
this.admitted = ready;
|
|
446
|
+
if (ready) return;
|
|
447
|
+
this.devices = [];
|
|
448
|
+
this.lockedDevices.clear();
|
|
449
|
+
this.capabilities.clear();
|
|
450
|
+
this.snapshots.clear();
|
|
451
|
+
this.snapshotAt.clear();
|
|
452
|
+
this.devServerOrigins.clear();
|
|
453
|
+
this.originProbed.clear();
|
|
454
|
+
this.health.clear();
|
|
455
|
+
this.lastSeen.clear();
|
|
456
|
+
if (!preserveWatchIntent) this.watching.clear();
|
|
457
|
+
for (const pending of this.pendingActions.values()) {
|
|
458
|
+
clearTimeout(pending.timer);
|
|
459
|
+
pending.reject(new Error("Buoy account access ended while the action was pending."));
|
|
460
|
+
}
|
|
461
|
+
this.pendingActions.clear();
|
|
462
|
+
for (const waiters of this.snapshotWaiters.values()) for (const waiter of waiters) waiter(void 0);
|
|
463
|
+
this.snapshotWaiters.clear();
|
|
464
|
+
if (this.socket) this.socket.sendBuffer = [];
|
|
465
|
+
}
|
|
466
|
+
requireDeviceAccount(deviceId) {
|
|
467
|
+
if (this.lockedDevices.has(deviceId)) throw new Error("The device's Buoy account access ended. Sign in and verify its key.");
|
|
468
|
+
}
|
|
469
|
+
lockDevice(deviceId) {
|
|
470
|
+
this.lockedDevices.add(deviceId);
|
|
471
|
+
this.capabilities.delete(deviceId);
|
|
472
|
+
this.health.delete(deviceId);
|
|
473
|
+
this.devServerOrigins.delete(deviceId);
|
|
474
|
+
this.originProbed.delete(deviceId);
|
|
475
|
+
const prefix = `${deviceId}::`;
|
|
476
|
+
for (const key of this.snapshots.keys()) if (key.startsWith(prefix)) {
|
|
477
|
+
this.snapshots.delete(key);
|
|
478
|
+
this.snapshotAt.delete(key);
|
|
479
|
+
}
|
|
480
|
+
for (const [key, waiters] of this.snapshotWaiters) if (key.startsWith(prefix)) {
|
|
481
|
+
this.snapshotWaiters.delete(key);
|
|
482
|
+
for (const waiter of waiters) waiter(void 0);
|
|
483
|
+
}
|
|
484
|
+
for (const [id, pending] of this.pendingActions) if (pending.target === deviceId || pending.target === ALL_DEVICES) {
|
|
485
|
+
clearTimeout(pending.timer);
|
|
486
|
+
this.pendingActions.delete(id);
|
|
487
|
+
pending.reject(new Error("Device account access ended while the action was pending."));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
238
490
|
pruneStaleDevices() {
|
|
239
491
|
const cutoff = Date.now() - DEVICE_RETENTION_MS;
|
|
240
492
|
for (const [deviceId, seen] of this.lastSeen) {
|
|
@@ -259,6 +511,9 @@ var SyncClient = class {
|
|
|
259
511
|
}
|
|
260
512
|
/** Disconnect and tear down a spawned broker, if any. */
|
|
261
513
|
async stop() {
|
|
514
|
+
this.setAdmission(false);
|
|
515
|
+
this.hostAccount?.stop();
|
|
516
|
+
this.hostAccount = null;
|
|
262
517
|
for (const [, p] of this.pendingActions) {
|
|
263
518
|
clearTimeout(p.timer);
|
|
264
519
|
p.reject(new Error("SyncClient stopped"));
|
|
@@ -278,6 +533,7 @@ var SyncClient = class {
|
|
|
278
533
|
// ----------------------------------------------------------------
|
|
279
534
|
/** All devices the broker knows about, merged with their advertised tools. */
|
|
280
535
|
listDevices() {
|
|
536
|
+
if (!this.admitted) return [];
|
|
281
537
|
return this.devices.filter((d) => d.deviceName !== DASHBOARD_NAME && d.deviceId).map((d) => {
|
|
282
538
|
const caps = this.capabilities.get(d.deviceId);
|
|
283
539
|
return {
|
|
@@ -286,7 +542,7 @@ var SyncClient = class {
|
|
|
286
542
|
platform: d.platform,
|
|
287
543
|
isConnected: d.isConnected !== false,
|
|
288
544
|
isPro: d.isPro === true,
|
|
289
|
-
tier: d.tier
|
|
545
|
+
tier: d.tier === "free" || d.tier === "pro" ? d.tier : d.tier === void 0 && d.isPro === true ? "pro" : "locked",
|
|
290
546
|
collision: d.collision === true,
|
|
291
547
|
tools: caps?.tools ?? []
|
|
292
548
|
};
|
|
@@ -309,9 +565,19 @@ var SyncClient = class {
|
|
|
309
565
|
* or many, an informative error is thrown (callers can pass "All" explicitly).
|
|
310
566
|
*/
|
|
311
567
|
resolveDevice(deviceId) {
|
|
312
|
-
|
|
313
|
-
|
|
568
|
+
this.requireAdmission();
|
|
569
|
+
if (deviceId) {
|
|
570
|
+
this.requireDeviceAccount(deviceId);
|
|
571
|
+
return deviceId;
|
|
572
|
+
}
|
|
573
|
+
const allConnected = this.connectedDevices();
|
|
574
|
+
const connected = allConnected.filter((d) => !this.lockedDevices.has(d.deviceId));
|
|
314
575
|
if (connected.length === 0) {
|
|
576
|
+
if (allConnected.length > 0) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`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.`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
315
581
|
throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.");
|
|
316
582
|
}
|
|
317
583
|
if (connected.length === 1) return connected[0].deviceId;
|
|
@@ -337,9 +603,10 @@ var SyncClient = class {
|
|
|
337
603
|
* is blocked.
|
|
338
604
|
*/
|
|
339
605
|
requireProDevice(deviceId) {
|
|
606
|
+
this.requireAdmission();
|
|
340
607
|
const tierNote = (dev2) => {
|
|
341
608
|
if (!dev2) return "";
|
|
342
|
-
return dev2.tier === "
|
|
609
|
+
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.`;
|
|
343
610
|
};
|
|
344
611
|
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
345
612
|
dev2
|
|
@@ -454,6 +721,7 @@ var SyncClient = class {
|
|
|
454
721
|
}
|
|
455
722
|
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
456
723
|
snapshotAgeMs(toolId, deviceId) {
|
|
724
|
+
this.requireAdmission();
|
|
457
725
|
const at = this.snapshotAt.get(snapKey(deviceId, toolId));
|
|
458
726
|
return at ? Date.now() - at : null;
|
|
459
727
|
}
|
|
@@ -489,6 +757,7 @@ var SyncClient = class {
|
|
|
489
757
|
}
|
|
490
758
|
/** The dev-server origin last reported by this device, if any. */
|
|
491
759
|
devServerOrigin(deviceId) {
|
|
760
|
+
this.requireAdmission();
|
|
492
761
|
const known = this.devServerOrigins.get(deviceId);
|
|
493
762
|
if (known) return known;
|
|
494
763
|
const snap = this.snapshots.get(snapKey(deviceId, "app"));
|
|
@@ -522,6 +791,7 @@ var SyncClient = class {
|
|
|
522
791
|
this.sendWatch(toolId, targetDeviceId);
|
|
523
792
|
}
|
|
524
793
|
sendWatch(toolId, targetDeviceId) {
|
|
794
|
+
if (!this.admitted || !this.socket?.connected || this.lockedDevices.has(targetDeviceId)) return;
|
|
525
795
|
this.socket?.emit("devtool-watch", {
|
|
526
796
|
toolId,
|
|
527
797
|
targetDeviceId,
|
|
@@ -547,10 +817,12 @@ var SyncClient = class {
|
|
|
547
817
|
unwatch(toolId, targetDeviceId) {
|
|
548
818
|
const key = snapKey(targetDeviceId, toolId);
|
|
549
819
|
if (!this.watching.delete(key)) return;
|
|
550
|
-
this.socket?.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
820
|
+
if (this.admitted && this.socket?.connected) this.socket.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
551
821
|
}
|
|
552
822
|
/** Last snapshot received for a tool/device, if any. */
|
|
553
823
|
cachedSnapshot(toolId, deviceId) {
|
|
824
|
+
this.requireAdmission();
|
|
825
|
+
this.requireDeviceAccount(deviceId);
|
|
554
826
|
return this.snapshots.get(snapKey(deviceId, toolId));
|
|
555
827
|
}
|
|
556
828
|
/**
|
|
@@ -578,6 +850,8 @@ var SyncClient = class {
|
|
|
578
850
|
const next = this.waitForSnapshot(key, budget);
|
|
579
851
|
this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
|
|
580
852
|
const payload = await next;
|
|
853
|
+
this.requireAdmission();
|
|
854
|
+
this.requireDeviceAccount(target);
|
|
581
855
|
if (payload !== void 0) {
|
|
582
856
|
if (!opts.silent) this.noteOk(target);
|
|
583
857
|
return { deviceId: target, payload };
|
|
@@ -598,16 +872,16 @@ var SyncClient = class {
|
|
|
598
872
|
return `\u26A0\uFE0F Served from the last sync (${ageText}) \u2014 ${this.unresponsiveNote(deviceId)}`;
|
|
599
873
|
}
|
|
600
874
|
waitForSnapshot(key, timeoutMs) {
|
|
601
|
-
return new Promise((
|
|
875
|
+
return new Promise((resolve2) => {
|
|
602
876
|
const waiters = this.snapshotWaiters.get(key) ?? [];
|
|
603
877
|
const timer = setTimeout(() => {
|
|
604
878
|
const remaining = (this.snapshotWaiters.get(key) ?? []).filter((w) => w !== onSnap);
|
|
605
879
|
this.snapshotWaiters.set(key, remaining);
|
|
606
|
-
|
|
880
|
+
resolve2(void 0);
|
|
607
881
|
}, timeoutMs);
|
|
608
882
|
const onSnap = (payload) => {
|
|
609
883
|
clearTimeout(timer);
|
|
610
|
-
|
|
884
|
+
resolve2(payload);
|
|
611
885
|
};
|
|
612
886
|
waiters.push(onSnap);
|
|
613
887
|
this.snapshotWaiters.set(key, waiters);
|
|
@@ -621,11 +895,12 @@ var SyncClient = class {
|
|
|
621
895
|
* target one device, or "All" to broadcast (resolves with the first responder).
|
|
622
896
|
*/
|
|
623
897
|
async callAction(toolId, action, params, deviceId, opts = {}) {
|
|
898
|
+
this.requireAdmission();
|
|
624
899
|
const target = deviceId === ALL_DEVICES ? ALL_DEVICES : this.resolveDevice(deviceId);
|
|
625
900
|
if (!this.socket) throw new Error("SyncClient is not connected");
|
|
626
|
-
const requestId = (0,
|
|
901
|
+
const requestId = (0, import_node_crypto2.randomUUID)();
|
|
627
902
|
const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target, toolId, action);
|
|
628
|
-
const result = await new Promise((
|
|
903
|
+
const result = await new Promise((resolve2, reject) => {
|
|
629
904
|
const timer = setTimeout(() => {
|
|
630
905
|
this.pendingActions.delete(requestId);
|
|
631
906
|
if (target !== ALL_DEVICES && !opts.silent) this.noteTimeout(target);
|
|
@@ -635,7 +910,7 @@ var SyncClient = class {
|
|
|
635
910
|
)
|
|
636
911
|
);
|
|
637
912
|
}, timeoutMs);
|
|
638
|
-
this.pendingActions.set(requestId, { resolve, reject, timer });
|
|
913
|
+
this.pendingActions.set(requestId, { resolve: resolve2, reject, timer, target });
|
|
639
914
|
this.socket.emit("devtool-action", {
|
|
640
915
|
requestId,
|
|
641
916
|
toolId,
|
|
@@ -653,16 +928,16 @@ var SyncClient = class {
|
|
|
653
928
|
};
|
|
654
929
|
var snapKey = (deviceId, toolId) => `${deviceId}::${toolId}`;
|
|
655
930
|
function probeBroker(url, timeoutMs = 800) {
|
|
656
|
-
return new Promise((
|
|
931
|
+
return new Promise((resolve2) => {
|
|
657
932
|
const req = import_node_http.default.get(url, (res) => {
|
|
658
933
|
res.resume();
|
|
659
|
-
|
|
934
|
+
resolve2((res.statusCode ?? 0) > 0);
|
|
660
935
|
});
|
|
661
936
|
req.setTimeout(timeoutMs, () => {
|
|
662
937
|
req.destroy();
|
|
663
|
-
|
|
938
|
+
resolve2(false);
|
|
664
939
|
});
|
|
665
|
-
req.on("error", () =>
|
|
940
|
+
req.on("error", () => resolve2(false));
|
|
666
941
|
});
|
|
667
942
|
}
|
|
668
943
|
async function waitForBroker(url, attempts = 20) {
|
|
@@ -682,19 +957,19 @@ var import_zod26 = require("zod");
|
|
|
682
957
|
var import_zod = require("zod");
|
|
683
958
|
|
|
684
959
|
// src/update/checkForUpdate.ts
|
|
685
|
-
var
|
|
686
|
-
var
|
|
687
|
-
var
|
|
960
|
+
var import_node_fs2 = require("fs");
|
|
961
|
+
var import_node_os2 = __toESM(require("os"));
|
|
962
|
+
var import_node_path2 = __toESM(require("path"));
|
|
688
963
|
|
|
689
964
|
// src/version.ts
|
|
690
|
-
var VERSION = "7.0.
|
|
965
|
+
var VERSION = "7.0.38" ? "7.0.38" : "0.0.0-dev";
|
|
691
966
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
692
967
|
|
|
693
968
|
// src/update/checkForUpdate.ts
|
|
694
969
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
695
970
|
var FETCH_TIMEOUT_MS = 1500;
|
|
696
971
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
697
|
-
var CACHE_FILE =
|
|
972
|
+
var CACHE_FILE = import_node_path2.default.join(import_node_os2.default.tmpdir(), "buoy-mcp-update-check.json");
|
|
698
973
|
function isNewer(a, b) {
|
|
699
974
|
const parse = (v) => v.split("-")[0].split(".").map((n) => Number(n) || 0);
|
|
700
975
|
const pa = parse(a);
|
|
@@ -709,7 +984,7 @@ function isNewer(a, b) {
|
|
|
709
984
|
}
|
|
710
985
|
async function readCache() {
|
|
711
986
|
try {
|
|
712
|
-
const raw = await
|
|
987
|
+
const raw = await import_node_fs2.promises.readFile(CACHE_FILE, "utf8");
|
|
713
988
|
const parsed = JSON.parse(raw);
|
|
714
989
|
if (typeof parsed.checkedAt === "number" && typeof parsed.latest === "string") {
|
|
715
990
|
return parsed;
|
|
@@ -727,7 +1002,7 @@ async function fetchLatest(now) {
|
|
|
727
1002
|
const body = await res.json();
|
|
728
1003
|
const latest = typeof body.version === "string" ? body.version : null;
|
|
729
1004
|
if (latest) {
|
|
730
|
-
await
|
|
1005
|
+
await import_node_fs2.promises.writeFile(CACHE_FILE, JSON.stringify({ checkedAt: now, latest })).catch(() => {
|
|
731
1006
|
});
|
|
732
1007
|
}
|
|
733
1008
|
return latest;
|
|
@@ -780,7 +1055,7 @@ function registerGenericTools(server, sync) {
|
|
|
780
1055
|
}
|
|
781
1056
|
const connected = devices.filter((d) => d.isConnected);
|
|
782
1057
|
const anyPro = connected.some((d) => d.isPro);
|
|
783
|
-
const anySignedIn = connected.some((d) => d.tier !== "
|
|
1058
|
+
const anySignedIn = connected.some((d) => d.tier !== "locked");
|
|
784
1059
|
const collided = connected.filter((d) => d.collision);
|
|
785
1060
|
return json({
|
|
786
1061
|
devices,
|
|
@@ -790,7 +1065,7 @@ function registerGenericTools(server, sync) {
|
|
|
790
1065
|
...anyPro ? {} : {
|
|
791
1066
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
792
1067
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
793
|
-
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
|
|
1068
|
+
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."
|
|
794
1069
|
},
|
|
795
1070
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
796
1071
|
});
|
|
@@ -1145,7 +1420,7 @@ function formatRankingMarkdown(ranking) {
|
|
|
1145
1420
|
].filter(Boolean).join(" \xB7 ");
|
|
1146
1421
|
if (leaderLine) lines.push(`**Per-metric best:** ${leaderLine}`);
|
|
1147
1422
|
lines.push(
|
|
1148
|
-
ranking.balanced.length ? `**
|
|
1423
|
+
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.`
|
|
1149
1424
|
);
|
|
1150
1425
|
if (ranking.flagged.length) {
|
|
1151
1426
|
lines.push(
|
|
@@ -1332,7 +1607,7 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1332
1607
|
server.registerTool(
|
|
1333
1608
|
"run_benchmark_batch",
|
|
1334
1609
|
{
|
|
1335
|
-
description: "Run a perf-monitor benchmark batch and return
|
|
1610
|
+
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.",
|
|
1336
1611
|
inputSchema: {
|
|
1337
1612
|
targetRoute: import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1338
1613
|
cases: import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
@@ -1343,10 +1618,10 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1343
1618
|
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1344
1619
|
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1345
1620
|
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1346
|
-
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave
|
|
1621
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),
|
|
1347
1622
|
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1348
1623
|
captureRenders: import_zod3.z.boolean().optional().describe(
|
|
1349
|
-
"Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds
|
|
1624
|
+
"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."
|
|
1350
1625
|
),
|
|
1351
1626
|
deviceId: import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1352
1627
|
maxWaitMs: import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
@@ -1540,7 +1815,7 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1540
1815
|
server.registerTool(
|
|
1541
1816
|
"compare_reports",
|
|
1542
1817
|
{
|
|
1543
|
-
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
|
|
1818
|
+
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.",
|
|
1544
1819
|
inputSchema: {
|
|
1545
1820
|
reportIdA: import_zod3.z.string().describe("First (baseline) report id."),
|
|
1546
1821
|
reportIdB: import_zod3.z.string().describe("Second report id to compare against the baseline."),
|
|
@@ -1609,7 +1884,7 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1609
1884
|
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1610
1885
|
runsPerCase: import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1611
1886
|
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1612
|
-
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave
|
|
1887
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),
|
|
1613
1888
|
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1614
1889
|
captureRenders: import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1615
1890
|
deviceId: deviceIdInput
|
|
@@ -1674,7 +1949,7 @@ function registerEventsTools(server, sync) {
|
|
|
1674
1949
|
server.registerTool(
|
|
1675
1950
|
"get_events",
|
|
1676
1951
|
{
|
|
1677
|
-
description: "
|
|
1952
|
+
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.",
|
|
1678
1953
|
inputSchema: {
|
|
1679
1954
|
limit: import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1680
1955
|
sources: import_zod4.z.array(import_zod4.z.string()).optional().describe('Only these sources. Coarse names accepted: "network", "redux", "react-query", "storage", "route", "render", "zustand", "jotai". Omit for all.'),
|
|
@@ -1803,12 +2078,12 @@ async function captureDiagnosis(sync, target) {
|
|
|
1803
2078
|
target
|
|
1804
2079
|
);
|
|
1805
2080
|
} catch {
|
|
1806
|
-
return "
|
|
2081
|
+
return "Capture status is unavailable. An empty result does not establish whether requests occurred.";
|
|
1807
2082
|
}
|
|
1808
2083
|
if (!status || status.capturing !== false) {
|
|
1809
|
-
return "
|
|
2084
|
+
return "No requests are available in this result. Check capture setup, supported transports and hook timing before concluding that no request occurred.";
|
|
1810
2085
|
}
|
|
1811
|
-
return "
|
|
2086
|
+
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.";
|
|
1812
2087
|
}
|
|
1813
2088
|
function skippedNote(skipped) {
|
|
1814
2089
|
return skipped > 0 ? `
|
|
@@ -1840,15 +2115,33 @@ function formatRow(index, e, markers) {
|
|
|
1840
2115
|
\u2514\u2500 ${bits.join(" \xB7 ")}`;
|
|
1841
2116
|
}
|
|
1842
2117
|
function registerNetworkTools(server, sync) {
|
|
2118
|
+
server.registerTool("network_conditions", {
|
|
2119
|
+
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.",
|
|
2120
|
+
inputSchema: {
|
|
2121
|
+
action: import_zod5.z.enum(["get", "set"]).describe("Read the effective profile or select one."),
|
|
2122
|
+
profile: import_zod5.z.enum(["normal", "offline", "slow", "verySlow"]).optional().describe("Required for set."),
|
|
2123
|
+
deviceId: deviceIdInput
|
|
2124
|
+
}
|
|
2125
|
+
}, async (args) => {
|
|
2126
|
+
try {
|
|
2127
|
+
if (args.action === "set" && !args.profile) return fail2("Set requires a profile.");
|
|
2128
|
+
const target = sync.resolveDevice(args.deviceId);
|
|
2129
|
+
const result = await sync.callAction(NETWORK_TOOL_ID, args.action === "get" ? "getNetworkConditions" : "setNetworkConditions", args.action === "set" ? { profile: args.profile } : void 0, target);
|
|
2130
|
+
if (!result || typeof result !== "object" || !("profile" in result)) return fail2("The device did not report its effective network conditions.");
|
|
2131
|
+
return text(JSON.stringify(result));
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
return fail2(errMsg2(error));
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
1843
2136
|
server.registerTool(
|
|
1844
2137
|
"get_network_requests",
|
|
1845
2138
|
{
|
|
1846
|
-
description: "List captured HTTP requests
|
|
2139
|
+
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.",
|
|
1847
2140
|
inputSchema: {
|
|
1848
2141
|
limit: import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),
|
|
1849
2142
|
pattern: import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
1850
2143
|
status: import_zod5.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
1851
|
-
flagged: import_zod5.z.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
|
|
2144
|
+
flagged: import_zod5.z.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."),
|
|
1852
2145
|
includeBodies: import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
1853
2146
|
deviceId: deviceIdInput
|
|
1854
2147
|
}
|
|
@@ -1927,7 +2220,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1927
2220
|
server.registerTool(
|
|
1928
2221
|
"network_action",
|
|
1929
2222
|
{
|
|
1930
|
-
description: "
|
|
2223
|
+
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.",
|
|
1931
2224
|
inputSchema: {
|
|
1932
2225
|
action: import_zod5.z.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)."),
|
|
1933
2226
|
id: import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
@@ -1983,7 +2276,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1983
2276
|
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
1984
2277
|
case "clearRequests":
|
|
1985
2278
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
1986
|
-
return text(`
|
|
2279
|
+
return text(`Clear request sent to ${target}. Re-list to confirm the live capture list; this action does not clear retained records.`);
|
|
1987
2280
|
}
|
|
1988
2281
|
} catch (e) {
|
|
1989
2282
|
return fail2(errMsg2(e));
|
|
@@ -1993,7 +2286,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1993
2286
|
server.registerTool(
|
|
1994
2287
|
"network_override",
|
|
1995
2288
|
{
|
|
1996
|
-
description: "
|
|
2289
|
+
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.",
|
|
1997
2290
|
inputSchema: {
|
|
1998
2291
|
action: import_zod5.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
1999
2292
|
"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."
|
|
@@ -2014,6 +2307,9 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2014
2307
|
body: import_zod5.z.union([import_zod5.z.string(), import_zod5.z.record(import_zod5.z.unknown()), import_zod5.z.array(import_zod5.z.unknown())]).optional().describe(
|
|
2015
2308
|
"For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
|
|
2016
2309
|
),
|
|
2310
|
+
bodyPatch: import_zod5.z.record(import_zod5.z.unknown()).optional().describe(
|
|
2311
|
+
'For `add` with `fromRequestId` (or a rule `id`): fields deep-merged INTO the real captured response \u2014 the way to change one field of a big payload without pasting it back. Send only what changes, e.g. {"name":"test123"}. Ignored when `body` is given.'
|
|
2312
|
+
),
|
|
2017
2313
|
statusText: import_zod5.z.string().optional().describe(
|
|
2018
2314
|
"For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
|
|
2019
2315
|
),
|
|
@@ -2028,7 +2324,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2028
2324
|
),
|
|
2029
2325
|
name: import_zod5.z.string().optional().describe("Label shown in the developer's UI."),
|
|
2030
2326
|
enabled: import_zod5.z.boolean().optional().describe(
|
|
2031
|
-
"For `add`:
|
|
2327
|
+
"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})."
|
|
2032
2328
|
),
|
|
2033
2329
|
id: import_zod5.z.string().optional().describe(
|
|
2034
2330
|
"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."
|
|
@@ -2065,6 +2361,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2065
2361
|
status: args.status,
|
|
2066
2362
|
statusText: args.statusText,
|
|
2067
2363
|
body: args.body,
|
|
2364
|
+
bodyPatch: args.bodyPatch,
|
|
2068
2365
|
headers: args.headers,
|
|
2069
2366
|
failKind: args.failKind,
|
|
2070
2367
|
delayMs: args.delayMs,
|
|
@@ -2098,7 +2395,7 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2098
2395
|
}
|
|
2099
2396
|
case "clear":
|
|
2100
2397
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2101
|
-
return text(`Removed all override rules on ${target}.
|
|
2398
|
+
return text(`Removed all override rules on ${target}. Other interception or simulation settings may still apply.`);
|
|
2102
2399
|
case "enable":
|
|
2103
2400
|
case "disable": {
|
|
2104
2401
|
const enabled = args.action === "enable";
|
|
@@ -2110,12 +2407,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2110
2407
|
target
|
|
2111
2408
|
);
|
|
2112
2409
|
return text(
|
|
2113
|
-
`Rule \`${args.id}\`
|
|
2410
|
+
`Rule \`${args.id}\` was requested ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2114
2411
|
);
|
|
2115
2412
|
}
|
|
2116
2413
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2117
2414
|
return text(
|
|
2118
|
-
enabled ? `Overrides
|
|
2415
|
+
enabled ? `Overrides enabled in settings for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2119
2416
|
);
|
|
2120
2417
|
}
|
|
2121
2418
|
case "list": {
|
|
@@ -2145,7 +2442,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2145
2442
|
return text(
|
|
2146
2443
|
`No override rules on ${target}.
|
|
2147
2444
|
|
|
2148
|
-
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
2445
|
+
Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`
|
|
2149
2446
|
);
|
|
2150
2447
|
}
|
|
2151
2448
|
const rows = rules.map((rule, i) => {
|
|
@@ -2242,7 +2539,7 @@ function registerReactQueryTools(server, sync) {
|
|
|
2242
2539
|
server.registerTool(
|
|
2243
2540
|
"get_react_query",
|
|
2244
2541
|
{
|
|
2245
|
-
description: "Read the
|
|
2542
|
+
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.",
|
|
2246
2543
|
inputSchema: {
|
|
2247
2544
|
includeData: import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
2248
2545
|
staleOnly: import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),
|
|
@@ -2301,13 +2598,13 @@ ${lines.join("\n")}${note}`);
|
|
|
2301
2598
|
const target = sync.requireProDevice(args.deviceId);
|
|
2302
2599
|
if (args.action === "clear") {
|
|
2303
2600
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
2304
|
-
return text("
|
|
2601
|
+
return text("Requested clearing of the connected React Query cache. Read it again to confirm.");
|
|
2305
2602
|
}
|
|
2306
2603
|
if (!args.queryHash) {
|
|
2307
2604
|
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
2308
2605
|
}
|
|
2309
2606
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
2310
|
-
return text(`
|
|
2607
|
+
return text(`Requested ${args.action} on query \`${args.queryHash}\`.`);
|
|
2311
2608
|
} catch (e) {
|
|
2312
2609
|
return fail2(errMsg2(e));
|
|
2313
2610
|
}
|
|
@@ -2322,7 +2619,7 @@ function registerZustandTools(server, sync) {
|
|
|
2322
2619
|
server.registerTool(
|
|
2323
2620
|
"get_zustand_state",
|
|
2324
2621
|
{
|
|
2325
|
-
description:
|
|
2622
|
+
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.',
|
|
2326
2623
|
inputSchema: {
|
|
2327
2624
|
includeValues: import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
2328
2625
|
limit: import_zod7.z.number().optional().describe("First N stores. Default all."),
|
|
@@ -2374,7 +2671,7 @@ ${lines.join("\n")}${note}`);
|
|
|
2374
2671
|
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
2375
2672
|
target
|
|
2376
2673
|
);
|
|
2377
|
-
return text(`
|
|
2674
|
+
return text(`Requested state update on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2378
2675
|
})
|
|
2379
2676
|
);
|
|
2380
2677
|
}
|
|
@@ -2386,7 +2683,7 @@ function registerJotaiTools(server, sync) {
|
|
|
2386
2683
|
server.registerTool(
|
|
2387
2684
|
"get_jotai_state",
|
|
2388
2685
|
{
|
|
2389
|
-
description:
|
|
2686
|
+
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.',
|
|
2390
2687
|
inputSchema: {
|
|
2391
2688
|
includeValues: import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
2392
2689
|
limit: import_zod8.z.number().optional().describe("First N atoms. Default all."),
|
|
@@ -2432,7 +2729,7 @@ ${lines.join("\n")}${note}`);
|
|
|
2432
2729
|
},
|
|
2433
2730
|
withDevice(sync, async (args, target) => {
|
|
2434
2731
|
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
2435
|
-
return text(`
|
|
2732
|
+
return text(`Requested update of Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2436
2733
|
})
|
|
2437
2734
|
);
|
|
2438
2735
|
}
|
|
@@ -2456,7 +2753,7 @@ function registerReduxTools(server, sync) {
|
|
|
2456
2753
|
server.registerTool(
|
|
2457
2754
|
"get_redux_state",
|
|
2458
2755
|
{
|
|
2459
|
-
description:
|
|
2756
|
+
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.',
|
|
2460
2757
|
inputSchema: {
|
|
2461
2758
|
includeValues: import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
2462
2759
|
deviceId: deviceIdInput
|
|
@@ -2605,9 +2902,9 @@ ${lines.join("\n")}`);
|
|
|
2605
2902
|
server.registerTool(
|
|
2606
2903
|
"storage_action",
|
|
2607
2904
|
{
|
|
2608
|
-
description: "Modify the app's AsyncStorage: set a key, remove a key, or clear
|
|
2905
|
+
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.",
|
|
2609
2906
|
inputSchema: {
|
|
2610
|
-
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear
|
|
2907
|
+
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),
|
|
2611
2908
|
key: import_zod10.z.string().optional().describe("The key (required for set and remove)."),
|
|
2612
2909
|
value: import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2613
2910
|
deviceId: deviceIdInput
|
|
@@ -2618,20 +2915,20 @@ ${lines.join("\n")}`);
|
|
|
2618
2915
|
const target = sync.requireProDevice(args.deviceId);
|
|
2619
2916
|
if (args.action === "clear") {
|
|
2620
2917
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2621
|
-
return text("
|
|
2918
|
+
return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.");
|
|
2622
2919
|
}
|
|
2623
2920
|
if (!args.key) {
|
|
2624
2921
|
return fail2(`'${args.action}' needs a key.`);
|
|
2625
2922
|
}
|
|
2626
2923
|
if (args.action === "remove") {
|
|
2627
2924
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2628
|
-
return text(`
|
|
2925
|
+
return text(`Requested removal of storage key "${args.key}".`);
|
|
2629
2926
|
}
|
|
2630
2927
|
if (args.value === void 0) {
|
|
2631
2928
|
return fail2("'set' needs a value (a string).");
|
|
2632
2929
|
}
|
|
2633
2930
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2634
|
-
return text(`
|
|
2931
|
+
return text(`Requested update of storage key "${args.key}".`);
|
|
2635
2932
|
} catch (e) {
|
|
2636
2933
|
return fail2(errMsg2(e));
|
|
2637
2934
|
}
|
|
@@ -2653,9 +2950,9 @@ function registerRoutesTools(server, sync) {
|
|
|
2653
2950
|
server.registerTool(
|
|
2654
2951
|
"get_routes",
|
|
2655
2952
|
{
|
|
2656
|
-
description:
|
|
2953
|
+
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.',
|
|
2657
2954
|
inputSchema: {
|
|
2658
|
-
includeSitemap: import_zod11.z.boolean().optional().describe("Also list
|
|
2955
|
+
includeSitemap: import_zod11.z.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),
|
|
2659
2956
|
deviceId: deviceIdInput
|
|
2660
2957
|
}
|
|
2661
2958
|
},
|
|
@@ -2736,7 +3033,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2736
3033
|
server.registerTool(
|
|
2737
3034
|
"get_console",
|
|
2738
3035
|
{
|
|
2739
|
-
description: "Read
|
|
3036
|
+
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.",
|
|
2740
3037
|
inputSchema: {
|
|
2741
3038
|
level: import_zod12.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2742
3039
|
limit: import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),
|
|
@@ -2769,7 +3066,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2769
3066
|
${lines.join("\n")}
|
|
2770
3067
|
\`\`\`
|
|
2771
3068
|
|
|
2772
|
-
_Tail of recent logs. Use get_snapshot
|
|
3069
|
+
_Tail of recent logs. Use get_snapshot for available console entry details._`);
|
|
2773
3070
|
})
|
|
2774
3071
|
);
|
|
2775
3072
|
}
|
|
@@ -2829,7 +3126,7 @@ function registerSentryTools(server, sync) {
|
|
|
2829
3126
|
server.registerTool(
|
|
2830
3127
|
"get_sentry_envelopes",
|
|
2831
3128
|
{
|
|
2832
|
-
description: "
|
|
3129
|
+
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.",
|
|
2833
3130
|
inputSchema: {
|
|
2834
3131
|
type: import_zod13.z.string().optional().describe(
|
|
2835
3132
|
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
@@ -2879,7 +3176,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2879
3176
|
server.registerTool(
|
|
2880
3177
|
"get_sentry_cost",
|
|
2881
3178
|
{
|
|
2882
|
-
description: "
|
|
3179
|
+
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.",
|
|
2883
3180
|
inputSchema: {
|
|
2884
3181
|
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2885
3182
|
}
|
|
@@ -2895,7 +3192,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2895
3192
|
const usage = p.usage;
|
|
2896
3193
|
if (!usage?.session) {
|
|
2897
3194
|
return text2(
|
|
2898
|
-
`${staleHeader2}No usage
|
|
3195
|
+
`${staleHeader2}No usage is available in this capture.${statusNote(p.status)}`
|
|
2899
3196
|
);
|
|
2900
3197
|
}
|
|
2901
3198
|
const session = usage.session;
|
|
@@ -2908,7 +3205,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2908
3205
|
const month = projected[key];
|
|
2909
3206
|
const quota = FREE_QUOTA[key];
|
|
2910
3207
|
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
2911
|
-
const pctNote = pct === null ? " (
|
|
3208
|
+
const pctNote = pct === null ? " (no configured reference quota)" : ` \u2014 ${pct}% of the configured reference quota of ${units(quota, format)}`;
|
|
2912
3209
|
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
2913
3210
|
});
|
|
2914
3211
|
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
@@ -2918,7 +3215,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2918
3215
|
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
2919
3216
|
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._";
|
|
2920
3217
|
return text2(
|
|
2921
|
-
`${staleHeader2}**Sentry
|
|
3218
|
+
`${staleHeader2}**Sentry usage estimate \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
2922
3219
|
|
|
2923
3220
|
${rows.join("\n")}${contributors}${caveat}`
|
|
2924
3221
|
);
|
|
@@ -2948,9 +3245,9 @@ ${rows.join("\n")}${contributors}${caveat}`
|
|
|
2948
3245
|
const recent = p.drops?.recent ?? [];
|
|
2949
3246
|
if (summary.length === 0) {
|
|
2950
3247
|
return text2(
|
|
2951
|
-
`${staleHeader2}
|
|
3248
|
+
`${staleHeader2}No drops are reported in the available capture.${statusNote(p.status)}
|
|
2952
3249
|
|
|
2953
|
-
_If an event is
|
|
3250
|
+
_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._`
|
|
2954
3251
|
);
|
|
2955
3252
|
}
|
|
2956
3253
|
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
@@ -3029,7 +3326,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
3029
3326
|
server.registerTool(
|
|
3030
3327
|
"get_time_machine",
|
|
3031
3328
|
{
|
|
3032
|
-
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
|
|
3329
|
+
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.",
|
|
3033
3330
|
inputSchema: {
|
|
3034
3331
|
deviceId: deviceIdInput
|
|
3035
3332
|
}
|
|
@@ -3052,7 +3349,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
3052
3349
|
}
|
|
3053
3350
|
const snapLines = snapshots.map((s) => {
|
|
3054
3351
|
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
3055
|
-
const desc = s.baseline ? "baseline
|
|
3352
|
+
const desc = s.baseline ? "baseline: clear supported providers and request reload" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
3056
3353
|
const route = s.route ? ` \xB7 route ${s.route.href}` : "";
|
|
3057
3354
|
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${route}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
3058
3355
|
});
|
|
@@ -3073,7 +3370,7 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3073
3370
|
server.registerTool(
|
|
3074
3371
|
"time_machine_action",
|
|
3075
3372
|
{
|
|
3076
|
-
description: "Manage
|
|
3373
|
+
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.",
|
|
3077
3374
|
inputSchema: {
|
|
3078
3375
|
action: import_zod14.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3079
3376
|
id: import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
@@ -3220,7 +3517,7 @@ function registerScenariosTools(server, sync) {
|
|
|
3220
3517
|
server.registerTool(
|
|
3221
3518
|
"get_scenarios",
|
|
3222
3519
|
{
|
|
3223
|
-
description: "List
|
|
3520
|
+
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.",
|
|
3224
3521
|
inputSchema: {
|
|
3225
3522
|
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3226
3523
|
}
|
|
@@ -3261,7 +3558,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3261
3558
|
server.registerTool(
|
|
3262
3559
|
"scenario_action",
|
|
3263
3560
|
{
|
|
3264
|
-
description:
|
|
3561
|
+
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.",
|
|
3265
3562
|
inputSchema: {
|
|
3266
3563
|
action: import_zod15.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3267
3564
|
folder: import_zod15.z.string().optional().describe(
|
|
@@ -3351,7 +3648,7 @@ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
|
3351
3648
|
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3352
3649
|
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3353
3650
|
lines.push(
|
|
3354
|
-
r.ok ? "
|
|
3651
|
+
r.ok ? "Pre-flight checks passed. Review the intended effects before running." : `\u274C **Cannot run:**
|
|
3355
3652
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3356
3653
|
);
|
|
3357
3654
|
return text3(lines.join("\n\n"));
|
|
@@ -3386,9 +3683,9 @@ The device is now in a partial state. Deactivate to reverse what landed, or fix
|
|
|
3386
3683
|
[
|
|
3387
3684
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3388
3685
|
steps,
|
|
3389
|
-
`**
|
|
3686
|
+
`**Reported effects:**
|
|
3390
3687
|
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3391
|
-
"The device shows a SIMULATED banner while this is active. Deactivate when done,
|
|
3688
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, and account for these effects when interpreting observations."
|
|
3392
3689
|
].join("\n\n")
|
|
3393
3690
|
);
|
|
3394
3691
|
}
|
|
@@ -3453,10 +3750,10 @@ var import_zod16 = require("zod");
|
|
|
3453
3750
|
// src/capture/simctl.ts
|
|
3454
3751
|
var import_node_child_process = require("child_process");
|
|
3455
3752
|
var import_node_util = require("util");
|
|
3456
|
-
var
|
|
3457
|
-
var
|
|
3458
|
-
var
|
|
3459
|
-
var
|
|
3753
|
+
var import_node_fs3 = require("fs");
|
|
3754
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3755
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3756
|
+
var import_node_crypto3 = require("crypto");
|
|
3460
3757
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
3461
3758
|
var clamp2 = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
3462
3759
|
function humanizeSimctlError(err) {
|
|
@@ -3482,7 +3779,7 @@ async function captureIosFramebuffer() {
|
|
|
3482
3779
|
error: "Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."
|
|
3483
3780
|
};
|
|
3484
3781
|
}
|
|
3485
|
-
const out =
|
|
3782
|
+
const out = import_node_path3.default.join(import_node_os3.default.tmpdir(), `buoy-mcp-sim-${(0, import_node_crypto3.randomUUID)()}.png`);
|
|
3486
3783
|
try {
|
|
3487
3784
|
await execFileAsync("xcrun", ["simctl", "io", "booted", "screenshot", "--type=png", out], {
|
|
3488
3785
|
timeout: 15e3
|
|
@@ -3491,7 +3788,7 @@ async function captureIosFramebuffer() {
|
|
|
3491
3788
|
return { ok: false, error: humanizeSimctlError(err) };
|
|
3492
3789
|
}
|
|
3493
3790
|
try {
|
|
3494
|
-
const buf = await
|
|
3791
|
+
const buf = await import_node_fs3.promises.readFile(out);
|
|
3495
3792
|
const size = pngSize(buf);
|
|
3496
3793
|
if (!size) return { ok: false, error: "Captured file wasn't a valid PNG." };
|
|
3497
3794
|
return { ok: true, srcPath: out, width: size.width, height: size.height };
|
|
@@ -3504,22 +3801,22 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
3504
3801
|
const y = clamp2(Math.round(region.y), 0, Math.max(0, full.height - 1));
|
|
3505
3802
|
const width = clamp2(Math.round(region.width), 1, full.width - x);
|
|
3506
3803
|
const height = clamp2(Math.round(region.height), 1, full.height - y);
|
|
3507
|
-
const out =
|
|
3804
|
+
const out = import_node_path3.default.join(import_node_os3.default.tmpdir(), `buoy-mcp-crop-${(0, import_node_crypto3.randomUUID)()}.png`);
|
|
3508
3805
|
try {
|
|
3509
3806
|
await execFileAsync(
|
|
3510
3807
|
"sips",
|
|
3511
3808
|
["-c", String(height), String(width), "--cropOffset", String(y), String(x), srcPath, "--out", out],
|
|
3512
3809
|
{ timeout: 1e4 }
|
|
3513
3810
|
);
|
|
3514
|
-
const buf = await
|
|
3811
|
+
const buf = await import_node_fs3.promises.readFile(out);
|
|
3515
3812
|
return { ok: true, buffer: buf, width, height };
|
|
3516
3813
|
} catch (err) {
|
|
3517
3814
|
const e = err;
|
|
3518
3815
|
return { ok: false, error: `Crop failed: ${e?.stderr || e?.message || err}` };
|
|
3519
3816
|
} finally {
|
|
3520
|
-
await
|
|
3817
|
+
await import_node_fs3.promises.unlink(out).catch(() => {
|
|
3521
3818
|
});
|
|
3522
|
-
await
|
|
3819
|
+
await import_node_fs3.promises.unlink(srcPath).catch(() => {
|
|
3523
3820
|
});
|
|
3524
3821
|
}
|
|
3525
3822
|
}
|
|
@@ -3591,11 +3888,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3591
3888
|
server.registerTool(
|
|
3592
3889
|
"screenshot_component",
|
|
3593
3890
|
{
|
|
3594
|
-
description: "
|
|
3891
|
+
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.",
|
|
3595
3892
|
inputSchema: {
|
|
3596
3893
|
query: import_zod16.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3597
3894
|
nativeTag: import_zod16.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3598
|
-
scrollIntoView: import_zod16.z.boolean().optional().describe("
|
|
3895
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),
|
|
3599
3896
|
deviceId: deviceIdInput
|
|
3600
3897
|
}
|
|
3601
3898
|
},
|
|
@@ -3683,21 +3980,21 @@ var import_zod17 = require("zod");
|
|
|
3683
3980
|
// src/capture/camera.ts
|
|
3684
3981
|
var import_node_child_process2 = require("child_process");
|
|
3685
3982
|
var import_node_util2 = require("util");
|
|
3686
|
-
var
|
|
3687
|
-
var
|
|
3688
|
-
var
|
|
3983
|
+
var import_node_fs4 = require("fs");
|
|
3984
|
+
var import_node_os4 = __toESM(require("os"));
|
|
3985
|
+
var import_node_path4 = __toESM(require("path"));
|
|
3689
3986
|
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3690
3987
|
function candidates() {
|
|
3691
3988
|
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3692
3989
|
const list = [
|
|
3693
|
-
...fromEnv ? [
|
|
3990
|
+
...fromEnv ? [import_node_path4.default.join(fromEnv, "buoycam")] : [],
|
|
3694
3991
|
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3695
|
-
|
|
3992
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3696
3993
|
];
|
|
3697
3994
|
let dir = __dirname;
|
|
3698
3995
|
for (let i = 0; i < 8; i++) {
|
|
3699
|
-
list.push(
|
|
3700
|
-
const parent =
|
|
3996
|
+
list.push(import_node_path4.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3997
|
+
const parent = import_node_path4.default.dirname(dir);
|
|
3701
3998
|
if (parent === dir) break;
|
|
3702
3999
|
dir = parent;
|
|
3703
4000
|
}
|
|
@@ -3706,7 +4003,7 @@ function candidates() {
|
|
|
3706
4003
|
var resolved = null;
|
|
3707
4004
|
function cliPath() {
|
|
3708
4005
|
if (resolved) return resolved;
|
|
3709
|
-
resolved = candidates().find((c) => (0,
|
|
4006
|
+
resolved = candidates().find((c) => (0, import_node_fs4.existsSync)(c)) ?? null;
|
|
3710
4007
|
return resolved;
|
|
3711
4008
|
}
|
|
3712
4009
|
async function run(args) {
|
|
@@ -3807,28 +4104,28 @@ function entitlementPaths() {
|
|
|
3807
4104
|
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3808
4105
|
if (fromEnv) return [fromEnv];
|
|
3809
4106
|
return [
|
|
3810
|
-
|
|
3811
|
-
|
|
4107
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
4108
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3812
4109
|
];
|
|
3813
4110
|
}
|
|
3814
4111
|
function readEntitlement() {
|
|
3815
4112
|
for (const p of entitlementPaths()) {
|
|
3816
4113
|
try {
|
|
3817
|
-
if (!(0,
|
|
3818
|
-
const parsed = JSON.parse((0,
|
|
4114
|
+
if (!(0, import_node_fs4.existsSync)(p)) continue;
|
|
4115
|
+
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(p, "utf8"));
|
|
3819
4116
|
return {
|
|
3820
|
-
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "
|
|
3821
|
-
isPro: parsed.isPro === true
|
|
4117
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "locked",
|
|
4118
|
+
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
|
|
3822
4119
|
};
|
|
3823
4120
|
} catch {
|
|
3824
4121
|
}
|
|
3825
4122
|
}
|
|
3826
|
-
return { tier: "
|
|
4123
|
+
return { tier: "locked", isPro: false };
|
|
3827
4124
|
}
|
|
3828
4125
|
function proRequired(tool) {
|
|
3829
4126
|
return `\`${tool}\` is a Buoy Pro feature.
|
|
3830
4127
|
|
|
3831
|
-
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel
|
|
4128
|
+
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.
|
|
3832
4129
|
|
|
3833
4130
|
_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._`;
|
|
3834
4131
|
}
|
|
@@ -4075,16 +4372,24 @@ function registerInteractTools(server, sync) {
|
|
|
4075
4372
|
server.registerTool(
|
|
4076
4373
|
"describe_screen",
|
|
4077
4374
|
{
|
|
4078
|
-
description: "List
|
|
4375
|
+
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.",
|
|
4079
4376
|
inputSchema: {
|
|
4080
4377
|
interactiveOnly: import_zod18.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4378
|
+
includeBuoy: import_zod18.z.boolean().optional().describe(
|
|
4379
|
+
"Also list Buoy's own overlay \u2014 the dial, tool sheets, Ask Buoy's chat. Default false: the screen described is the app's. Exact testIDs inside Buoy (buoy-\u2026) can still be tapped without this."
|
|
4380
|
+
),
|
|
4081
4381
|
deviceId: deviceIdInput
|
|
4082
4382
|
}
|
|
4083
4383
|
},
|
|
4084
4384
|
async (args) => {
|
|
4085
4385
|
try {
|
|
4086
4386
|
const target = sync.requireProDevice(args.deviceId);
|
|
4087
|
-
const res = await sync.callAction(
|
|
4387
|
+
const res = await sync.callAction(
|
|
4388
|
+
TOOL_ID2,
|
|
4389
|
+
"describeScreen",
|
|
4390
|
+
{ includeBuoy: args.includeBuoy },
|
|
4391
|
+
target
|
|
4392
|
+
);
|
|
4088
4393
|
const all = res?.elements ?? [];
|
|
4089
4394
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
4090
4395
|
if (elements.length === 0) {
|
|
@@ -4092,7 +4397,8 @@ function registerInteractTools(server, sync) {
|
|
|
4092
4397
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
4093
4398
|
);
|
|
4094
4399
|
}
|
|
4095
|
-
const
|
|
4400
|
+
const hidden = res.hiddenBuoy ? ` Buoy's own overlay (${res.hiddenBuoy} element${res.hiddenBuoy === 1 ? "" : "s"}) is left out \u2014 pass includeBuoy:true to see it.` : "";
|
|
4401
|
+
const header = `Screen ${res.screen.width}\xD7${res.screen.height} \u2014 ${elements.length} element${elements.length === 1 ? "" : "s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).${hidden}`;
|
|
4096
4402
|
return text5(`${header}
|
|
4097
4403
|
|
|
4098
4404
|
${elements.map(formatElement).join("\n")}`);
|
|
@@ -4104,7 +4410,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4104
4410
|
server.registerTool(
|
|
4105
4411
|
"tap_element",
|
|
4106
4412
|
{
|
|
4107
|
-
description: "
|
|
4413
|
+
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.",
|
|
4108
4414
|
inputSchema: {
|
|
4109
4415
|
nativeTag: import_zod18.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4110
4416
|
testID: import_zod18.z.string().optional().describe("Exact testID of the element."),
|
|
@@ -4112,7 +4418,8 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4112
4418
|
value: import_zod18.z.union([import_zod18.z.number(), import_zod18.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
4113
4419
|
text: import_zod18.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4114
4420
|
longPress: import_zod18.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
4115
|
-
scrollIntoView: import_zod18.z.boolean().optional().describe("
|
|
4421
|
+
scrollIntoView: import_zod18.z.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),
|
|
4422
|
+
includeBuoy: import_zod18.z.boolean().optional().describe("Let a fuzzy `query` match Buoy's own overlay. Default false \u2014 an exact nativeTag/testID always may."),
|
|
4116
4423
|
deviceId: deviceIdInput
|
|
4117
4424
|
}
|
|
4118
4425
|
},
|
|
@@ -4132,7 +4439,8 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4132
4439
|
value: args.value,
|
|
4133
4440
|
text: args.text,
|
|
4134
4441
|
longPress: args.longPress,
|
|
4135
|
-
scrollIntoView: args.scrollIntoView
|
|
4442
|
+
scrollIntoView: args.scrollIntoView,
|
|
4443
|
+
includeBuoy: args.includeBuoy
|
|
4136
4444
|
},
|
|
4137
4445
|
target
|
|
4138
4446
|
);
|
|
@@ -4144,7 +4452,12 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4144
4452
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
4145
4453
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
4146
4454
|
const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : m.via === "onLongPress" ? `Long-pressed "${label}"${id}` : `Tapped "${label}"${id}`;
|
|
4147
|
-
|
|
4455
|
+
const fx = res.effect;
|
|
4456
|
+
const effectNote = !fx ? "" : fx.commits > 0 ? ` ${fx.commits} re-render${fx.commits === 1 ? "" : "s"} followed.` : ` No re-render followed within ${fx.observedMs}ms \u2014 the handler ran but the app did not react. The element may sit on a screen beneath the current one, or be inert; do not treat this as done.`;
|
|
4457
|
+
const alsoNote = res.alsoMatched?.length ? `
|
|
4458
|
+
Also matched equally well (the choice was a coin toss \u2014 re-tap by #nativeTag if this was wrong):
|
|
4459
|
+
` + res.alsoMatched.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n") : "";
|
|
4460
|
+
return text5(`${verb}${scrolledNote}.${effectNote}${alsoNote}${flowTip}`);
|
|
4148
4461
|
}
|
|
4149
4462
|
let msg = res?.reason ?? "Tap failed.";
|
|
4150
4463
|
if (res?.candidates?.length) {
|
|
@@ -4229,11 +4542,11 @@ function registerJsThreadTools(server, sync) {
|
|
|
4229
4542
|
server.registerTool(
|
|
4230
4543
|
"get_js_thread_top",
|
|
4231
4544
|
{
|
|
4232
|
-
description:
|
|
4545
|
+
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.",
|
|
4233
4546
|
inputSchema: {
|
|
4234
4547
|
deviceId: deviceIdInput,
|
|
4235
|
-
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000
|
|
4236
|
-
clearFirst: import_zod19.z.boolean().optional().describe("Reset
|
|
4548
|
+
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),
|
|
4549
|
+
clearFirst: import_zod19.z.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")
|
|
4237
4550
|
}
|
|
4238
4551
|
},
|
|
4239
4552
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -4417,7 +4730,7 @@ function registerImagesTools(server, sync) {
|
|
|
4417
4730
|
server.registerTool(
|
|
4418
4731
|
"get_images",
|
|
4419
4732
|
{
|
|
4420
|
-
description: "
|
|
4733
|
+
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.",
|
|
4421
4734
|
inputSchema: {
|
|
4422
4735
|
deviceId: deviceIdInput,
|
|
4423
4736
|
limit: import_zod20.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
@@ -4474,7 +4787,7 @@ ${JSON.stringify(slim)}
|
|
|
4474
4787
|
server.registerTool(
|
|
4475
4788
|
"image_action",
|
|
4476
4789
|
{
|
|
4477
|
-
description: "
|
|
4790
|
+
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.",
|
|
4478
4791
|
inputSchema: {
|
|
4479
4792
|
deviceId: deviceIdInput,
|
|
4480
4793
|
id: import_zod20.z.number().describe("Record id from get_images."),
|
|
@@ -4544,12 +4857,12 @@ ${JSON.stringify(slim)}
|
|
|
4544
4857
|
server.registerTool(
|
|
4545
4858
|
"set_image_simulation",
|
|
4546
4859
|
{
|
|
4547
|
-
description: "
|
|
4860
|
+
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.",
|
|
4548
4861
|
inputSchema: {
|
|
4549
4862
|
deviceId: deviceIdInput,
|
|
4550
4863
|
network: import_zod20.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4551
|
-
blankImages: import_zod20.z.boolean().optional().describe("Render
|
|
4552
|
-
massAction: import_zod20.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to
|
|
4864
|
+
blankImages: import_zod20.z.boolean().optional().describe("Render integrated images without a source."),
|
|
4865
|
+
massAction: import_zod20.z.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."),
|
|
4553
4866
|
clearRegistry: import_zod20.z.boolean().optional().describe("Clear the captured image log."),
|
|
4554
4867
|
clearExpoCaches: import_zod20.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4555
4868
|
}
|
|
@@ -4688,7 +5001,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4688
5001
|
server.registerTool(
|
|
4689
5002
|
"get_assets",
|
|
4690
5003
|
{
|
|
4691
|
-
description: "
|
|
5004
|
+
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.",
|
|
4692
5005
|
inputSchema: {
|
|
4693
5006
|
deviceId: deviceIdInput,
|
|
4694
5007
|
limit: import_zod21.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
@@ -4821,12 +5134,12 @@ function messageSocketUrl(origin) {
|
|
|
4821
5134
|
return `${origin.replace(/^http/, "ws").replace(/\/+$/, "")}/message`;
|
|
4822
5135
|
}
|
|
4823
5136
|
function reloadViaDevServer(origin) {
|
|
4824
|
-
return new Promise((
|
|
5137
|
+
return new Promise((resolve2) => {
|
|
4825
5138
|
let settled = false;
|
|
4826
5139
|
const done = (result) => {
|
|
4827
5140
|
if (settled) return;
|
|
4828
5141
|
settled = true;
|
|
4829
|
-
|
|
5142
|
+
resolve2(result);
|
|
4830
5143
|
};
|
|
4831
5144
|
let socket;
|
|
4832
5145
|
try {
|
|
@@ -4911,7 +5224,7 @@ function registerReloadTools(server, sync) {
|
|
|
4911
5224
|
server.registerTool(
|
|
4912
5225
|
"reload_app",
|
|
4913
5226
|
{
|
|
4914
|
-
description: "
|
|
5227
|
+
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.",
|
|
4915
5228
|
inputSchema: {
|
|
4916
5229
|
strategy: import_zod22.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4917
5230
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
@@ -5035,7 +5348,7 @@ function registerTriageTools(server, sync) {
|
|
|
5035
5348
|
server.registerTool(
|
|
5036
5349
|
"get_triage",
|
|
5037
5350
|
{
|
|
5038
|
-
description: "
|
|
5351
|
+
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.",
|
|
5039
5352
|
inputSchema: {
|
|
5040
5353
|
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
5041
5354
|
}
|
|
@@ -5202,10 +5515,10 @@ function registerFlowTools(server, sync) {
|
|
|
5202
5515
|
server.registerTool(
|
|
5203
5516
|
"run_flow",
|
|
5204
5517
|
{
|
|
5205
|
-
description: "Run
|
|
5518
|
+
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.",
|
|
5206
5519
|
inputSchema: {
|
|
5207
5520
|
steps: import_zod24.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5208
|
-
include: import_zod24.z.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final
|
|
5521
|
+
include: import_zod24.z.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."),
|
|
5209
5522
|
continueOnFailure: import_zod24.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5210
5523
|
deviceId: deviceIdInput
|
|
5211
5524
|
}
|
|
@@ -5527,7 +5840,7 @@ function formatCapture(c, top) {
|
|
|
5527
5840
|
}
|
|
5528
5841
|
lines.push("");
|
|
5529
5842
|
lines.push(
|
|
5530
|
-
"`wasted`
|
|
5843
|
+
"`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."
|
|
5531
5844
|
);
|
|
5532
5845
|
return lines.join("\n");
|
|
5533
5846
|
}
|
|
@@ -5563,14 +5876,14 @@ function registerRenderTools(server, sync) {
|
|
|
5563
5876
|
server.registerTool(
|
|
5564
5877
|
"measure_renders",
|
|
5565
5878
|
{
|
|
5566
|
-
description: "
|
|
5879
|
+
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.",
|
|
5567
5880
|
inputSchema: {
|
|
5568
5881
|
steps: import_zod25.z.array(stepSchema2).max(20).optional().describe(
|
|
5569
5882
|
"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."
|
|
5570
5883
|
),
|
|
5571
5884
|
idleMs: import_zod25.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5572
5885
|
label: import_zod25.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5573
|
-
compareToPrevious: import_zod25.z.boolean().optional().describe("
|
|
5886
|
+
compareToPrevious: import_zod25.z.boolean().optional().describe("Compare with the previous measurement on this device. Use equivalent steps and conditions; review warnings and behavior separately."),
|
|
5574
5887
|
top: import_zod25.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5575
5888
|
deviceId: deviceIdInput
|
|
5576
5889
|
}
|
|
@@ -5660,7 +5973,7 @@ function registerRenderTools(server, sync) {
|
|
|
5660
5973
|
"_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._"
|
|
5661
5974
|
);
|
|
5662
5975
|
} else if (!args.compareToPrevious) {
|
|
5663
|
-
parts.push("", "
|
|
5976
|
+
parts.push("", "_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._");
|
|
5664
5977
|
}
|
|
5665
5978
|
return text8(parts.join("\n"));
|
|
5666
5979
|
} catch (e) {
|
|
@@ -5671,8 +5984,8 @@ function registerRenderTools(server, sync) {
|
|
|
5671
5984
|
}
|
|
5672
5985
|
|
|
5673
5986
|
// src/verify/editWatcher.ts
|
|
5674
|
-
var
|
|
5675
|
-
var
|
|
5987
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5988
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5676
5989
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5677
5990
|
".ts",
|
|
5678
5991
|
".tsx",
|
|
@@ -5715,14 +6028,14 @@ var EditWatcher = class {
|
|
|
5715
6028
|
if (mode === "never" || this.started) return;
|
|
5716
6029
|
this.started = true;
|
|
5717
6030
|
try {
|
|
5718
|
-
this.watcher =
|
|
6031
|
+
this.watcher = import_node_fs5.default.watch(
|
|
5719
6032
|
rootDir,
|
|
5720
6033
|
{ recursive: true },
|
|
5721
6034
|
(_event, filename) => {
|
|
5722
6035
|
if (!filename) return;
|
|
5723
6036
|
const rel2 = String(filename);
|
|
5724
|
-
if (rel2.split(
|
|
5725
|
-
if (!WATCHED_EXT.has(
|
|
6037
|
+
if (rel2.split(import_node_path5.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
6038
|
+
if (!WATCHED_EXT.has(import_node_path5.default.extname(rel2))) return;
|
|
5726
6039
|
this.noteEdit(rel2);
|
|
5727
6040
|
}
|
|
5728
6041
|
);
|
|
@@ -5773,129 +6086,89 @@ var EditWatcher = class {
|
|
|
5773
6086
|
|
|
5774
6087
|
---
|
|
5775
6088
|
\u26A0\uFE0F **${files.length} source file${files.length === 1 ? "" : "s"} changed since anything was last checked on the device** (${shown}${more}).
|
|
5776
|
-
|
|
6089
|
+
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.`;
|
|
5777
6090
|
}
|
|
5778
6091
|
};
|
|
5779
6092
|
var editWatcher = new EditWatcher();
|
|
5780
6093
|
|
|
5781
6094
|
// src/server.ts
|
|
5782
|
-
var SERVER_INSTRUCTIONS = `Buoy MCP
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
-
|
|
5795
|
-
|
|
5796
|
-
-
|
|
5797
|
-
|
|
5798
|
-
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
and
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
\`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
|
|
5860
|
-
freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
|
|
5861
|
-
Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
|
|
5862
|
-
This is about TASKS on the thread \u2014 for React components re-rendering too often, it's
|
|
5863
|
-
\`measure_renders\`, not this.
|
|
5864
|
-
|
|
5865
|
-
Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
|
|
5866
|
-
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
5867
|
-
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
5868
|
-
|
|
5869
|
-
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
5870
|
-
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
5871
|
-
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
5872
|
-
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
5873
|
-
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
5874
|
-
|
|
5875
|
-
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
5876
|
-
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
5877
|
-
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
5878
|
-
re-read anything you cached.
|
|
5879
|
-
|
|
5880
|
-
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
5881
|
-
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
5882
|
-
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
5883
|
-
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
5884
|
-
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
5885
|
-
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
5886
|
-
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
5887
|
-
|
|
5888
|
-
For everything else:
|
|
5889
|
-
- \`list_tool_actions\` to see what a tool can do,
|
|
5890
|
-
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
5891
|
-
- \`call_action\` to invoke any tool action generically.
|
|
5892
|
-
|
|
5893
|
-
Higher-level tools (e.g. perf-monitor's \`run_benchmark_batch\`) wrap these for common workflows.
|
|
5894
|
-
If no device is connected, open the app with Buoy devtools running and try again.
|
|
5895
|
-
|
|
5896
|
-
The Buoy MCP is a Pro feature: \`list_devices\` is always available, but data/action tools
|
|
5897
|
-
require the connected app to have an active Buoy Pro license. If they error with "Buoy Pro
|
|
5898
|
-
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
6095
|
+
var SERVER_INSTRUCTIONS = `Buoy MCP connects your editor to supported Buoy tools through the configured broker.
|
|
6096
|
+
|
|
6097
|
+
Start with account and connection setup. Use \`list_devices\` to select an app and inspect
|
|
6098
|
+
its available tools. App data and action tools require Buoy Pro. If access fails, explain
|
|
6099
|
+
the reported requirement and setup steps. Camera tools use a separate local setup and
|
|
6100
|
+
account gate; \`camera_diagnose\` can report setup problems without an app connection.
|
|
6101
|
+
|
|
6102
|
+
For a runtime bug, \`get_triage\` summarizes available console errors, network activity,
|
|
6103
|
+
storage and screen information. Follow up on the relevant section and read source when
|
|
6104
|
+
needed. Missing data can reflect missing integration, retention limits or a disconnected
|
|
6105
|
+
app. An unresponsive app is not proof of a crash, and post-crash evidence may be unavailable.
|
|
6106
|
+
|
|
6107
|
+
Choose follow-up tools by the symptom:
|
|
6108
|
+
- Persisted values: \`get_storage\` with \`includeValues:true\` for configured storage sources.
|
|
6109
|
+
- Requests or API responses: \`get_network_requests\` with \`includeBodies:true\` for captured
|
|
6110
|
+
bodies. Capture depends on supported transports, installed hooks and recording timing.
|
|
6111
|
+
- Errors: \`get_console\` for retained captured output. A stopped app cannot provide new data.
|
|
6112
|
+
- Cache or store state: \`get_react_query\`, \`get_zustand_state\`, \`get_redux_state\` or
|
|
6113
|
+
\`get_jotai_state\` for the integrations registered in the app.
|
|
6114
|
+
|
|
6115
|
+
Use \`get_events\` with \`sources\` to read retained activity from connected event sources.
|
|
6116
|
+
It returns compact summaries by default; request \`includeData:true\` when details are needed.
|
|
6117
|
+
For request IDs and network actions, use \`get_network_requests\`. Its \`flagged:'any'\` filter
|
|
6118
|
+
finds pinned or saved requests. A flag is a reason to inspect a request, not proof of its
|
|
6119
|
+
cause or who flagged it. Use \`network_action\` to retain relevant evidence within task scope.
|
|
6120
|
+
|
|
6121
|
+
For supported UI interactions, \`run_flow\` runs steps such as tap, type, set, reload,
|
|
6122
|
+
waitFor and expect. Review step results, failures and available captured output. Use
|
|
6123
|
+
\`describe_screen\` to discover exposed elements and \`tap_element\` for individual actions.
|
|
6124
|
+
A completed tool call alone does not establish that the user's expected behavior occurred.
|
|
6125
|
+
|
|
6126
|
+
For render-heavy interactions, use \`measure_renders\` with representative \`steps\`.
|
|
6127
|
+
Repeat the same steps after a change with \`compareToPrevious:true\`. Interpret available
|
|
6128
|
+
render counts, reasons and wasted-render diagnostics alongside behavior and performance;
|
|
6129
|
+
these diagnostics do not prove that a render was unnecessary. For instrumented JavaScript
|
|
6130
|
+
tasks, use \`get_js_thread_top\` and \`get_js_thread_origin_detail\`. Their coverage depends
|
|
6131
|
+
on the installed hooks. Keep measurement conditions comparable.
|
|
6132
|
+
|
|
6133
|
+
\`get_images\` inspects instrumented image loads and available cache, timing, size and error
|
|
6134
|
+
data. Use \`image_action\` for supported per-image actions and \`set_image_simulation\` for
|
|
6135
|
+
integrated image behavior. Simulation does not disconnect the device's network.
|
|
6136
|
+
\`get_assets\` inspects the configured manifest, runtime registry and available bundle graph.
|
|
6137
|
+
Reported load history and size estimates do not prove complete shipped-asset coverage or
|
|
6138
|
+
that an unobserved asset is unused. \`asset_action\` can refresh data and manage baselines.
|
|
6139
|
+
|
|
6140
|
+
\`reload_app\` requests a JS reload using supported app mechanisms and can wait for
|
|
6141
|
+
reconnection. It can try a dev-server fallback for an unresponsive app when that server
|
|
6142
|
+
is known and reachable. Recovery is not guaranteed. Reload can discard in-memory evidence;
|
|
6143
|
+
collect needed observations first when possible and read state again after reconnecting.
|
|
6144
|
+
|
|
6145
|
+
\`get_time_machine\` lists snapshots and provider capabilities. \`time_machine_action\`
|
|
6146
|
+
captures or restores supported registered state sources. Preview the affected state before
|
|
6147
|
+
restoring. Live restore changes supported state in place; reload mode restores supported
|
|
6148
|
+
persisted sources and requests a reload. A baseline clears providers that support clearing
|
|
6149
|
+
and requests a reload. This does not reset backend state or reproduce a complete reinstall.
|
|
6150
|
+
Check per-source applied/skipped outcomes and any reload or route-restoration failures.
|
|
6151
|
+
|
|
6152
|
+
For iOS Simulator camera input, inspect setup with \`camera_diagnose\`. Use \`camera_source\`
|
|
6153
|
+
for supported barcode, file or screen input, \`camera_devices\` for simulators/apps and
|
|
6154
|
+
\`camera_inputs\` for local sources. \`camera_launch\` attaches input when launching the app;
|
|
6155
|
+
\`camera_zero_setup\` configures attachment for another launcher. \`camera_status\` reports
|
|
6156
|
+
available source/decoder status, which does not establish the app's scanner result.
|
|
6157
|
+
Use \`camera_stop\` when finished. These tools operate locally without an in-app Buoy
|
|
6158
|
+
integration, but require their supported host, simulator and account setup.
|
|
6159
|
+
|
|
6160
|
+
\`screenshot_component\` locates a supported component by testID/name and attempts a crop.
|
|
6161
|
+
It requires the MCP server on the same Mac as the booted iOS Simulator and the component
|
|
6162
|
+
inspection setup. Inspect the result before using it as visual evidence.
|
|
6163
|
+
|
|
6164
|
+
For other supported operations, use \`list_tool_actions\`, \`get_snapshot\` or \`call_action\`.
|
|
6165
|
+
Snapshots describe available current state and can be large; request only needed data.
|
|
6166
|
+
|
|
6167
|
+
After a behavior change, repeat the affected interaction and check the expected result.
|
|
6168
|
+
Include restart or persistence checks when relevant to the change. Use appropriate static
|
|
6169
|
+
checks for documentation-only work. Report actual checks, observations and remaining limits.
|
|
6170
|
+
BUOY_VERIFY reminders track selected edits and tool calls; clearing a reminder is not a
|
|
6171
|
+
test result. Stay within the user's authorized task and preserve existing project work.`;
|
|
5899
6172
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
5900
6173
|
"measure_renders",
|
|
5901
6174
|
"scenario_action",
|
|
@@ -5968,9 +6241,9 @@ var deviceIdField = {
|
|
|
5968
6241
|
|
|
5969
6242
|
// src/init.ts
|
|
5970
6243
|
var import_node_child_process3 = require("child_process");
|
|
5971
|
-
var
|
|
5972
|
-
var
|
|
5973
|
-
var
|
|
6244
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
6245
|
+
var import_node_os5 = __toESM(require("os"));
|
|
6246
|
+
var import_node_path6 = __toESM(require("path"));
|
|
5974
6247
|
function defaultEnv() {
|
|
5975
6248
|
return { BUOY_VERIFY: "auto" };
|
|
5976
6249
|
}
|
|
@@ -5978,34 +6251,34 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5978
6251
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5979
6252
|
function runInit(opts = {}) {
|
|
5980
6253
|
const cwd = opts.cwd ?? process.cwd();
|
|
5981
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
6254
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path6.default.resolve(__dirname, "../skill");
|
|
5982
6255
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5983
6256
|
const entry = resolveEntry(opts, log);
|
|
5984
6257
|
const done = [];
|
|
5985
|
-
upsertMcpConfig(
|
|
5986
|
-
|
|
5987
|
-
upsertMcpConfig(
|
|
5988
|
-
if (
|
|
5989
|
-
upsertMcpConfig(
|
|
6258
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
6259
|
+
import_node_fs6.default.mkdirSync(import_node_path6.default.join(cwd, ".cursor"), { recursive: true });
|
|
6260
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
6261
|
+
if (import_node_fs6.default.existsSync(import_node_path6.default.join(cwd, ".vscode"))) {
|
|
6262
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5990
6263
|
}
|
|
5991
6264
|
installSkills(skillSrcDir, cwd, done);
|
|
5992
|
-
appendDebuggingGuide(
|
|
5993
|
-
appendDebuggingGuide(
|
|
5994
|
-
log("Buoy MCP
|
|
6265
|
+
appendDebuggingGuide(import_node_path6.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
6266
|
+
appendDebuggingGuide(import_node_path6.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
6267
|
+
log("Buoy MCP configuration written:");
|
|
5995
6268
|
for (const line of done) log(" \u2022 " + line);
|
|
5996
6269
|
log("");
|
|
5997
6270
|
log("Next steps:");
|
|
5998
6271
|
log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
|
|
5999
|
-
log(" 2.
|
|
6000
|
-
log(
|
|
6272
|
+
log(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");
|
|
6273
|
+
log(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");
|
|
6001
6274
|
if (entry.command === "npx") {
|
|
6002
6275
|
log("");
|
|
6003
|
-
log(`
|
|
6004
|
-
log(`
|
|
6276
|
+
log(`The editor launches the server with \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
|
|
6277
|
+
log(`Registry access may be needed. Re-run \`npx -y ${PACKAGE_NAME}@latest init\` to refresh configuration.`);
|
|
6005
6278
|
} else if (!opts.cliPath) {
|
|
6006
6279
|
log("");
|
|
6007
6280
|
log(`This uses a pinned local install (no network on startup). To update it later,`);
|
|
6008
|
-
log(`
|
|
6281
|
+
log(`To update the local package, run: npx -y ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}@latest init --local`);
|
|
6009
6282
|
}
|
|
6010
6283
|
}
|
|
6011
6284
|
function resolveEntry(opts, log) {
|
|
@@ -6032,7 +6305,7 @@ function npxEntry() {
|
|
|
6032
6305
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
6033
6306
|
}
|
|
6034
6307
|
function localEntry(opts, log) {
|
|
6035
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
6308
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path6.default.join(import_node_os5.default.homedir(), ".buoy-mcp");
|
|
6036
6309
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
6037
6310
|
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
6038
6311
|
if (cliPath2) {
|
|
@@ -6043,7 +6316,7 @@ function localEntry(opts, log) {
|
|
|
6043
6316
|
}
|
|
6044
6317
|
function installLocalCopy(installDir, registry, log) {
|
|
6045
6318
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
6046
|
-
|
|
6319
|
+
import_node_fs6.default.mkdirSync(installDir, { recursive: true });
|
|
6047
6320
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
6048
6321
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
6049
6322
|
const res = (0, import_node_child_process3.spawnSync)(
|
|
@@ -6066,8 +6339,8 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
6066
6339
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
6067
6340
|
return null;
|
|
6068
6341
|
}
|
|
6069
|
-
const cliPath2 =
|
|
6070
|
-
if (!
|
|
6342
|
+
const cliPath2 = import_node_path6.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
6343
|
+
if (!import_node_fs6.default.existsSync(cliPath2)) {
|
|
6071
6344
|
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
6072
6345
|
return null;
|
|
6073
6346
|
}
|
|
@@ -6119,129 +6392,109 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
6119
6392
|
`;
|
|
6120
6393
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
6121
6394
|
let json3 = {};
|
|
6122
|
-
if (
|
|
6395
|
+
if (import_node_fs6.default.existsSync(file)) {
|
|
6123
6396
|
try {
|
|
6124
|
-
json3 = JSON.parse(
|
|
6397
|
+
json3 = JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
|
|
6125
6398
|
} catch {
|
|
6126
|
-
|
|
6399
|
+
import_node_fs6.default.copyFileSync(file, file + ".bak");
|
|
6127
6400
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
6128
6401
|
json3 = {};
|
|
6129
6402
|
}
|
|
6130
6403
|
} else {
|
|
6131
|
-
|
|
6404
|
+
import_node_fs6.default.mkdirSync(import_node_path6.default.dirname(file), { recursive: true });
|
|
6132
6405
|
}
|
|
6133
6406
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
6134
6407
|
const existed = "buoy" in servers;
|
|
6135
6408
|
servers.buoy = entry;
|
|
6136
6409
|
json3[key] = servers;
|
|
6137
|
-
|
|
6410
|
+
import_node_fs6.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
6138
6411
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
6139
6412
|
}
|
|
6140
6413
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
6141
6414
|
var DEBUGGING_GUIDE = `${GUIDE_HEADING}
|
|
6142
6415
|
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
- \`run_flow
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
- \`
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
- \`
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
Saying "fixed" is a claim about how the app behaves, so check it the same way
|
|
6176
|
-
you'd check anything else about the app \u2014 by looking at it. Re-run the exact
|
|
6177
|
-
interaction that was broken and report what the numbers did:
|
|
6178
|
-
|
|
6179
|
-
- \`measure_renders\` with the same \`steps\` and \`compareToPrevious: true\` for a
|
|
6180
|
-
performance change \u2014 it prints the before/after delta and flags a change that
|
|
6181
|
-
made things worse.
|
|
6182
|
-
- \`run_flow\` re-driving the broken screen for a behaviour change.
|
|
6183
|
-
|
|
6184
|
-
Two things this catches that reading cannot: a fix that is right but incomplete
|
|
6185
|
-
(the value was wrong in three places and you corrected one), and a fix that
|
|
6186
|
-
moved the cost instead of removing it \u2014 the usual failure mode for re-render
|
|
6187
|
-
work, where a page gets faster to type in and slower to tap.
|
|
6188
|
-
|
|
6189
|
-
If the app changed since anything was last checked, Buoy will say so in a tool
|
|
6190
|
-
result. Whether that's worth acting on is your call \u2014 a doc edit isn't worth a
|
|
6191
|
-
device check, a state-management change is. Set \`BUOY_VERIFY\` in the MCP config
|
|
6192
|
-
to \`always\` (repeat until checked) or \`never\` (silent) to change how often it
|
|
6193
|
-
mentions it.
|
|
6416
|
+
Use Buoy to inspect a connected app when runtime evidence helps the task.
|
|
6417
|
+
Complete account, tool integration and connection setup first. MCP data and
|
|
6418
|
+
action access requires Pro. Follow the user's scope and repository instructions.
|
|
6419
|
+
|
|
6420
|
+
- \`get_triage\`: summarize available device and tool observations, then choose
|
|
6421
|
+
a relevant tool. Missing data is not proof that no problem occurred.
|
|
6422
|
+
- \`run_flow\`: run supported tap, type, wait and assertion steps to reproduce
|
|
6423
|
+
a failure or check a change. Review the flow before running it.
|
|
6424
|
+
- \`get_events\`: inspect retained events from connected tools.
|
|
6425
|
+
- \`get_network_requests\`: inspect captured requests and responses. Capture
|
|
6426
|
+
depends on installed hooks, transport and when recording began.
|
|
6427
|
+
- \`network_conditions\`: take the app offline or add latency to check error
|
|
6428
|
+
and retry paths. Development builds only. Restore \`normal\` when finished.
|
|
6429
|
+
- \`get_console\`: inspect retained console and error output. A terminated or
|
|
6430
|
+
disconnected app may not provide new data.
|
|
6431
|
+
- \`get_storage\`: inspect registered storage backends and available keys.
|
|
6432
|
+
- \`reload_app\`: reload where supported. Confirm the device remains connected;
|
|
6433
|
+
a reload does not guarantee recovery from a crash.
|
|
6434
|
+
- \`get_snapshot\`: inspect the available state of a specific tool.
|
|
6435
|
+
|
|
6436
|
+
After a behavior change, repeat the failing interaction when the environment
|
|
6437
|
+
allows it. Use relevant tests and report what was actually checked. For a
|
|
6438
|
+
performance change, compare \`measure_renders\` runs with the same steps and
|
|
6439
|
+
\`compareToPrevious: true\`; inspect the measurements and their limitations.
|
|
6440
|
+
Use \`run_flow\` for supported UI checks. Do not claim a fix was verified when
|
|
6441
|
+
only source review or a build was performed.
|
|
6442
|
+
|
|
6443
|
+
\`BUOY_VERIFY\` controls verification reminders: \`auto\` uses edit-based
|
|
6444
|
+
reminders, \`always\` repeats until checked, and \`never\` disables them.
|
|
6445
|
+
A reminder is not a test result. Choose checks appropriate to the change;
|
|
6446
|
+
prose-only edits do not require running device actions.
|
|
6194
6447
|
`;
|
|
6195
6448
|
function appendDebuggingGuide(file, cwd, done) {
|
|
6196
6449
|
let existing = "";
|
|
6197
|
-
if (
|
|
6450
|
+
if (import_node_fs6.default.existsSync(file)) {
|
|
6198
6451
|
try {
|
|
6199
|
-
existing =
|
|
6452
|
+
existing = import_node_fs6.default.readFileSync(file, "utf8");
|
|
6200
6453
|
} catch {
|
|
6201
6454
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
6202
6455
|
return;
|
|
6203
6456
|
}
|
|
6204
6457
|
if (existing.includes(GUIDE_HEADING)) {
|
|
6205
|
-
done.push(`${rel(file, cwd)} already has the "${GUIDE_HEADING}" block
|
|
6458
|
+
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.`);
|
|
6206
6459
|
return;
|
|
6207
6460
|
}
|
|
6208
6461
|
}
|
|
6209
6462
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6210
6463
|
try {
|
|
6211
|
-
|
|
6464
|
+
import_node_fs6.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
6212
6465
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
6213
6466
|
} catch {
|
|
6214
6467
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
6215
6468
|
}
|
|
6216
6469
|
}
|
|
6217
6470
|
function installSkills(skillSrcDir, cwd, done) {
|
|
6218
|
-
if (!
|
|
6471
|
+
if (!import_node_fs6.default.existsSync(skillSrcDir)) {
|
|
6219
6472
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
6220
6473
|
return;
|
|
6221
6474
|
}
|
|
6222
|
-
const skills =
|
|
6475
|
+
const skills = import_node_fs6.default.readdirSync(skillSrcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && import_node_fs6.default.existsSync(import_node_path6.default.join(skillSrcDir, e.name, "SKILL.md")));
|
|
6223
6476
|
if (skills.length === 0) {
|
|
6224
6477
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
6225
6478
|
return;
|
|
6226
6479
|
}
|
|
6227
6480
|
for (const skill of skills) {
|
|
6228
|
-
const from =
|
|
6229
|
-
const to =
|
|
6481
|
+
const from = import_node_path6.default.join(skillSrcDir, skill.name);
|
|
6482
|
+
const to = import_node_path6.default.join(cwd, ".claude", "skills", skill.name);
|
|
6230
6483
|
copyDir(from, to);
|
|
6231
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6484
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path6.default.join(to, "SKILL.md"), cwd)}`);
|
|
6232
6485
|
}
|
|
6233
6486
|
}
|
|
6234
6487
|
function copyDir(from, to) {
|
|
6235
|
-
|
|
6236
|
-
for (const entry of
|
|
6237
|
-
const s =
|
|
6238
|
-
const d =
|
|
6488
|
+
import_node_fs6.default.mkdirSync(to, { recursive: true });
|
|
6489
|
+
for (const entry of import_node_fs6.default.readdirSync(from, { withFileTypes: true })) {
|
|
6490
|
+
const s = import_node_path6.default.join(from, entry.name);
|
|
6491
|
+
const d = import_node_path6.default.join(to, entry.name);
|
|
6239
6492
|
if (entry.isDirectory()) copyDir(s, d);
|
|
6240
|
-
else
|
|
6493
|
+
else import_node_fs6.default.copyFileSync(s, d);
|
|
6241
6494
|
}
|
|
6242
6495
|
}
|
|
6243
6496
|
function rel(file, cwd) {
|
|
6244
|
-
const r =
|
|
6497
|
+
const r = import_node_path6.default.relative(cwd, file);
|
|
6245
6498
|
return r && !r.startsWith("..") ? r : file;
|
|
6246
6499
|
}
|
|
6247
6500
|
// Annotate the CommonJS export names for ESM import in node:
|