@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/cli.js
CHANGED
|
@@ -26,9 +26,177 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
28
28
|
|
|
29
|
+
// src/sync/NodeAccount.ts
|
|
30
|
+
var import_node_fs = require("fs");
|
|
31
|
+
var import_node_path = require("path");
|
|
32
|
+
var import_node_os = require("os");
|
|
33
|
+
var import_node_crypto = require("crypto");
|
|
34
|
+
var import_headless = require("@buoy-gg/license/headless");
|
|
35
|
+
var KEY_NAMES = ["BUOY_LICENSE_KEY", "BUOY_KEY", "EXPO_PUBLIC_BUOY_KEY", "VITE_BUOY_KEY", "REACT_APP_BUOY_KEY"];
|
|
36
|
+
function readProjectKey(projectDir, env = process.env) {
|
|
37
|
+
for (const name of KEY_NAMES) if (env[name] !== void 0) return env[name].trim().toUpperCase() || null;
|
|
38
|
+
for (const file of [".env.development.local", ".env.local", ".env"]) {
|
|
39
|
+
let contents;
|
|
40
|
+
try {
|
|
41
|
+
contents = (0, import_node_fs.readFileSync)((0, import_node_path.join)(projectDir, file), "utf8");
|
|
42
|
+
} catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const values = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
47
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);
|
|
48
|
+
if (!match) continue;
|
|
49
|
+
const raw = match[2].trim();
|
|
50
|
+
const quoted = raw.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);
|
|
51
|
+
values.set(match[1], quoted ? quoted[2] : raw.split("#")[0].trim());
|
|
52
|
+
}
|
|
53
|
+
for (const name of KEY_NAMES) if (values.has(name)) return values.get(name).trim().toUpperCase() || null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
var NodeAccount = class {
|
|
58
|
+
constructor(changed, opts = {}) {
|
|
59
|
+
this.changed = changed;
|
|
60
|
+
this.key = null;
|
|
61
|
+
this.cache = null;
|
|
62
|
+
this.timer = null;
|
|
63
|
+
this.generation = 0;
|
|
64
|
+
this.busy = false;
|
|
65
|
+
this.nextCheck = 0;
|
|
66
|
+
this.retry = 0;
|
|
67
|
+
this.lastAdmission = false;
|
|
68
|
+
this.hasAccess = () => (0, import_headless.resolveAccountAccess)({
|
|
69
|
+
initialized: true,
|
|
70
|
+
validating: false,
|
|
71
|
+
key: this.key,
|
|
72
|
+
tierSource: this.cache?.tier ? "key" : "none",
|
|
73
|
+
verifiedAt: this.cache?.cachedAt ?? null
|
|
74
|
+
}).status === "admitted";
|
|
75
|
+
this.projectDir = (0, import_node_path.resolve)(opts.projectDir ?? process.env.BUOY_PROJECT_DIR ?? process.cwd());
|
|
76
|
+
const project = (0, import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0, 20);
|
|
77
|
+
this.cacheFile = opts.cacheFile ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".buoy", `account-${project}.json`);
|
|
78
|
+
}
|
|
79
|
+
start() {
|
|
80
|
+
if (this.timer) return;
|
|
81
|
+
try {
|
|
82
|
+
const cached = JSON.parse((0, import_node_fs.readFileSync)(this.cacheFile, "utf8"));
|
|
83
|
+
if (cached.valid && (0, import_headless.verifyCacheSignature)(cached)) this.cache = cached;
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
void this.tick();
|
|
87
|
+
this.timer = setInterval(() => {
|
|
88
|
+
void this.tick();
|
|
89
|
+
}, 1e3);
|
|
90
|
+
this.timer.unref();
|
|
91
|
+
}
|
|
92
|
+
stop() {
|
|
93
|
+
++this.generation;
|
|
94
|
+
if (this.timer) clearInterval(this.timer);
|
|
95
|
+
this.timer = null;
|
|
96
|
+
this.key = null;
|
|
97
|
+
this.cache = null;
|
|
98
|
+
this.notify();
|
|
99
|
+
}
|
|
100
|
+
notify() {
|
|
101
|
+
const admitted = this.hasAccess();
|
|
102
|
+
if (admitted === this.lastAdmission) return;
|
|
103
|
+
this.lastAdmission = admitted;
|
|
104
|
+
this.changed();
|
|
105
|
+
}
|
|
106
|
+
async tick() {
|
|
107
|
+
const selected = readProjectKey(this.projectDir);
|
|
108
|
+
if (selected !== this.key) {
|
|
109
|
+
++this.generation;
|
|
110
|
+
this.key = selected;
|
|
111
|
+
if (this.cache?.licenseKey !== selected) this.cache = null;
|
|
112
|
+
this.nextCheck = 0;
|
|
113
|
+
this.retry = 0;
|
|
114
|
+
if (!selected) this.removeCache();
|
|
115
|
+
}
|
|
116
|
+
this.notify();
|
|
117
|
+
if (!selected || this.busy || Date.now() < this.nextCheck) return;
|
|
118
|
+
if (this.cache && this.hasAccess() && Date.now() - this.cache.lastValidatedAt < import_headless.LICENSE_LAUNCH_VALIDATION_TTL && this.nextCheck === 0) {
|
|
119
|
+
this.nextCheck = this.cache.lastValidatedAt + import_headless.LICENSE_LAUNCH_VALIDATION_TTL;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.busy = true;
|
|
123
|
+
const startedAt = this.generation;
|
|
124
|
+
try {
|
|
125
|
+
if ((0, import_headless.isBuoyToken)(selected)) {
|
|
126
|
+
const claims = (0, import_headless.verifyFreeToken)(selected);
|
|
127
|
+
if (!claims) {
|
|
128
|
+
this.cache = null;
|
|
129
|
+
this.removeCache();
|
|
130
|
+
} else {
|
|
131
|
+
const now2 = Date.now();
|
|
132
|
+
this.cache = (0, import_headless.signCache)({
|
|
133
|
+
licenseKey: selected,
|
|
134
|
+
licenseId: `token:${claims.accountHash}`,
|
|
135
|
+
valid: true,
|
|
136
|
+
tier: "free",
|
|
137
|
+
isPro: false,
|
|
138
|
+
expiresAt: null,
|
|
139
|
+
cachedAt: now2,
|
|
140
|
+
lastValidatedAt: now2
|
|
141
|
+
});
|
|
142
|
+
this.persistCache();
|
|
143
|
+
}
|
|
144
|
+
this.nextCheck = Date.now() + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const response = await (0, import_headless.validateLicense)(selected);
|
|
148
|
+
if (startedAt !== this.generation || readProjectKey(this.projectDir) !== selected) return;
|
|
149
|
+
const product = response.data?.relationships?.product?.data?.id;
|
|
150
|
+
const expired = response.meta.code === "EXPIRED" && ((0, import_headless.isRecognizedAccount)(response.data) || this.cache?.licenseKey === selected);
|
|
151
|
+
if (product && product !== import_headless.KEYGEN_CONFIG.productId || !expired && !(0, import_headless.isAcceptedValidation)(response)) {
|
|
152
|
+
this.cache = null;
|
|
153
|
+
this.removeCache();
|
|
154
|
+
this.nextCheck = Date.now() + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const tier = expired ? "free" : (0, import_headless.resolveTier)(response.data);
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
this.cache = (0, import_headless.signCache)({
|
|
160
|
+
licenseKey: selected,
|
|
161
|
+
licenseId: response.data?.id ?? "",
|
|
162
|
+
valid: true,
|
|
163
|
+
tier,
|
|
164
|
+
isPro: tier === "pro",
|
|
165
|
+
expiresAt: expired ? null : response.data.attributes.expiry,
|
|
166
|
+
cachedAt: now,
|
|
167
|
+
lastValidatedAt: now
|
|
168
|
+
});
|
|
169
|
+
this.persistCache();
|
|
170
|
+
this.retry = 0;
|
|
171
|
+
this.nextCheck = now + import_headless.LICENSE_REVALIDATION_INTERVAL;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (startedAt !== this.generation) return;
|
|
174
|
+
const delay6 = (0, import_headless.isTransientLicenseError)(error) ? import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++] : void 0;
|
|
175
|
+
this.nextCheck = Date.now() + (delay6 ?? import_headless.LICENSE_REVALIDATION_INTERVAL);
|
|
176
|
+
} finally {
|
|
177
|
+
this.busy = false;
|
|
178
|
+
this.notify();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
persistCache() {
|
|
182
|
+
try {
|
|
183
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(this.cacheFile), { recursive: true, mode: 448 });
|
|
184
|
+
(0, import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`, JSON.stringify(this.cache), { mode: 384 });
|
|
185
|
+
(0, import_node_fs.renameSync)(`${this.cacheFile}.tmp`, this.cacheFile);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
removeCache() {
|
|
190
|
+
try {
|
|
191
|
+
(0, import_node_fs.unlinkSync)(this.cacheFile);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
29
197
|
// src/sync/SyncClient.ts
|
|
30
198
|
var import_node_http = __toESM(require("http"));
|
|
31
|
-
var
|
|
199
|
+
var import_node_crypto2 = require("crypto");
|
|
32
200
|
var import_socket = require("socket.io-client");
|
|
33
201
|
var import_sync_broker2 = require("@buoy-gg/sync-broker");
|
|
34
202
|
|
|
@@ -77,6 +245,9 @@ function diagnoseCrashed(opts) {
|
|
|
77
245
|
var SyncClient = class {
|
|
78
246
|
constructor(opts = {}) {
|
|
79
247
|
this.socket = null;
|
|
248
|
+
this.admitted = false;
|
|
249
|
+
this.lockedDevices = /* @__PURE__ */ new Set();
|
|
250
|
+
this.hostAccount = null;
|
|
80
251
|
this.broker = null;
|
|
81
252
|
/** Latest capabilities announce per deviceId. */
|
|
82
253
|
this.capabilities = /* @__PURE__ */ new Map();
|
|
@@ -125,7 +296,10 @@ var SyncClient = class {
|
|
|
125
296
|
);
|
|
126
297
|
}
|
|
127
298
|
this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);
|
|
299
|
+
this.hostAccount = new NodeAccount(() => this.broker?.refreshAccess());
|
|
300
|
+
this.hostAccount.start();
|
|
128
301
|
this.broker = (0, import_sync_broker2.createBroker)({
|
|
302
|
+
canUseDesktop: this.hostAccount.hasAccess,
|
|
129
303
|
port: this.port,
|
|
130
304
|
onError: (e) => this.log(`broker error: ${e.code ?? e.message}`)
|
|
131
305
|
});
|
|
@@ -138,7 +312,7 @@ var SyncClient = class {
|
|
|
138
312
|
return mode;
|
|
139
313
|
}
|
|
140
314
|
connectSocket() {
|
|
141
|
-
return new Promise((
|
|
315
|
+
return new Promise((resolve2, reject) => {
|
|
142
316
|
const socket = (0, import_socket.io)(this.url, {
|
|
143
317
|
transports: ["websocket"],
|
|
144
318
|
query: { deviceName: DASHBOARD_NAME },
|
|
@@ -148,31 +322,59 @@ var SyncClient = class {
|
|
|
148
322
|
});
|
|
149
323
|
this.socket = socket;
|
|
150
324
|
let settled = false;
|
|
151
|
-
const
|
|
325
|
+
const onReady = () => {
|
|
152
326
|
if (settled) return;
|
|
153
327
|
settled = true;
|
|
154
328
|
this.log("connected to broker");
|
|
155
|
-
|
|
329
|
+
resolve2();
|
|
156
330
|
};
|
|
157
|
-
|
|
158
|
-
|
|
331
|
+
const readinessTimeout = setTimeout(() => {
|
|
332
|
+
if (settled) return;
|
|
333
|
+
settled = true;
|
|
334
|
+
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."));
|
|
335
|
+
}, 5e3);
|
|
336
|
+
socket.on("buoy:access", (message) => {
|
|
337
|
+
const ready = !!message && typeof message === "object" && message.protocol === 1 && message.ready === true;
|
|
338
|
+
this.setAdmission(ready);
|
|
339
|
+
if (ready) {
|
|
340
|
+
clearTimeout(readinessTimeout);
|
|
341
|
+
this.rearmWatches();
|
|
342
|
+
onReady();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
socket.on("connect", () => {
|
|
346
|
+
socket.emit("buoy:access:request");
|
|
347
|
+
});
|
|
348
|
+
socket.on("disconnect", () => this.setAdmission(false, true));
|
|
159
349
|
socket.once("connect_error", (err) => {
|
|
160
350
|
if (settled) return;
|
|
161
351
|
settled = true;
|
|
352
|
+
clearTimeout(readinessTimeout);
|
|
162
353
|
reject(new Error(`failed to connect to broker at ${this.url}: ${err.message}`));
|
|
163
354
|
});
|
|
164
355
|
socket.on("devtool-capabilities", (msg) => {
|
|
356
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
165
357
|
this.capabilities.set(msg.persistentDeviceId, msg);
|
|
166
358
|
void this.learnDevServerOrigin(msg.persistentDeviceId);
|
|
167
359
|
});
|
|
168
360
|
socket.on("all-devices-update", (list) => {
|
|
361
|
+
if (!this.admitted) return;
|
|
169
362
|
if (!Array.isArray(list)) return;
|
|
363
|
+
let deviceReadmitted = false;
|
|
364
|
+
for (const device of list) {
|
|
365
|
+
if (device.deviceId && device.tier === "locked") this.lockDevice(String(device.deviceId));
|
|
366
|
+
else if (device.deviceId && (device.tier === "free" || device.tier === "pro")) {
|
|
367
|
+
deviceReadmitted = this.lockedDevices.delete(String(device.deviceId)) || deviceReadmitted;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
170
370
|
this.devices = list;
|
|
171
371
|
const now = Date.now();
|
|
172
372
|
for (const d of list) if (d.deviceId) this.lastSeen.set(d.deviceId, now);
|
|
173
373
|
this.pruneStaleDevices();
|
|
374
|
+
if (deviceReadmitted) this.rearmWatches();
|
|
174
375
|
});
|
|
175
376
|
socket.on("devtool-sync", (msg) => {
|
|
377
|
+
if (!this.admitted || this.lockedDevices.has(msg.persistentDeviceId)) return;
|
|
176
378
|
const key = snapKey(msg.persistentDeviceId, msg.toolId);
|
|
177
379
|
this.snapshots.set(key, msg.payload);
|
|
178
380
|
this.snapshotAt.set(key, Date.now());
|
|
@@ -183,6 +385,7 @@ var SyncClient = class {
|
|
|
183
385
|
}
|
|
184
386
|
});
|
|
185
387
|
socket.on("devtool-action-result", (msg) => {
|
|
388
|
+
if (!this.admitted) return;
|
|
186
389
|
const pending = this.pendingActions.get(msg.requestId);
|
|
187
390
|
if (!pending) return;
|
|
188
391
|
this.pendingActions.delete(msg.requestId);
|
|
@@ -200,6 +403,55 @@ var SyncClient = class {
|
|
|
200
403
|
* with every simulator erase and fresh dev client, and each one can hold
|
|
201
404
|
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
202
405
|
*/
|
|
406
|
+
requireAdmission() {
|
|
407
|
+
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.");
|
|
408
|
+
}
|
|
409
|
+
setAdmission(ready, preserveWatchIntent = false) {
|
|
410
|
+
this.admitted = ready;
|
|
411
|
+
if (ready) return;
|
|
412
|
+
this.devices = [];
|
|
413
|
+
this.lockedDevices.clear();
|
|
414
|
+
this.capabilities.clear();
|
|
415
|
+
this.snapshots.clear();
|
|
416
|
+
this.snapshotAt.clear();
|
|
417
|
+
this.devServerOrigins.clear();
|
|
418
|
+
this.originProbed.clear();
|
|
419
|
+
this.health.clear();
|
|
420
|
+
this.lastSeen.clear();
|
|
421
|
+
if (!preserveWatchIntent) this.watching.clear();
|
|
422
|
+
for (const pending of this.pendingActions.values()) {
|
|
423
|
+
clearTimeout(pending.timer);
|
|
424
|
+
pending.reject(new Error("Buoy account access ended while the action was pending."));
|
|
425
|
+
}
|
|
426
|
+
this.pendingActions.clear();
|
|
427
|
+
for (const waiters of this.snapshotWaiters.values()) for (const waiter of waiters) waiter(void 0);
|
|
428
|
+
this.snapshotWaiters.clear();
|
|
429
|
+
if (this.socket) this.socket.sendBuffer = [];
|
|
430
|
+
}
|
|
431
|
+
requireDeviceAccount(deviceId) {
|
|
432
|
+
if (this.lockedDevices.has(deviceId)) throw new Error("The device's Buoy account access ended. Sign in and verify its key.");
|
|
433
|
+
}
|
|
434
|
+
lockDevice(deviceId) {
|
|
435
|
+
this.lockedDevices.add(deviceId);
|
|
436
|
+
this.capabilities.delete(deviceId);
|
|
437
|
+
this.health.delete(deviceId);
|
|
438
|
+
this.devServerOrigins.delete(deviceId);
|
|
439
|
+
this.originProbed.delete(deviceId);
|
|
440
|
+
const prefix = `${deviceId}::`;
|
|
441
|
+
for (const key of this.snapshots.keys()) if (key.startsWith(prefix)) {
|
|
442
|
+
this.snapshots.delete(key);
|
|
443
|
+
this.snapshotAt.delete(key);
|
|
444
|
+
}
|
|
445
|
+
for (const [key, waiters] of this.snapshotWaiters) if (key.startsWith(prefix)) {
|
|
446
|
+
this.snapshotWaiters.delete(key);
|
|
447
|
+
for (const waiter of waiters) waiter(void 0);
|
|
448
|
+
}
|
|
449
|
+
for (const [id, pending] of this.pendingActions) if (pending.target === deviceId || pending.target === ALL_DEVICES) {
|
|
450
|
+
clearTimeout(pending.timer);
|
|
451
|
+
this.pendingActions.delete(id);
|
|
452
|
+
pending.reject(new Error("Device account access ended while the action was pending."));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
203
455
|
pruneStaleDevices() {
|
|
204
456
|
const cutoff = Date.now() - DEVICE_RETENTION_MS;
|
|
205
457
|
for (const [deviceId, seen] of this.lastSeen) {
|
|
@@ -224,6 +476,9 @@ var SyncClient = class {
|
|
|
224
476
|
}
|
|
225
477
|
/** Disconnect and tear down a spawned broker, if any. */
|
|
226
478
|
async stop() {
|
|
479
|
+
this.setAdmission(false);
|
|
480
|
+
this.hostAccount?.stop();
|
|
481
|
+
this.hostAccount = null;
|
|
227
482
|
for (const [, p] of this.pendingActions) {
|
|
228
483
|
clearTimeout(p.timer);
|
|
229
484
|
p.reject(new Error("SyncClient stopped"));
|
|
@@ -243,6 +498,7 @@ var SyncClient = class {
|
|
|
243
498
|
// ----------------------------------------------------------------
|
|
244
499
|
/** All devices the broker knows about, merged with their advertised tools. */
|
|
245
500
|
listDevices() {
|
|
501
|
+
if (!this.admitted) return [];
|
|
246
502
|
return this.devices.filter((d) => d.deviceName !== DASHBOARD_NAME && d.deviceId).map((d) => {
|
|
247
503
|
const caps = this.capabilities.get(d.deviceId);
|
|
248
504
|
return {
|
|
@@ -251,7 +507,7 @@ var SyncClient = class {
|
|
|
251
507
|
platform: d.platform,
|
|
252
508
|
isConnected: d.isConnected !== false,
|
|
253
509
|
isPro: d.isPro === true,
|
|
254
|
-
tier: d.tier
|
|
510
|
+
tier: d.tier === "free" || d.tier === "pro" ? d.tier : d.tier === void 0 && d.isPro === true ? "pro" : "locked",
|
|
255
511
|
collision: d.collision === true,
|
|
256
512
|
tools: caps?.tools ?? []
|
|
257
513
|
};
|
|
@@ -274,9 +530,19 @@ var SyncClient = class {
|
|
|
274
530
|
* or many, an informative error is thrown (callers can pass "All" explicitly).
|
|
275
531
|
*/
|
|
276
532
|
resolveDevice(deviceId) {
|
|
277
|
-
|
|
278
|
-
|
|
533
|
+
this.requireAdmission();
|
|
534
|
+
if (deviceId) {
|
|
535
|
+
this.requireDeviceAccount(deviceId);
|
|
536
|
+
return deviceId;
|
|
537
|
+
}
|
|
538
|
+
const allConnected = this.connectedDevices();
|
|
539
|
+
const connected = allConnected.filter((d) => !this.lockedDevices.has(d.deviceId));
|
|
279
540
|
if (connected.length === 0) {
|
|
541
|
+
if (allConnected.length > 0) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`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.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
280
546
|
throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.");
|
|
281
547
|
}
|
|
282
548
|
if (connected.length === 1) return connected[0].deviceId;
|
|
@@ -302,9 +568,10 @@ var SyncClient = class {
|
|
|
302
568
|
* is blocked.
|
|
303
569
|
*/
|
|
304
570
|
requireProDevice(deviceId) {
|
|
571
|
+
this.requireAdmission();
|
|
305
572
|
const tierNote = (dev2) => {
|
|
306
573
|
if (!dev2) return "";
|
|
307
|
-
return dev2.tier === "
|
|
574
|
+
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.`;
|
|
308
575
|
};
|
|
309
576
|
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
310
577
|
dev2
|
|
@@ -419,6 +686,7 @@ var SyncClient = class {
|
|
|
419
686
|
}
|
|
420
687
|
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
421
688
|
snapshotAgeMs(toolId, deviceId) {
|
|
689
|
+
this.requireAdmission();
|
|
422
690
|
const at = this.snapshotAt.get(snapKey(deviceId, toolId));
|
|
423
691
|
return at ? Date.now() - at : null;
|
|
424
692
|
}
|
|
@@ -454,6 +722,7 @@ var SyncClient = class {
|
|
|
454
722
|
}
|
|
455
723
|
/** The dev-server origin last reported by this device, if any. */
|
|
456
724
|
devServerOrigin(deviceId) {
|
|
725
|
+
this.requireAdmission();
|
|
457
726
|
const known = this.devServerOrigins.get(deviceId);
|
|
458
727
|
if (known) return known;
|
|
459
728
|
const snap = this.snapshots.get(snapKey(deviceId, "app"));
|
|
@@ -487,6 +756,7 @@ var SyncClient = class {
|
|
|
487
756
|
this.sendWatch(toolId, targetDeviceId);
|
|
488
757
|
}
|
|
489
758
|
sendWatch(toolId, targetDeviceId) {
|
|
759
|
+
if (!this.admitted || !this.socket?.connected || this.lockedDevices.has(targetDeviceId)) return;
|
|
490
760
|
this.socket?.emit("devtool-watch", {
|
|
491
761
|
toolId,
|
|
492
762
|
targetDeviceId,
|
|
@@ -512,10 +782,12 @@ var SyncClient = class {
|
|
|
512
782
|
unwatch(toolId, targetDeviceId) {
|
|
513
783
|
const key = snapKey(targetDeviceId, toolId);
|
|
514
784
|
if (!this.watching.delete(key)) return;
|
|
515
|
-
this.socket?.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
785
|
+
if (this.admitted && this.socket?.connected) this.socket.emit("devtool-watch", { toolId, targetDeviceId, watching: false });
|
|
516
786
|
}
|
|
517
787
|
/** Last snapshot received for a tool/device, if any. */
|
|
518
788
|
cachedSnapshot(toolId, deviceId) {
|
|
789
|
+
this.requireAdmission();
|
|
790
|
+
this.requireDeviceAccount(deviceId);
|
|
519
791
|
return this.snapshots.get(snapKey(deviceId, toolId));
|
|
520
792
|
}
|
|
521
793
|
/**
|
|
@@ -543,6 +815,8 @@ var SyncClient = class {
|
|
|
543
815
|
const next = this.waitForSnapshot(key, budget);
|
|
544
816
|
this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
|
|
545
817
|
const payload = await next;
|
|
818
|
+
this.requireAdmission();
|
|
819
|
+
this.requireDeviceAccount(target);
|
|
546
820
|
if (payload !== void 0) {
|
|
547
821
|
if (!opts.silent) this.noteOk(target);
|
|
548
822
|
return { deviceId: target, payload };
|
|
@@ -563,16 +837,16 @@ var SyncClient = class {
|
|
|
563
837
|
return `\u26A0\uFE0F Served from the last sync (${ageText}) \u2014 ${this.unresponsiveNote(deviceId)}`;
|
|
564
838
|
}
|
|
565
839
|
waitForSnapshot(key, timeoutMs) {
|
|
566
|
-
return new Promise((
|
|
840
|
+
return new Promise((resolve2) => {
|
|
567
841
|
const waiters = this.snapshotWaiters.get(key) ?? [];
|
|
568
842
|
const timer = setTimeout(() => {
|
|
569
843
|
const remaining = (this.snapshotWaiters.get(key) ?? []).filter((w) => w !== onSnap);
|
|
570
844
|
this.snapshotWaiters.set(key, remaining);
|
|
571
|
-
|
|
845
|
+
resolve2(void 0);
|
|
572
846
|
}, timeoutMs);
|
|
573
847
|
const onSnap = (payload) => {
|
|
574
848
|
clearTimeout(timer);
|
|
575
|
-
|
|
849
|
+
resolve2(payload);
|
|
576
850
|
};
|
|
577
851
|
waiters.push(onSnap);
|
|
578
852
|
this.snapshotWaiters.set(key, waiters);
|
|
@@ -586,11 +860,12 @@ var SyncClient = class {
|
|
|
586
860
|
* target one device, or "All" to broadcast (resolves with the first responder).
|
|
587
861
|
*/
|
|
588
862
|
async callAction(toolId, action, params, deviceId, opts = {}) {
|
|
863
|
+
this.requireAdmission();
|
|
589
864
|
const target = deviceId === ALL_DEVICES ? ALL_DEVICES : this.resolveDevice(deviceId);
|
|
590
865
|
if (!this.socket) throw new Error("SyncClient is not connected");
|
|
591
|
-
const requestId = (0,
|
|
866
|
+
const requestId = (0, import_node_crypto2.randomUUID)();
|
|
592
867
|
const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target, toolId, action);
|
|
593
|
-
const result = await new Promise((
|
|
868
|
+
const result = await new Promise((resolve2, reject) => {
|
|
594
869
|
const timer = setTimeout(() => {
|
|
595
870
|
this.pendingActions.delete(requestId);
|
|
596
871
|
if (target !== ALL_DEVICES && !opts.silent) this.noteTimeout(target);
|
|
@@ -600,7 +875,7 @@ var SyncClient = class {
|
|
|
600
875
|
)
|
|
601
876
|
);
|
|
602
877
|
}, timeoutMs);
|
|
603
|
-
this.pendingActions.set(requestId, { resolve, reject, timer });
|
|
878
|
+
this.pendingActions.set(requestId, { resolve: resolve2, reject, timer, target });
|
|
604
879
|
this.socket.emit("devtool-action", {
|
|
605
880
|
requestId,
|
|
606
881
|
toolId,
|
|
@@ -618,16 +893,16 @@ var SyncClient = class {
|
|
|
618
893
|
};
|
|
619
894
|
var snapKey = (deviceId, toolId) => `${deviceId}::${toolId}`;
|
|
620
895
|
function probeBroker(url, timeoutMs = 800) {
|
|
621
|
-
return new Promise((
|
|
896
|
+
return new Promise((resolve2) => {
|
|
622
897
|
const req = import_node_http.default.get(url, (res) => {
|
|
623
898
|
res.resume();
|
|
624
|
-
|
|
899
|
+
resolve2((res.statusCode ?? 0) > 0);
|
|
625
900
|
});
|
|
626
901
|
req.setTimeout(timeoutMs, () => {
|
|
627
902
|
req.destroy();
|
|
628
|
-
|
|
903
|
+
resolve2(false);
|
|
629
904
|
});
|
|
630
|
-
req.on("error", () =>
|
|
905
|
+
req.on("error", () => resolve2(false));
|
|
631
906
|
});
|
|
632
907
|
}
|
|
633
908
|
async function waitForBroker(url, attempts = 20) {
|
|
@@ -647,19 +922,19 @@ var import_zod26 = require("zod");
|
|
|
647
922
|
var import_zod = require("zod");
|
|
648
923
|
|
|
649
924
|
// src/update/checkForUpdate.ts
|
|
650
|
-
var
|
|
651
|
-
var
|
|
652
|
-
var
|
|
925
|
+
var import_node_fs2 = require("fs");
|
|
926
|
+
var import_node_os2 = __toESM(require("os"));
|
|
927
|
+
var import_node_path2 = __toESM(require("path"));
|
|
653
928
|
|
|
654
929
|
// src/version.ts
|
|
655
|
-
var VERSION = "7.0.
|
|
930
|
+
var VERSION = "7.0.38" ? "7.0.38" : "0.0.0-dev";
|
|
656
931
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
657
932
|
|
|
658
933
|
// src/update/checkForUpdate.ts
|
|
659
934
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
660
935
|
var FETCH_TIMEOUT_MS = 1500;
|
|
661
936
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
662
|
-
var CACHE_FILE =
|
|
937
|
+
var CACHE_FILE = import_node_path2.default.join(import_node_os2.default.tmpdir(), "buoy-mcp-update-check.json");
|
|
663
938
|
function isNewer(a, b) {
|
|
664
939
|
const parse = (v) => v.split("-")[0].split(".").map((n) => Number(n) || 0);
|
|
665
940
|
const pa = parse(a);
|
|
@@ -674,7 +949,7 @@ function isNewer(a, b) {
|
|
|
674
949
|
}
|
|
675
950
|
async function readCache() {
|
|
676
951
|
try {
|
|
677
|
-
const raw = await
|
|
952
|
+
const raw = await import_node_fs2.promises.readFile(CACHE_FILE, "utf8");
|
|
678
953
|
const parsed = JSON.parse(raw);
|
|
679
954
|
if (typeof parsed.checkedAt === "number" && typeof parsed.latest === "string") {
|
|
680
955
|
return parsed;
|
|
@@ -692,7 +967,7 @@ async function fetchLatest(now) {
|
|
|
692
967
|
const body = await res.json();
|
|
693
968
|
const latest = typeof body.version === "string" ? body.version : null;
|
|
694
969
|
if (latest) {
|
|
695
|
-
await
|
|
970
|
+
await import_node_fs2.promises.writeFile(CACHE_FILE, JSON.stringify({ checkedAt: now, latest })).catch(() => {
|
|
696
971
|
});
|
|
697
972
|
}
|
|
698
973
|
return latest;
|
|
@@ -745,7 +1020,7 @@ function registerGenericTools(server, sync) {
|
|
|
745
1020
|
}
|
|
746
1021
|
const connected = devices.filter((d) => d.isConnected);
|
|
747
1022
|
const anyPro = connected.some((d) => d.isPro);
|
|
748
|
-
const anySignedIn = connected.some((d) => d.tier !== "
|
|
1023
|
+
const anySignedIn = connected.some((d) => d.tier !== "locked");
|
|
749
1024
|
const collided = connected.filter((d) => d.collision);
|
|
750
1025
|
return json({
|
|
751
1026
|
devices,
|
|
@@ -755,7 +1030,7 @@ function registerGenericTools(server, sync) {
|
|
|
755
1030
|
...anyPro ? {} : {
|
|
756
1031
|
// A headless build has no on-device UI, so "upgrade in the app"
|
|
757
1032
|
// cannot be followed there — name the `licenseKey` prop too.
|
|
758
|
-
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
|
|
1033
|
+
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."
|
|
759
1034
|
},
|
|
760
1035
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
761
1036
|
});
|
|
@@ -1110,7 +1385,7 @@ function formatRankingMarkdown(ranking) {
|
|
|
1110
1385
|
].filter(Boolean).join(" \xB7 ");
|
|
1111
1386
|
if (leaderLine) lines.push(`**Per-metric best:** ${leaderLine}`);
|
|
1112
1387
|
lines.push(
|
|
1113
|
-
ranking.balanced.length ? `**
|
|
1388
|
+
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.`
|
|
1114
1389
|
);
|
|
1115
1390
|
if (ranking.flagged.length) {
|
|
1116
1391
|
lines.push(
|
|
@@ -1297,7 +1572,7 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1297
1572
|
server.registerTool(
|
|
1298
1573
|
"run_benchmark_batch",
|
|
1299
1574
|
{
|
|
1300
|
-
description: "Run a perf-monitor benchmark batch and return
|
|
1575
|
+
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.",
|
|
1301
1576
|
inputSchema: {
|
|
1302
1577
|
targetRoute: import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),
|
|
1303
1578
|
cases: import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),
|
|
@@ -1308,10 +1583,10 @@ function registerPerfMonitorTools(server, sync) {
|
|
|
1308
1583
|
coolDownMs: import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),
|
|
1309
1584
|
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),
|
|
1310
1585
|
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),
|
|
1311
|
-
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave
|
|
1586
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),
|
|
1312
1587
|
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),
|
|
1313
1588
|
captureRenders: import_zod3.z.boolean().optional().describe(
|
|
1314
|
-
"Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds
|
|
1589
|
+
"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."
|
|
1315
1590
|
),
|
|
1316
1591
|
deviceId: import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),
|
|
1317
1592
|
maxWaitMs: import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")
|
|
@@ -1505,7 +1780,7 @@ ${JSON.stringify(ranking, null, 2)}
|
|
|
1505
1780
|
server.registerTool(
|
|
1506
1781
|
"compare_reports",
|
|
1507
1782
|
{
|
|
1508
|
-
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
|
|
1783
|
+
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.",
|
|
1509
1784
|
inputSchema: {
|
|
1510
1785
|
reportIdA: import_zod3.z.string().describe("First (baseline) report id."),
|
|
1511
1786
|
reportIdB: import_zod3.z.string().describe("Second report id to compare against the baseline."),
|
|
@@ -1574,7 +1849,7 @@ ${JSON.stringify(settings, null, 2)}
|
|
|
1574
1849
|
navTimeoutMs: import_zod3.z.number().optional().describe("Max wait for navigation per run (ms)."),
|
|
1575
1850
|
runsPerCase: import_zod3.z.number().optional().describe("Runs per page; median is the canonical result (1\u201310)."),
|
|
1576
1851
|
discardWarmupRuns: import_zod3.z.number().optional().describe("Discard first N runs of each page before the median."),
|
|
1577
|
-
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave
|
|
1852
|
+
shuffleCases: import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias."),
|
|
1578
1853
|
reloadBetweenCases: import_zod3.z.boolean().optional().describe("Fully reload JS between pages (needs <AutomationResumer/> mounted)."),
|
|
1579
1854
|
captureRenders: import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run."),
|
|
1580
1855
|
deviceId: deviceIdInput
|
|
@@ -1639,7 +1914,7 @@ function registerEventsTools(server, sync) {
|
|
|
1639
1914
|
server.registerTool(
|
|
1640
1915
|
"get_events",
|
|
1641
1916
|
{
|
|
1642
|
-
description: "
|
|
1917
|
+
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.",
|
|
1643
1918
|
inputSchema: {
|
|
1644
1919
|
limit: import_zod4.z.number().optional().describe("Most-recent N events to include. Default 25."),
|
|
1645
1920
|
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.'),
|
|
@@ -1768,12 +2043,12 @@ async function captureDiagnosis(sync, target) {
|
|
|
1768
2043
|
target
|
|
1769
2044
|
);
|
|
1770
2045
|
} catch {
|
|
1771
|
-
return "
|
|
2046
|
+
return "Capture status is unavailable. An empty result does not establish whether requests occurred.";
|
|
1772
2047
|
}
|
|
1773
2048
|
if (!status || status.capturing !== false) {
|
|
1774
|
-
return "
|
|
2049
|
+
return "No requests are available in this result. Check capture setup, supported transports and hook timing before concluding that no request occurred.";
|
|
1775
2050
|
}
|
|
1776
|
-
return "
|
|
2051
|
+
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.";
|
|
1777
2052
|
}
|
|
1778
2053
|
function skippedNote(skipped) {
|
|
1779
2054
|
return skipped > 0 ? `
|
|
@@ -1805,15 +2080,33 @@ function formatRow(index, e, markers) {
|
|
|
1805
2080
|
\u2514\u2500 ${bits.join(" \xB7 ")}`;
|
|
1806
2081
|
}
|
|
1807
2082
|
function registerNetworkTools(server, sync) {
|
|
2083
|
+
server.registerTool("network_conditions", {
|
|
2084
|
+
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.",
|
|
2085
|
+
inputSchema: {
|
|
2086
|
+
action: import_zod5.z.enum(["get", "set"]).describe("Read the effective profile or select one."),
|
|
2087
|
+
profile: import_zod5.z.enum(["normal", "offline", "slow", "verySlow"]).optional().describe("Required for set."),
|
|
2088
|
+
deviceId: deviceIdInput
|
|
2089
|
+
}
|
|
2090
|
+
}, async (args) => {
|
|
2091
|
+
try {
|
|
2092
|
+
if (args.action === "set" && !args.profile) return fail2("Set requires a profile.");
|
|
2093
|
+
const target = sync.resolveDevice(args.deviceId);
|
|
2094
|
+
const result = await sync.callAction(NETWORK_TOOL_ID, args.action === "get" ? "getNetworkConditions" : "setNetworkConditions", args.action === "set" ? { profile: args.profile } : void 0, target);
|
|
2095
|
+
if (!result || typeof result !== "object" || !("profile" in result)) return fail2("The device did not report its effective network conditions.");
|
|
2096
|
+
return text(JSON.stringify(result));
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
return fail2(errMsg2(error));
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
1808
2101
|
server.registerTool(
|
|
1809
2102
|
"get_network_requests",
|
|
1810
2103
|
{
|
|
1811
|
-
description: "List captured HTTP requests
|
|
2104
|
+
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.",
|
|
1812
2105
|
inputSchema: {
|
|
1813
2106
|
limit: import_zod5.z.number().optional().describe("Most-recent N requests. Default 25."),
|
|
1814
2107
|
pattern: import_zod5.z.string().optional().describe("Only requests whose URL contains this substring (case-insensitive)."),
|
|
1815
2108
|
status: import_zod5.z.enum(["all", "errors", "success", "pending"]).optional().describe("Filter by outcome. Default all."),
|
|
1816
|
-
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
|
|
2109
|
+
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."),
|
|
1817
2110
|
includeBodies: import_zod5.z.boolean().optional().describe("Include truncated request/response bodies. Default false (compact)."),
|
|
1818
2111
|
deviceId: deviceIdInput
|
|
1819
2112
|
}
|
|
@@ -1892,7 +2185,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1892
2185
|
server.registerTool(
|
|
1893
2186
|
"network_action",
|
|
1894
2187
|
{
|
|
1895
|
-
description: "
|
|
2188
|
+
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.",
|
|
1896
2189
|
inputSchema: {
|
|
1897
2190
|
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)."),
|
|
1898
2191
|
id: import_zod5.z.string().optional().describe("Request id for pin/unpin/save/unsave \u2014 from get_network_requests."),
|
|
@@ -1948,7 +2241,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1948
2241
|
return text(`Cleared saved requests on ${target} (pinned ones kept).`);
|
|
1949
2242
|
case "clearRequests":
|
|
1950
2243
|
await sync.callAction(NETWORK_TOOL_ID, "clearEvents", {}, target);
|
|
1951
|
-
return text(`
|
|
2244
|
+
return text(`Clear request sent to ${target}. Re-list to confirm the live capture list; this action does not clear retained records.`);
|
|
1952
2245
|
}
|
|
1953
2246
|
} catch (e) {
|
|
1954
2247
|
return fail2(errMsg2(e));
|
|
@@ -1958,7 +2251,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1958
2251
|
server.registerTool(
|
|
1959
2252
|
"network_override",
|
|
1960
2253
|
{
|
|
1961
|
-
description: "
|
|
2254
|
+
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.",
|
|
1962
2255
|
inputSchema: {
|
|
1963
2256
|
action: import_zod5.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
|
|
1964
2257
|
"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."
|
|
@@ -1979,6 +2272,9 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1979
2272
|
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(
|
|
1980
2273
|
"For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
|
|
1981
2274
|
),
|
|
2275
|
+
bodyPatch: import_zod5.z.record(import_zod5.z.unknown()).optional().describe(
|
|
2276
|
+
'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.'
|
|
2277
|
+
),
|
|
1982
2278
|
statusText: import_zod5.z.string().optional().describe(
|
|
1983
2279
|
"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."
|
|
1984
2280
|
),
|
|
@@ -1993,7 +2289,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
1993
2289
|
),
|
|
1994
2290
|
name: import_zod5.z.string().optional().describe("Label shown in the developer's UI."),
|
|
1995
2291
|
enabled: import_zod5.z.boolean().optional().describe(
|
|
1996
|
-
"For `add`:
|
|
2292
|
+
"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})."
|
|
1997
2293
|
),
|
|
1998
2294
|
id: import_zod5.z.string().optional().describe(
|
|
1999
2295
|
"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."
|
|
@@ -2030,6 +2326,7 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
|
|
|
2030
2326
|
status: args.status,
|
|
2031
2327
|
statusText: args.statusText,
|
|
2032
2328
|
body: args.body,
|
|
2329
|
+
bodyPatch: args.bodyPatch,
|
|
2033
2330
|
headers: args.headers,
|
|
2034
2331
|
failKind: args.failKind,
|
|
2035
2332
|
delayMs: args.delayMs,
|
|
@@ -2063,7 +2360,7 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2063
2360
|
}
|
|
2064
2361
|
case "clear":
|
|
2065
2362
|
await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
|
|
2066
|
-
return text(`Removed all override rules on ${target}.
|
|
2363
|
+
return text(`Removed all override rules on ${target}. Other interception or simulation settings may still apply.`);
|
|
2067
2364
|
case "enable":
|
|
2068
2365
|
case "disable": {
|
|
2069
2366
|
const enabled = args.action === "enable";
|
|
@@ -2075,12 +2372,12 @@ Trigger the request now (navigate / tap_element), then check it with get_network
|
|
|
2075
2372
|
target
|
|
2076
2373
|
);
|
|
2077
2374
|
return text(
|
|
2078
|
-
`Rule \`${args.id}\`
|
|
2375
|
+
`Rule \`${args.id}\` was requested ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
|
|
2079
2376
|
);
|
|
2080
2377
|
}
|
|
2081
2378
|
await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
|
|
2082
2379
|
return text(
|
|
2083
|
-
enabled ? `Overrides
|
|
2380
|
+
enabled ? `Overrides enabled in settings for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
|
|
2084
2381
|
);
|
|
2085
2382
|
}
|
|
2086
2383
|
case "list": {
|
|
@@ -2110,7 +2407,7 @@ ${JSON.stringify(rule, null, 2)}
|
|
|
2110
2407
|
return text(
|
|
2111
2408
|
`No override rules on ${target}.
|
|
2112
2409
|
|
|
2113
|
-
Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
|
|
2410
|
+
Add one with network_override({action:"add", urlPattern:"https://api.example.com/test*", status:500}) to force an error state.`
|
|
2114
2411
|
);
|
|
2115
2412
|
}
|
|
2116
2413
|
const rows = rules.map((rule, i) => {
|
|
@@ -2207,7 +2504,7 @@ function registerReactQueryTools(server, sync) {
|
|
|
2207
2504
|
server.registerTool(
|
|
2208
2505
|
"get_react_query",
|
|
2209
2506
|
{
|
|
2210
|
-
description: "Read the
|
|
2507
|
+
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.",
|
|
2211
2508
|
inputSchema: {
|
|
2212
2509
|
includeData: import_zod6.z.boolean().optional().describe("Include each query's full cached data payload (HEAVY). Default false."),
|
|
2213
2510
|
staleOnly: import_zod6.z.boolean().optional().describe("Only stale queries. Default false."),
|
|
@@ -2266,13 +2563,13 @@ ${lines.join("\n")}${note}`);
|
|
|
2266
2563
|
const target = sync.requireProDevice(args.deviceId);
|
|
2267
2564
|
if (args.action === "clear") {
|
|
2268
2565
|
await callQueryAction(sync, "clearQueryCache", {}, target);
|
|
2269
|
-
return text("
|
|
2566
|
+
return text("Requested clearing of the connected React Query cache. Read it again to confirm.");
|
|
2270
2567
|
}
|
|
2271
2568
|
if (!args.queryHash) {
|
|
2272
2569
|
return fail2(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
|
|
2273
2570
|
}
|
|
2274
2571
|
await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
|
|
2275
|
-
return text(`
|
|
2572
|
+
return text(`Requested ${args.action} on query \`${args.queryHash}\`.`);
|
|
2276
2573
|
} catch (e) {
|
|
2277
2574
|
return fail2(errMsg2(e));
|
|
2278
2575
|
}
|
|
@@ -2287,7 +2584,7 @@ function registerZustandTools(server, sync) {
|
|
|
2287
2584
|
server.registerTool(
|
|
2288
2585
|
"get_zustand_state",
|
|
2289
2586
|
{
|
|
2290
|
-
description:
|
|
2587
|
+
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.',
|
|
2291
2588
|
inputSchema: {
|
|
2292
2589
|
includeValues: import_zod7.z.boolean().optional().describe("Include each store's full current state object (HEAVY). Default false (keys only)."),
|
|
2293
2590
|
limit: import_zod7.z.number().optional().describe("First N stores. Default all."),
|
|
@@ -2339,7 +2636,7 @@ ${lines.join("\n")}${note}`);
|
|
|
2339
2636
|
{ storeName: args.store, state: args.state, replace: args.replace ?? true },
|
|
2340
2637
|
target
|
|
2341
2638
|
);
|
|
2342
|
-
return text(`
|
|
2639
|
+
return text(`Requested state update on Zustand store "${args.store}" (${args.replace === false ? "merged" : "replaced"}).`);
|
|
2343
2640
|
})
|
|
2344
2641
|
);
|
|
2345
2642
|
}
|
|
@@ -2351,7 +2648,7 @@ function registerJotaiTools(server, sync) {
|
|
|
2351
2648
|
server.registerTool(
|
|
2352
2649
|
"get_jotai_state",
|
|
2353
2650
|
{
|
|
2354
|
-
description:
|
|
2651
|
+
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.',
|
|
2355
2652
|
inputSchema: {
|
|
2356
2653
|
includeValues: import_zod8.z.boolean().optional().describe("Include each atom's current value (HEAVY). Default false."),
|
|
2357
2654
|
limit: import_zod8.z.number().optional().describe("First N atoms. Default all."),
|
|
@@ -2397,7 +2694,7 @@ ${lines.join("\n")}${note}`);
|
|
|
2397
2694
|
},
|
|
2398
2695
|
withDevice(sync, async (args, target) => {
|
|
2399
2696
|
await sync.callAction(JOTAI_TOOL_ID, "setAtom", { label: args.atom, value: args.value }, target);
|
|
2400
|
-
return text(`
|
|
2697
|
+
return text(`Requested update of Jotai atom "${args.atom}" to ${JSON.stringify(args.value)}.`);
|
|
2401
2698
|
})
|
|
2402
2699
|
);
|
|
2403
2700
|
}
|
|
@@ -2421,7 +2718,7 @@ function registerReduxTools(server, sync) {
|
|
|
2421
2718
|
server.registerTool(
|
|
2422
2719
|
"get_redux_state",
|
|
2423
2720
|
{
|
|
2424
|
-
description:
|
|
2721
|
+
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.',
|
|
2425
2722
|
inputSchema: {
|
|
2426
2723
|
includeValues: import_zod9.z.boolean().optional().describe("Include the full current state tree (HEAVY). Default false (slice names only)."),
|
|
2427
2724
|
deviceId: deviceIdInput
|
|
@@ -2570,9 +2867,9 @@ ${lines.join("\n")}`);
|
|
|
2570
2867
|
server.registerTool(
|
|
2571
2868
|
"storage_action",
|
|
2572
2869
|
{
|
|
2573
|
-
description: "Modify the app's AsyncStorage: set a key, remove a key, or clear
|
|
2870
|
+
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.",
|
|
2574
2871
|
inputSchema: {
|
|
2575
|
-
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear
|
|
2872
|
+
action: import_zod10.z.enum(["set", "remove", "clear"]).describe("set/remove a key, or clear supported app AsyncStorage keys."),
|
|
2576
2873
|
key: import_zod10.z.string().optional().describe("The key (required for set and remove)."),
|
|
2577
2874
|
value: import_zod10.z.string().optional().describe("The string value to store (required for set; JSON-encode objects)."),
|
|
2578
2875
|
deviceId: deviceIdInput
|
|
@@ -2583,20 +2880,20 @@ ${lines.join("\n")}`);
|
|
|
2583
2880
|
const target = sync.requireProDevice(args.deviceId);
|
|
2584
2881
|
if (args.action === "clear") {
|
|
2585
2882
|
await sync.callAction(STORAGE_TOOL_ID, "clearAppStorage", {}, target);
|
|
2586
|
-
return text("
|
|
2883
|
+
return text("Requested clearing of supported app AsyncStorage keys. Read storage again to confirm.");
|
|
2587
2884
|
}
|
|
2588
2885
|
if (!args.key) {
|
|
2589
2886
|
return fail2(`'${args.action}' needs a key.`);
|
|
2590
2887
|
}
|
|
2591
2888
|
if (args.action === "remove") {
|
|
2592
2889
|
await sync.callAction(STORAGE_TOOL_ID, "async.removeItem", { key: args.key }, target);
|
|
2593
|
-
return text(`
|
|
2890
|
+
return text(`Requested removal of storage key "${args.key}".`);
|
|
2594
2891
|
}
|
|
2595
2892
|
if (args.value === void 0) {
|
|
2596
2893
|
return fail2("'set' needs a value (a string).");
|
|
2597
2894
|
}
|
|
2598
2895
|
await sync.callAction(STORAGE_TOOL_ID, "async.setItem", { key: args.key, value: args.value }, target);
|
|
2599
|
-
return text(`
|
|
2896
|
+
return text(`Requested update of storage key "${args.key}".`);
|
|
2600
2897
|
} catch (e) {
|
|
2601
2898
|
return fail2(errMsg2(e));
|
|
2602
2899
|
}
|
|
@@ -2618,9 +2915,9 @@ function registerRoutesTools(server, sync) {
|
|
|
2618
2915
|
server.registerTool(
|
|
2619
2916
|
"get_routes",
|
|
2620
2917
|
{
|
|
2621
|
-
description:
|
|
2918
|
+
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.',
|
|
2622
2919
|
inputSchema: {
|
|
2623
|
-
includeSitemap: import_zod11.z.boolean().optional().describe("Also list
|
|
2920
|
+
includeSitemap: import_zod11.z.boolean().optional().describe("Also list available configured route paths (can be long). Default false."),
|
|
2624
2921
|
deviceId: deviceIdInput
|
|
2625
2922
|
}
|
|
2626
2923
|
},
|
|
@@ -2701,7 +2998,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2701
2998
|
server.registerTool(
|
|
2702
2999
|
"get_console",
|
|
2703
3000
|
{
|
|
2704
|
-
description: "Read
|
|
3001
|
+
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.",
|
|
2705
3002
|
inputSchema: {
|
|
2706
3003
|
level: import_zod12.z.enum(["verbose", "info", "warning", "error"]).optional().describe("Minimum level to include (severity threshold). e.g. 'warning' = warnings + errors. Default all."),
|
|
2707
3004
|
limit: import_zod12.z.number().optional().describe("Most-recent N entries. Default 50."),
|
|
@@ -2734,7 +3031,7 @@ function registerConsoleTools(server, sync) {
|
|
|
2734
3031
|
${lines.join("\n")}
|
|
2735
3032
|
\`\`\`
|
|
2736
3033
|
|
|
2737
|
-
_Tail of recent logs. Use get_snapshot
|
|
3034
|
+
_Tail of recent logs. Use get_snapshot for available console entry details._`);
|
|
2738
3035
|
})
|
|
2739
3036
|
);
|
|
2740
3037
|
}
|
|
@@ -2794,7 +3091,7 @@ function registerSentryTools(server, sync) {
|
|
|
2794
3091
|
server.registerTool(
|
|
2795
3092
|
"get_sentry_envelopes",
|
|
2796
3093
|
{
|
|
2797
|
-
description: "
|
|
3094
|
+
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.",
|
|
2798
3095
|
inputSchema: {
|
|
2799
3096
|
type: import_zod13.z.string().optional().describe(
|
|
2800
3097
|
"Only envelopes containing this item type, e.g. 'event' for errors or 'transaction' for traces."
|
|
@@ -2844,7 +3141,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2844
3141
|
server.registerTool(
|
|
2845
3142
|
"get_sentry_cost",
|
|
2846
3143
|
{
|
|
2847
|
-
description: "
|
|
3144
|
+
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.",
|
|
2848
3145
|
inputSchema: {
|
|
2849
3146
|
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2850
3147
|
}
|
|
@@ -2860,7 +3157,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2860
3157
|
const usage = p.usage;
|
|
2861
3158
|
if (!usage?.session) {
|
|
2862
3159
|
return text2(
|
|
2863
|
-
`${staleHeader2}No usage
|
|
3160
|
+
`${staleHeader2}No usage is available in this capture.${statusNote(p.status)}`
|
|
2864
3161
|
);
|
|
2865
3162
|
}
|
|
2866
3163
|
const session = usage.session;
|
|
@@ -2873,7 +3170,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2873
3170
|
const month = projected[key];
|
|
2874
3171
|
const quota = FREE_QUOTA[key];
|
|
2875
3172
|
const pct = quota > 0 ? Math.round(month / quota * 100) : null;
|
|
2876
|
-
const pctNote = pct === null ? " (
|
|
3173
|
+
const pctNote = pct === null ? " (no configured reference quota)" : ` \u2014 ${pct}% of the configured reference quota of ${units(quota, format)}`;
|
|
2877
3174
|
return `- ${key}: ${now} this session \u2192 ~${units(month, format)}/mo${pctNote}`;
|
|
2878
3175
|
});
|
|
2879
3176
|
const windowNote = usage.windowMs ? ` over ${Math.round(usage.windowMs / 1e3)}s` : "";
|
|
@@ -2883,7 +3180,7 @@ _Full payloads (exception, breadcrumbs, contexts) via get_snapshot('sentry'). Bi
|
|
|
2883
3180
|
${usage.topContributors.map((c) => `- ${c.label} \u2014 ${c.units} ${c.category}`).join("\n")}` : "";
|
|
2884
3181
|
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._";
|
|
2885
3182
|
return text2(
|
|
2886
|
-
`${staleHeader2}**Sentry
|
|
3183
|
+
`${staleHeader2}**Sentry usage estimate \u2014 ${p.envelopes?.length ?? 0} envelopes${windowNote}:**
|
|
2887
3184
|
|
|
2888
3185
|
${rows.join("\n")}${contributors}${caveat}`
|
|
2889
3186
|
);
|
|
@@ -2913,9 +3210,9 @@ ${rows.join("\n")}${contributors}${caveat}`
|
|
|
2913
3210
|
const recent = p.drops?.recent ?? [];
|
|
2914
3211
|
if (summary.length === 0) {
|
|
2915
3212
|
return text2(
|
|
2916
|
-
`${staleHeader2}
|
|
3213
|
+
`${staleHeader2}No drops are reported in the available capture.${statusNote(p.status)}
|
|
2917
3214
|
|
|
2918
|
-
_If an event is
|
|
3215
|
+
_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._`
|
|
2919
3216
|
);
|
|
2920
3217
|
}
|
|
2921
3218
|
const limit = typeof args.limit === "number" && args.limit > 0 ? args.limit : 30;
|
|
@@ -2994,7 +3291,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
2994
3291
|
server.registerTool(
|
|
2995
3292
|
"get_time_machine",
|
|
2996
3293
|
{
|
|
2997
|
-
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
|
|
3294
|
+
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.",
|
|
2998
3295
|
inputSchema: {
|
|
2999
3296
|
deviceId: deviceIdInput
|
|
3000
3297
|
}
|
|
@@ -3017,7 +3314,7 @@ function registerTimeMachineTools(server, sync) {
|
|
|
3017
3314
|
}
|
|
3018
3315
|
const snapLines = snapshots.map((s) => {
|
|
3019
3316
|
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
3020
|
-
const desc = s.baseline ? "baseline
|
|
3317
|
+
const desc = s.baseline ? "baseline: clear supported providers and request reload" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
3021
3318
|
const route = s.route ? ` \xB7 route ${s.route.href}` : "";
|
|
3022
3319
|
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${route}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
3023
3320
|
});
|
|
@@ -3038,7 +3335,7 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
3038
3335
|
server.registerTool(
|
|
3039
3336
|
"time_machine_action",
|
|
3040
3337
|
{
|
|
3041
|
-
description: "Manage
|
|
3338
|
+
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.",
|
|
3042
3339
|
inputSchema: {
|
|
3043
3340
|
action: import_zod14.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
3044
3341
|
id: import_zod14.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
@@ -3185,7 +3482,7 @@ function registerScenariosTools(server, sync) {
|
|
|
3185
3482
|
server.registerTool(
|
|
3186
3483
|
"get_scenarios",
|
|
3187
3484
|
{
|
|
3188
|
-
description: "List
|
|
3485
|
+
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.",
|
|
3189
3486
|
inputSchema: {
|
|
3190
3487
|
deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
3191
3488
|
}
|
|
@@ -3226,7 +3523,7 @@ ${drafts.map(summarize).join("\n")}`
|
|
|
3226
3523
|
server.registerTool(
|
|
3227
3524
|
"scenario_action",
|
|
3228
3525
|
{
|
|
3229
|
-
description:
|
|
3526
|
+
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.",
|
|
3230
3527
|
inputSchema: {
|
|
3231
3528
|
action: import_zod15.z.enum(["preview", "run", "deactivate", "save", "setFolder", "delete"]).describe("What to do."),
|
|
3232
3529
|
folder: import_zod15.z.string().optional().describe(
|
|
@@ -3316,7 +3613,7 @@ ${r.willDo.map((s) => ` \xB7 ${s.label} [${s.durability}]`).join("\n")}`
|
|
|
3316
3613
|
if (r.warnings?.length) lines.push(`**Warnings:**
|
|
3317
3614
|
${r.warnings.map((w) => ` \xB7 ${w}`).join("\n")}`);
|
|
3318
3615
|
lines.push(
|
|
3319
|
-
r.ok ? "
|
|
3616
|
+
r.ok ? "Pre-flight checks passed. Review the intended effects before running." : `\u274C **Cannot run:**
|
|
3320
3617
|
${r.errors.map((e) => ` \xB7 ${e}`).join("\n")}`
|
|
3321
3618
|
);
|
|
3322
3619
|
return text3(lines.join("\n\n"));
|
|
@@ -3351,9 +3648,9 @@ The device is now in a partial state. Deactivate to reverse what landed, or fix
|
|
|
3351
3648
|
[
|
|
3352
3649
|
`\u2705 **Scenario active \u2014 ${r.name ?? args.id}**`,
|
|
3353
3650
|
steps,
|
|
3354
|
-
`**
|
|
3651
|
+
`**Reported effects:**
|
|
3355
3652
|
${(r.effects ?? []).map((e) => ` \xB7 ${e.label} [${e.kind}]`).join("\n")}`,
|
|
3356
|
-
"The device shows a SIMULATED banner while this is active. Deactivate when done,
|
|
3653
|
+
"The device shows a SIMULATED banner while this is active. Deactivate when done, and account for these effects when interpreting observations."
|
|
3357
3654
|
].join("\n\n")
|
|
3358
3655
|
);
|
|
3359
3656
|
}
|
|
@@ -3418,10 +3715,10 @@ var import_zod16 = require("zod");
|
|
|
3418
3715
|
// src/capture/simctl.ts
|
|
3419
3716
|
var import_node_child_process = require("child_process");
|
|
3420
3717
|
var import_node_util = require("util");
|
|
3421
|
-
var
|
|
3422
|
-
var
|
|
3423
|
-
var
|
|
3424
|
-
var
|
|
3718
|
+
var import_node_fs3 = require("fs");
|
|
3719
|
+
var import_node_os3 = __toESM(require("os"));
|
|
3720
|
+
var import_node_path3 = __toESM(require("path"));
|
|
3721
|
+
var import_node_crypto3 = require("crypto");
|
|
3425
3722
|
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
3426
3723
|
var clamp2 = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
3427
3724
|
function humanizeSimctlError(err) {
|
|
@@ -3447,7 +3744,7 @@ async function captureIosFramebuffer() {
|
|
|
3447
3744
|
error: "Screenshotting a component requires macOS with Xcode's iOS Simulator (uses `xcrun simctl`)."
|
|
3448
3745
|
};
|
|
3449
3746
|
}
|
|
3450
|
-
const out =
|
|
3747
|
+
const out = import_node_path3.default.join(import_node_os3.default.tmpdir(), `buoy-mcp-sim-${(0, import_node_crypto3.randomUUID)()}.png`);
|
|
3451
3748
|
try {
|
|
3452
3749
|
await execFileAsync("xcrun", ["simctl", "io", "booted", "screenshot", "--type=png", out], {
|
|
3453
3750
|
timeout: 15e3
|
|
@@ -3456,7 +3753,7 @@ async function captureIosFramebuffer() {
|
|
|
3456
3753
|
return { ok: false, error: humanizeSimctlError(err) };
|
|
3457
3754
|
}
|
|
3458
3755
|
try {
|
|
3459
|
-
const buf = await
|
|
3756
|
+
const buf = await import_node_fs3.promises.readFile(out);
|
|
3460
3757
|
const size = pngSize(buf);
|
|
3461
3758
|
if (!size) return { ok: false, error: "Captured file wasn't a valid PNG." };
|
|
3462
3759
|
return { ok: true, srcPath: out, width: size.width, height: size.height };
|
|
@@ -3469,22 +3766,22 @@ async function cropToBuffer(srcPath, full, region) {
|
|
|
3469
3766
|
const y = clamp2(Math.round(region.y), 0, Math.max(0, full.height - 1));
|
|
3470
3767
|
const width = clamp2(Math.round(region.width), 1, full.width - x);
|
|
3471
3768
|
const height = clamp2(Math.round(region.height), 1, full.height - y);
|
|
3472
|
-
const out =
|
|
3769
|
+
const out = import_node_path3.default.join(import_node_os3.default.tmpdir(), `buoy-mcp-crop-${(0, import_node_crypto3.randomUUID)()}.png`);
|
|
3473
3770
|
try {
|
|
3474
3771
|
await execFileAsync(
|
|
3475
3772
|
"sips",
|
|
3476
3773
|
["-c", String(height), String(width), "--cropOffset", String(y), String(x), srcPath, "--out", out],
|
|
3477
3774
|
{ timeout: 1e4 }
|
|
3478
3775
|
);
|
|
3479
|
-
const buf = await
|
|
3776
|
+
const buf = await import_node_fs3.promises.readFile(out);
|
|
3480
3777
|
return { ok: true, buffer: buf, width, height };
|
|
3481
3778
|
} catch (err) {
|
|
3482
3779
|
const e = err;
|
|
3483
3780
|
return { ok: false, error: `Crop failed: ${e?.stderr || e?.message || err}` };
|
|
3484
3781
|
} finally {
|
|
3485
|
-
await
|
|
3782
|
+
await import_node_fs3.promises.unlink(out).catch(() => {
|
|
3486
3783
|
});
|
|
3487
|
-
await
|
|
3784
|
+
await import_node_fs3.promises.unlink(srcPath).catch(() => {
|
|
3488
3785
|
});
|
|
3489
3786
|
}
|
|
3490
3787
|
}
|
|
@@ -3556,11 +3853,11 @@ function registerScreenshotTools(server, sync) {
|
|
|
3556
3853
|
server.registerTool(
|
|
3557
3854
|
"screenshot_component",
|
|
3558
3855
|
{
|
|
3559
|
-
description: "
|
|
3856
|
+
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.",
|
|
3560
3857
|
inputSchema: {
|
|
3561
3858
|
query: import_zod16.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
|
|
3562
3859
|
nativeTag: import_zod16.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
|
|
3563
|
-
scrollIntoView: import_zod16.z.boolean().optional().describe("
|
|
3860
|
+
scrollIntoView: import_zod16.z.boolean().optional().describe("Attempt to scroll an off-screen component into view before capture. Default true; inspect the result."),
|
|
3564
3861
|
deviceId: deviceIdInput
|
|
3565
3862
|
}
|
|
3566
3863
|
},
|
|
@@ -3648,21 +3945,21 @@ var import_zod17 = require("zod");
|
|
|
3648
3945
|
// src/capture/camera.ts
|
|
3649
3946
|
var import_node_child_process2 = require("child_process");
|
|
3650
3947
|
var import_node_util2 = require("util");
|
|
3651
|
-
var
|
|
3652
|
-
var
|
|
3653
|
-
var
|
|
3948
|
+
var import_node_fs4 = require("fs");
|
|
3949
|
+
var import_node_os4 = __toESM(require("os"));
|
|
3950
|
+
var import_node_path4 = __toESM(require("path"));
|
|
3654
3951
|
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
3655
3952
|
function candidates() {
|
|
3656
3953
|
const fromEnv = process.env.BUOY_SIMCAM_DIR;
|
|
3657
3954
|
const list = [
|
|
3658
|
-
...fromEnv ? [
|
|
3955
|
+
...fromEnv ? [import_node_path4.default.join(fromEnv, "buoycam")] : [],
|
|
3659
3956
|
"/Applications/Buoy.app/Contents/Resources/simcam/buoycam",
|
|
3660
|
-
|
|
3957
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Applications/Buoy.app/Contents/Resources/simcam/buoycam")
|
|
3661
3958
|
];
|
|
3662
3959
|
let dir = __dirname;
|
|
3663
3960
|
for (let i = 0; i < 8; i++) {
|
|
3664
|
-
list.push(
|
|
3665
|
-
const parent =
|
|
3961
|
+
list.push(import_node_path4.default.join(dir, "apps/desktop/resources/simcam/buoycam"));
|
|
3962
|
+
const parent = import_node_path4.default.dirname(dir);
|
|
3666
3963
|
if (parent === dir) break;
|
|
3667
3964
|
dir = parent;
|
|
3668
3965
|
}
|
|
@@ -3671,7 +3968,7 @@ function candidates() {
|
|
|
3671
3968
|
var resolved = null;
|
|
3672
3969
|
function cliPath() {
|
|
3673
3970
|
if (resolved) return resolved;
|
|
3674
|
-
resolved = candidates().find((c) => (0,
|
|
3971
|
+
resolved = candidates().find((c) => (0, import_node_fs4.existsSync)(c)) ?? null;
|
|
3675
3972
|
return resolved;
|
|
3676
3973
|
}
|
|
3677
3974
|
async function run(args) {
|
|
@@ -3772,28 +4069,28 @@ function entitlementPaths() {
|
|
|
3772
4069
|
const fromEnv = process.env.BUOY_ENTITLEMENT_FILE;
|
|
3773
4070
|
if (fromEnv) return [fromEnv];
|
|
3774
4071
|
return [
|
|
3775
|
-
|
|
3776
|
-
|
|
4072
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Library/Application Support/Buoy/entitlement.json"),
|
|
4073
|
+
import_node_path4.default.join(import_node_os4.default.homedir(), "Library/Application Support/buoy-desktop/entitlement.json")
|
|
3777
4074
|
];
|
|
3778
4075
|
}
|
|
3779
4076
|
function readEntitlement() {
|
|
3780
4077
|
for (const p of entitlementPaths()) {
|
|
3781
4078
|
try {
|
|
3782
|
-
if (!(0,
|
|
3783
|
-
const parsed = JSON.parse((0,
|
|
4079
|
+
if (!(0, import_node_fs4.existsSync)(p)) continue;
|
|
4080
|
+
const parsed = JSON.parse((0, import_node_fs4.readFileSync)(p, "utf8"));
|
|
3784
4081
|
return {
|
|
3785
|
-
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "
|
|
3786
|
-
isPro: parsed.isPro === true
|
|
4082
|
+
tier: parsed.tier === "pro" || parsed.tier === "free" ? parsed.tier : "locked",
|
|
4083
|
+
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
|
|
3787
4084
|
};
|
|
3788
4085
|
} catch {
|
|
3789
4086
|
}
|
|
3790
4087
|
}
|
|
3791
|
-
return { tier: "
|
|
4088
|
+
return { tier: "locked", isPro: false };
|
|
3792
4089
|
}
|
|
3793
4090
|
function proRequired(tool) {
|
|
3794
4091
|
return `\`${tool}\` is a Buoy Pro feature.
|
|
3795
4092
|
|
|
3796
|
-
The simulator camera itself is free \u2014 the Mac camera, image, video, test-pattern and QR sources all work in the Buoy desktop panel
|
|
4093
|
+
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.
|
|
3797
4094
|
|
|
3798
4095
|
_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._`;
|
|
3799
4096
|
}
|
|
@@ -4040,16 +4337,24 @@ function registerInteractTools(server, sync) {
|
|
|
4040
4337
|
server.registerTool(
|
|
4041
4338
|
"describe_screen",
|
|
4042
4339
|
{
|
|
4043
|
-
description: "List
|
|
4340
|
+
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.",
|
|
4044
4341
|
inputSchema: {
|
|
4045
4342
|
interactiveOnly: import_zod18.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
4343
|
+
includeBuoy: import_zod18.z.boolean().optional().describe(
|
|
4344
|
+
"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."
|
|
4345
|
+
),
|
|
4046
4346
|
deviceId: deviceIdInput
|
|
4047
4347
|
}
|
|
4048
4348
|
},
|
|
4049
4349
|
async (args) => {
|
|
4050
4350
|
try {
|
|
4051
4351
|
const target = sync.requireProDevice(args.deviceId);
|
|
4052
|
-
const res = await sync.callAction(
|
|
4352
|
+
const res = await sync.callAction(
|
|
4353
|
+
TOOL_ID2,
|
|
4354
|
+
"describeScreen",
|
|
4355
|
+
{ includeBuoy: args.includeBuoy },
|
|
4356
|
+
target
|
|
4357
|
+
);
|
|
4053
4358
|
const all = res?.elements ?? [];
|
|
4054
4359
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
4055
4360
|
if (elements.length === 0) {
|
|
@@ -4057,7 +4362,8 @@ function registerInteractTools(server, sync) {
|
|
|
4057
4362
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
4058
4363
|
);
|
|
4059
4364
|
}
|
|
4060
|
-
const
|
|
4365
|
+
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.` : "";
|
|
4366
|
+
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}`;
|
|
4061
4367
|
return text5(`${header}
|
|
4062
4368
|
|
|
4063
4369
|
${elements.map(formatElement).join("\n")}`);
|
|
@@ -4069,7 +4375,7 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4069
4375
|
server.registerTool(
|
|
4070
4376
|
"tap_element",
|
|
4071
4377
|
{
|
|
4072
|
-
description: "
|
|
4378
|
+
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.",
|
|
4073
4379
|
inputSchema: {
|
|
4074
4380
|
nativeTag: import_zod18.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
4075
4381
|
testID: import_zod18.z.string().optional().describe("Exact testID of the element."),
|
|
@@ -4077,7 +4383,8 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4077
4383
|
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)."),
|
|
4078
4384
|
text: import_zod18.z.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
4079
4385
|
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."),
|
|
4080
|
-
scrollIntoView: import_zod18.z.boolean().optional().describe("
|
|
4386
|
+
scrollIntoView: import_zod18.z.boolean().optional().describe("Attempt to scroll an off-screen target into view. Default true; inspect the action result."),
|
|
4387
|
+
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."),
|
|
4081
4388
|
deviceId: deviceIdInput
|
|
4082
4389
|
}
|
|
4083
4390
|
},
|
|
@@ -4097,7 +4404,8 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4097
4404
|
value: args.value,
|
|
4098
4405
|
text: args.text,
|
|
4099
4406
|
longPress: args.longPress,
|
|
4100
|
-
scrollIntoView: args.scrollIntoView
|
|
4407
|
+
scrollIntoView: args.scrollIntoView,
|
|
4408
|
+
includeBuoy: args.includeBuoy
|
|
4101
4409
|
},
|
|
4102
4410
|
target
|
|
4103
4411
|
);
|
|
@@ -4109,7 +4417,12 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
4109
4417
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
4110
4418
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
4111
4419
|
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}`;
|
|
4112
|
-
|
|
4420
|
+
const fx = res.effect;
|
|
4421
|
+
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.`;
|
|
4422
|
+
const alsoNote = res.alsoMatched?.length ? `
|
|
4423
|
+
Also matched equally well (the choice was a coin toss \u2014 re-tap by #nativeTag if this was wrong):
|
|
4424
|
+
` + res.alsoMatched.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n") : "";
|
|
4425
|
+
return text5(`${verb}${scrolledNote}.${effectNote}${alsoNote}${flowTip}`);
|
|
4113
4426
|
}
|
|
4114
4427
|
let msg = res?.reason ?? "Tap failed.";
|
|
4115
4428
|
if (res?.candidates?.length) {
|
|
@@ -4194,11 +4507,11 @@ function registerJsThreadTools(server, sync) {
|
|
|
4194
4507
|
server.registerTool(
|
|
4195
4508
|
"get_js_thread_top",
|
|
4196
4509
|
{
|
|
4197
|
-
description:
|
|
4510
|
+
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.",
|
|
4198
4511
|
inputSchema: {
|
|
4199
4512
|
deviceId: deviceIdInput,
|
|
4200
|
-
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000
|
|
4201
|
-
clearFirst: import_zod19.z.boolean().optional().describe("Reset
|
|
4513
|
+
durationMs: import_zod19.z.number().optional().describe("How long to sample before reading the table. Default 3000; clamped to 250\u201330000."),
|
|
4514
|
+
clearFirst: import_zod19.z.boolean().optional().describe("Reset tracked statistics before sampling. Hook coverage and attribution limits still apply.")
|
|
4202
4515
|
}
|
|
4203
4516
|
},
|
|
4204
4517
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -4382,7 +4695,7 @@ function registerImagesTools(server, sync) {
|
|
|
4382
4695
|
server.registerTool(
|
|
4383
4696
|
"get_images",
|
|
4384
4697
|
{
|
|
4385
|
-
description: "
|
|
4698
|
+
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.",
|
|
4386
4699
|
inputSchema: {
|
|
4387
4700
|
deviceId: deviceIdInput,
|
|
4388
4701
|
limit: import_zod20.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
@@ -4439,7 +4752,7 @@ ${JSON.stringify(slim)}
|
|
|
4439
4752
|
server.registerTool(
|
|
4440
4753
|
"image_action",
|
|
4441
4754
|
{
|
|
4442
|
-
description: "
|
|
4755
|
+
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.",
|
|
4443
4756
|
inputSchema: {
|
|
4444
4757
|
deviceId: deviceIdInput,
|
|
4445
4758
|
id: import_zod20.z.number().describe("Record id from get_images."),
|
|
@@ -4509,12 +4822,12 @@ ${JSON.stringify(slim)}
|
|
|
4509
4822
|
server.registerTool(
|
|
4510
4823
|
"set_image_simulation",
|
|
4511
4824
|
{
|
|
4512
|
-
description: "
|
|
4825
|
+
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.",
|
|
4513
4826
|
inputSchema: {
|
|
4514
4827
|
deviceId: deviceIdInput,
|
|
4515
4828
|
network: import_zod20.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
4516
|
-
blankImages: import_zod20.z.boolean().optional().describe("Render
|
|
4517
|
-
massAction: import_zod20.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to
|
|
4829
|
+
blankImages: import_zod20.z.boolean().optional().describe("Render integrated images without a source."),
|
|
4830
|
+
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."),
|
|
4518
4831
|
clearRegistry: import_zod20.z.boolean().optional().describe("Clear the captured image log."),
|
|
4519
4832
|
clearExpoCaches: import_zod20.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
4520
4833
|
}
|
|
@@ -4653,7 +4966,7 @@ function registerAssetsTools(server, sync) {
|
|
|
4653
4966
|
server.registerTool(
|
|
4654
4967
|
"get_assets",
|
|
4655
4968
|
{
|
|
4656
|
-
description: "
|
|
4969
|
+
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.",
|
|
4657
4970
|
inputSchema: {
|
|
4658
4971
|
deviceId: deviceIdInput,
|
|
4659
4972
|
limit: import_zod21.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
@@ -4786,12 +5099,12 @@ function messageSocketUrl(origin) {
|
|
|
4786
5099
|
return `${origin.replace(/^http/, "ws").replace(/\/+$/, "")}/message`;
|
|
4787
5100
|
}
|
|
4788
5101
|
function reloadViaDevServer(origin) {
|
|
4789
|
-
return new Promise((
|
|
5102
|
+
return new Promise((resolve2) => {
|
|
4790
5103
|
let settled = false;
|
|
4791
5104
|
const done = (result) => {
|
|
4792
5105
|
if (settled) return;
|
|
4793
5106
|
settled = true;
|
|
4794
|
-
|
|
5107
|
+
resolve2(result);
|
|
4795
5108
|
};
|
|
4796
5109
|
let socket;
|
|
4797
5110
|
try {
|
|
@@ -4876,7 +5189,7 @@ function registerReloadTools(server, sync) {
|
|
|
4876
5189
|
server.registerTool(
|
|
4877
5190
|
"reload_app",
|
|
4878
5191
|
{
|
|
4879
|
-
description: "
|
|
5192
|
+
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.",
|
|
4880
5193
|
inputSchema: {
|
|
4881
5194
|
strategy: import_zod22.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
4882
5195
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
@@ -5000,7 +5313,7 @@ function registerTriageTools(server, sync) {
|
|
|
5000
5313
|
server.registerTool(
|
|
5001
5314
|
"get_triage",
|
|
5002
5315
|
{
|
|
5003
|
-
description: "
|
|
5316
|
+
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.",
|
|
5004
5317
|
inputSchema: {
|
|
5005
5318
|
deviceId: import_zod23.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
5006
5319
|
}
|
|
@@ -5167,10 +5480,10 @@ function registerFlowTools(server, sync) {
|
|
|
5167
5480
|
server.registerTool(
|
|
5168
5481
|
"run_flow",
|
|
5169
5482
|
{
|
|
5170
|
-
description: "Run
|
|
5483
|
+
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.",
|
|
5171
5484
|
inputSchema: {
|
|
5172
5485
|
steps: import_zod24.z.array(stepSchema).min(1).describe("Steps to run, in order."),
|
|
5173
|
-
include: import_zod24.z.enum(["all", "screen", "none"]).optional().describe("What to report after the flow: 'all' (default) = final
|
|
5486
|
+
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."),
|
|
5174
5487
|
continueOnFailure: import_zod24.z.boolean().optional().describe("Keep going after a failed step instead of stopping. Default false."),
|
|
5175
5488
|
deviceId: deviceIdInput
|
|
5176
5489
|
}
|
|
@@ -5492,7 +5805,7 @@ function formatCapture(c, top) {
|
|
|
5492
5805
|
}
|
|
5493
5806
|
lines.push("");
|
|
5494
5807
|
lines.push(
|
|
5495
|
-
"`wasted`
|
|
5808
|
+
"`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."
|
|
5496
5809
|
);
|
|
5497
5810
|
return lines.join("\n");
|
|
5498
5811
|
}
|
|
@@ -5528,14 +5841,14 @@ function registerRenderTools(server, sync) {
|
|
|
5528
5841
|
server.registerTool(
|
|
5529
5842
|
"measure_renders",
|
|
5530
5843
|
{
|
|
5531
|
-
description: "
|
|
5844
|
+
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.",
|
|
5532
5845
|
inputSchema: {
|
|
5533
5846
|
steps: import_zod25.z.array(stepSchema2).max(20).optional().describe(
|
|
5534
5847
|
"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."
|
|
5535
5848
|
),
|
|
5536
5849
|
idleMs: import_zod25.z.number().min(500).max(2e4).optional().describe(`Idle sample length when no steps are given. Default ${DEFAULT_IDLE_MS}.`),
|
|
5537
5850
|
label: import_zod25.z.string().optional().describe('Name this measurement, e.g. "before fix". Shown in the next comparison.'),
|
|
5538
|
-
compareToPrevious: import_zod25.z.boolean().optional().describe("
|
|
5851
|
+
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."),
|
|
5539
5852
|
top: import_zod25.z.number().min(1).max(40).optional().describe(`Components to list. Default ${DEFAULT_TOP}.`),
|
|
5540
5853
|
deviceId: deviceIdInput
|
|
5541
5854
|
}
|
|
@@ -5625,7 +5938,7 @@ function registerRenderTools(server, sync) {
|
|
|
5625
5938
|
"_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._"
|
|
5626
5939
|
);
|
|
5627
5940
|
} else if (!args.compareToPrevious) {
|
|
5628
|
-
parts.push("", "
|
|
5941
|
+
parts.push("", "_Compare another run using the same steps and `compareToPrevious:true`; check behavior and interaction warnings as well._");
|
|
5629
5942
|
}
|
|
5630
5943
|
return text8(parts.join("\n"));
|
|
5631
5944
|
} catch (e) {
|
|
@@ -5636,8 +5949,8 @@ function registerRenderTools(server, sync) {
|
|
|
5636
5949
|
}
|
|
5637
5950
|
|
|
5638
5951
|
// src/verify/editWatcher.ts
|
|
5639
|
-
var
|
|
5640
|
-
var
|
|
5952
|
+
var import_node_fs5 = __toESM(require("fs"));
|
|
5953
|
+
var import_node_path5 = __toESM(require("path"));
|
|
5641
5954
|
var WATCHED_EXT = /* @__PURE__ */ new Set([
|
|
5642
5955
|
".ts",
|
|
5643
5956
|
".tsx",
|
|
@@ -5680,14 +5993,14 @@ var EditWatcher = class {
|
|
|
5680
5993
|
if (mode === "never" || this.started) return;
|
|
5681
5994
|
this.started = true;
|
|
5682
5995
|
try {
|
|
5683
|
-
this.watcher =
|
|
5996
|
+
this.watcher = import_node_fs5.default.watch(
|
|
5684
5997
|
rootDir,
|
|
5685
5998
|
{ recursive: true },
|
|
5686
5999
|
(_event, filename) => {
|
|
5687
6000
|
if (!filename) return;
|
|
5688
6001
|
const rel2 = String(filename);
|
|
5689
|
-
if (rel2.split(
|
|
5690
|
-
if (!WATCHED_EXT.has(
|
|
6002
|
+
if (rel2.split(import_node_path5.default.sep).some((seg) => IGNORED_DIRS.has(seg))) return;
|
|
6003
|
+
if (!WATCHED_EXT.has(import_node_path5.default.extname(rel2))) return;
|
|
5691
6004
|
this.noteEdit(rel2);
|
|
5692
6005
|
}
|
|
5693
6006
|
);
|
|
@@ -5738,129 +6051,89 @@ var EditWatcher = class {
|
|
|
5738
6051
|
|
|
5739
6052
|
---
|
|
5740
6053
|
\u26A0\uFE0F **${files.length} source file${files.length === 1 ? "" : "s"} changed since anything was last checked on the device** (${shown}${more}).
|
|
5741
|
-
|
|
6054
|
+
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.`;
|
|
5742
6055
|
}
|
|
5743
6056
|
};
|
|
5744
6057
|
var editWatcher = new EditWatcher();
|
|
5745
6058
|
|
|
5746
6059
|
// src/server.ts
|
|
5747
|
-
var SERVER_INSTRUCTIONS = `Buoy MCP
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
-
|
|
5760
|
-
|
|
5761
|
-
-
|
|
5762
|
-
|
|
5763
|
-
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
and
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
\`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
|
|
5825
|
-
freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
|
|
5826
|
-
Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
|
|
5827
|
-
This is about TASKS on the thread \u2014 for React components re-rendering too often, it's
|
|
5828
|
-
\`measure_renders\`, not this.
|
|
5829
|
-
|
|
5830
|
-
Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
|
|
5831
|
-
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
5832
|
-
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
5833
|
-
|
|
5834
|
-
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
5835
|
-
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
5836
|
-
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
5837
|
-
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
5838
|
-
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
5839
|
-
|
|
5840
|
-
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
5841
|
-
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
5842
|
-
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
5843
|
-
re-read anything you cached.
|
|
5844
|
-
|
|
5845
|
-
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
5846
|
-
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
5847
|
-
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
5848
|
-
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
5849
|
-
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
5850
|
-
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
5851
|
-
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
5852
|
-
|
|
5853
|
-
For everything else:
|
|
5854
|
-
- \`list_tool_actions\` to see what a tool can do,
|
|
5855
|
-
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
5856
|
-
- \`call_action\` to invoke any tool action generically.
|
|
5857
|
-
|
|
5858
|
-
Higher-level tools (e.g. perf-monitor's \`run_benchmark_batch\`) wrap these for common workflows.
|
|
5859
|
-
If no device is connected, open the app with Buoy devtools running and try again.
|
|
5860
|
-
|
|
5861
|
-
The Buoy MCP is a Pro feature: \`list_devices\` is always available, but data/action tools
|
|
5862
|
-
require the connected app to have an active Buoy Pro license. If they error with "Buoy Pro
|
|
5863
|
-
required", tell the user to upgrade to Buoy Pro in the app \u2014 don't try to work around it.`;
|
|
6060
|
+
var SERVER_INSTRUCTIONS = `Buoy MCP connects your editor to supported Buoy tools through the configured broker.
|
|
6061
|
+
|
|
6062
|
+
Start with account and connection setup. Use \`list_devices\` to select an app and inspect
|
|
6063
|
+
its available tools. App data and action tools require Buoy Pro. If access fails, explain
|
|
6064
|
+
the reported requirement and setup steps. Camera tools use a separate local setup and
|
|
6065
|
+
account gate; \`camera_diagnose\` can report setup problems without an app connection.
|
|
6066
|
+
|
|
6067
|
+
For a runtime bug, \`get_triage\` summarizes available console errors, network activity,
|
|
6068
|
+
storage and screen information. Follow up on the relevant section and read source when
|
|
6069
|
+
needed. Missing data can reflect missing integration, retention limits or a disconnected
|
|
6070
|
+
app. An unresponsive app is not proof of a crash, and post-crash evidence may be unavailable.
|
|
6071
|
+
|
|
6072
|
+
Choose follow-up tools by the symptom:
|
|
6073
|
+
- Persisted values: \`get_storage\` with \`includeValues:true\` for configured storage sources.
|
|
6074
|
+
- Requests or API responses: \`get_network_requests\` with \`includeBodies:true\` for captured
|
|
6075
|
+
bodies. Capture depends on supported transports, installed hooks and recording timing.
|
|
6076
|
+
- Errors: \`get_console\` for retained captured output. A stopped app cannot provide new data.
|
|
6077
|
+
- Cache or store state: \`get_react_query\`, \`get_zustand_state\`, \`get_redux_state\` or
|
|
6078
|
+
\`get_jotai_state\` for the integrations registered in the app.
|
|
6079
|
+
|
|
6080
|
+
Use \`get_events\` with \`sources\` to read retained activity from connected event sources.
|
|
6081
|
+
It returns compact summaries by default; request \`includeData:true\` when details are needed.
|
|
6082
|
+
For request IDs and network actions, use \`get_network_requests\`. Its \`flagged:'any'\` filter
|
|
6083
|
+
finds pinned or saved requests. A flag is a reason to inspect a request, not proof of its
|
|
6084
|
+
cause or who flagged it. Use \`network_action\` to retain relevant evidence within task scope.
|
|
6085
|
+
|
|
6086
|
+
For supported UI interactions, \`run_flow\` runs steps such as tap, type, set, reload,
|
|
6087
|
+
waitFor and expect. Review step results, failures and available captured output. Use
|
|
6088
|
+
\`describe_screen\` to discover exposed elements and \`tap_element\` for individual actions.
|
|
6089
|
+
A completed tool call alone does not establish that the user's expected behavior occurred.
|
|
6090
|
+
|
|
6091
|
+
For render-heavy interactions, use \`measure_renders\` with representative \`steps\`.
|
|
6092
|
+
Repeat the same steps after a change with \`compareToPrevious:true\`. Interpret available
|
|
6093
|
+
render counts, reasons and wasted-render diagnostics alongside behavior and performance;
|
|
6094
|
+
these diagnostics do not prove that a render was unnecessary. For instrumented JavaScript
|
|
6095
|
+
tasks, use \`get_js_thread_top\` and \`get_js_thread_origin_detail\`. Their coverage depends
|
|
6096
|
+
on the installed hooks. Keep measurement conditions comparable.
|
|
6097
|
+
|
|
6098
|
+
\`get_images\` inspects instrumented image loads and available cache, timing, size and error
|
|
6099
|
+
data. Use \`image_action\` for supported per-image actions and \`set_image_simulation\` for
|
|
6100
|
+
integrated image behavior. Simulation does not disconnect the device's network.
|
|
6101
|
+
\`get_assets\` inspects the configured manifest, runtime registry and available bundle graph.
|
|
6102
|
+
Reported load history and size estimates do not prove complete shipped-asset coverage or
|
|
6103
|
+
that an unobserved asset is unused. \`asset_action\` can refresh data and manage baselines.
|
|
6104
|
+
|
|
6105
|
+
\`reload_app\` requests a JS reload using supported app mechanisms and can wait for
|
|
6106
|
+
reconnection. It can try a dev-server fallback for an unresponsive app when that server
|
|
6107
|
+
is known and reachable. Recovery is not guaranteed. Reload can discard in-memory evidence;
|
|
6108
|
+
collect needed observations first when possible and read state again after reconnecting.
|
|
6109
|
+
|
|
6110
|
+
\`get_time_machine\` lists snapshots and provider capabilities. \`time_machine_action\`
|
|
6111
|
+
captures or restores supported registered state sources. Preview the affected state before
|
|
6112
|
+
restoring. Live restore changes supported state in place; reload mode restores supported
|
|
6113
|
+
persisted sources and requests a reload. A baseline clears providers that support clearing
|
|
6114
|
+
and requests a reload. This does not reset backend state or reproduce a complete reinstall.
|
|
6115
|
+
Check per-source applied/skipped outcomes and any reload or route-restoration failures.
|
|
6116
|
+
|
|
6117
|
+
For iOS Simulator camera input, inspect setup with \`camera_diagnose\`. Use \`camera_source\`
|
|
6118
|
+
for supported barcode, file or screen input, \`camera_devices\` for simulators/apps and
|
|
6119
|
+
\`camera_inputs\` for local sources. \`camera_launch\` attaches input when launching the app;
|
|
6120
|
+
\`camera_zero_setup\` configures attachment for another launcher. \`camera_status\` reports
|
|
6121
|
+
available source/decoder status, which does not establish the app's scanner result.
|
|
6122
|
+
Use \`camera_stop\` when finished. These tools operate locally without an in-app Buoy
|
|
6123
|
+
integration, but require their supported host, simulator and account setup.
|
|
6124
|
+
|
|
6125
|
+
\`screenshot_component\` locates a supported component by testID/name and attempts a crop.
|
|
6126
|
+
It requires the MCP server on the same Mac as the booted iOS Simulator and the component
|
|
6127
|
+
inspection setup. Inspect the result before using it as visual evidence.
|
|
6128
|
+
|
|
6129
|
+
For other supported operations, use \`list_tool_actions\`, \`get_snapshot\` or \`call_action\`.
|
|
6130
|
+
Snapshots describe available current state and can be large; request only needed data.
|
|
6131
|
+
|
|
6132
|
+
After a behavior change, repeat the affected interaction and check the expected result.
|
|
6133
|
+
Include restart or persistence checks when relevant to the change. Use appropriate static
|
|
6134
|
+
checks for documentation-only work. Report actual checks, observations and remaining limits.
|
|
6135
|
+
BUOY_VERIFY reminders track selected edits and tool calls; clearing a reminder is not a
|
|
6136
|
+
test result. Stay within the user's authorized task and preserve existing project work.`;
|
|
5864
6137
|
var VERIFYING_TOOLS = /* @__PURE__ */ new Set([
|
|
5865
6138
|
"measure_renders",
|
|
5866
6139
|
"scenario_action",
|
|
@@ -5933,9 +6206,9 @@ var deviceIdField = {
|
|
|
5933
6206
|
|
|
5934
6207
|
// src/init.ts
|
|
5935
6208
|
var import_node_child_process3 = require("child_process");
|
|
5936
|
-
var
|
|
5937
|
-
var
|
|
5938
|
-
var
|
|
6209
|
+
var import_node_fs6 = __toESM(require("fs"));
|
|
6210
|
+
var import_node_os5 = __toESM(require("os"));
|
|
6211
|
+
var import_node_path6 = __toESM(require("path"));
|
|
5939
6212
|
function defaultEnv() {
|
|
5940
6213
|
return { BUOY_VERIFY: "auto" };
|
|
5941
6214
|
}
|
|
@@ -5943,34 +6216,34 @@ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
|
|
|
5943
6216
|
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5944
6217
|
function runInit(opts = {}) {
|
|
5945
6218
|
const cwd = opts.cwd ?? process.cwd();
|
|
5946
|
-
const skillSrcDir = opts.skillSrcDir ??
|
|
6219
|
+
const skillSrcDir = opts.skillSrcDir ?? import_node_path6.default.resolve(__dirname, "../skill");
|
|
5947
6220
|
const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
|
|
5948
6221
|
const entry = resolveEntry(opts, log);
|
|
5949
6222
|
const done = [];
|
|
5950
|
-
upsertMcpConfig(
|
|
5951
|
-
|
|
5952
|
-
upsertMcpConfig(
|
|
5953
|
-
if (
|
|
5954
|
-
upsertMcpConfig(
|
|
6223
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
|
|
6224
|
+
import_node_fs6.default.mkdirSync(import_node_path6.default.join(cwd, ".cursor"), { recursive: true });
|
|
6225
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
|
|
6226
|
+
if (import_node_fs6.default.existsSync(import_node_path6.default.join(cwd, ".vscode"))) {
|
|
6227
|
+
upsertMcpConfig(import_node_path6.default.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
|
|
5955
6228
|
}
|
|
5956
6229
|
installSkills(skillSrcDir, cwd, done);
|
|
5957
|
-
appendDebuggingGuide(
|
|
5958
|
-
appendDebuggingGuide(
|
|
5959
|
-
log("Buoy MCP
|
|
6230
|
+
appendDebuggingGuide(import_node_path6.default.join(cwd, "CLAUDE.md"), cwd, done);
|
|
6231
|
+
appendDebuggingGuide(import_node_path6.default.join(cwd, "AGENTS.md"), cwd, done);
|
|
6232
|
+
log("Buoy MCP configuration written:");
|
|
5960
6233
|
for (const line of done) log(" \u2022 " + line);
|
|
5961
6234
|
log("");
|
|
5962
6235
|
log("Next steps:");
|
|
5963
6236
|
log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
|
|
5964
|
-
log(" 2.
|
|
5965
|
-
log(
|
|
6237
|
+
log(" 2. Complete account setup, then open an app with the required Buoy tools and sync integration.");
|
|
6238
|
+
log(" 3. Confirm the device and tools are available. MCP data/action access requires Pro.");
|
|
5966
6239
|
if (entry.command === "npx") {
|
|
5967
6240
|
log("");
|
|
5968
|
-
log(`
|
|
5969
|
-
log(`
|
|
6241
|
+
log(`The editor launches the server with \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
|
|
6242
|
+
log(`Registry access may be needed. Re-run \`npx -y ${PACKAGE_NAME}@latest init\` to refresh configuration.`);
|
|
5970
6243
|
} else if (!opts.cliPath) {
|
|
5971
6244
|
log("");
|
|
5972
6245
|
log(`This uses a pinned local install (no network on startup). To update it later,`);
|
|
5973
|
-
log(`
|
|
6246
|
+
log(`To update the local package, run: npx -y ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}@latest init --local`);
|
|
5974
6247
|
}
|
|
5975
6248
|
}
|
|
5976
6249
|
function resolveEntry(opts, log) {
|
|
@@ -5997,7 +6270,7 @@ function npxEntry() {
|
|
|
5997
6270
|
return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`], env: defaultEnv() };
|
|
5998
6271
|
}
|
|
5999
6272
|
function localEntry(opts, log) {
|
|
6000
|
-
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ??
|
|
6273
|
+
const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path6.default.join(import_node_os5.default.homedir(), ".buoy-mcp");
|
|
6001
6274
|
const registry = opts.registry ?? PUBLIC_REGISTRY;
|
|
6002
6275
|
const cliPath2 = installLocalCopy(installDir, registry, log);
|
|
6003
6276
|
if (cliPath2) {
|
|
@@ -6008,7 +6281,7 @@ function localEntry(opts, log) {
|
|
|
6008
6281
|
}
|
|
6009
6282
|
function installLocalCopy(installDir, registry, log) {
|
|
6010
6283
|
const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
|
|
6011
|
-
|
|
6284
|
+
import_node_fs6.default.mkdirSync(installDir, { recursive: true });
|
|
6012
6285
|
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
6013
6286
|
log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
|
|
6014
6287
|
const res = (0, import_node_child_process3.spawnSync)(
|
|
@@ -6031,8 +6304,8 @@ function installLocalCopy(installDir, registry, log) {
|
|
|
6031
6304
|
log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
|
|
6032
6305
|
return null;
|
|
6033
6306
|
}
|
|
6034
|
-
const cliPath2 =
|
|
6035
|
-
if (!
|
|
6307
|
+
const cliPath2 = import_node_path6.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
|
|
6308
|
+
if (!import_node_fs6.default.existsSync(cliPath2)) {
|
|
6036
6309
|
log(`\u26A0 install finished but ${cliPath2} is missing.`);
|
|
6037
6310
|
return null;
|
|
6038
6311
|
}
|
|
@@ -6084,129 +6357,109 @@ fetch(url, { method: 'HEAD', signal: c.signal })
|
|
|
6084
6357
|
`;
|
|
6085
6358
|
function upsertMcpConfig(file, key, entry, cwd, done) {
|
|
6086
6359
|
let json3 = {};
|
|
6087
|
-
if (
|
|
6360
|
+
if (import_node_fs6.default.existsSync(file)) {
|
|
6088
6361
|
try {
|
|
6089
|
-
json3 = JSON.parse(
|
|
6362
|
+
json3 = JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
|
|
6090
6363
|
} catch {
|
|
6091
|
-
|
|
6364
|
+
import_node_fs6.default.copyFileSync(file, file + ".bak");
|
|
6092
6365
|
done.push(`existing ${rel(file, cwd)} was invalid JSON \u2014 backed up to ${rel(file, cwd)}.bak`);
|
|
6093
6366
|
json3 = {};
|
|
6094
6367
|
}
|
|
6095
6368
|
} else {
|
|
6096
|
-
|
|
6369
|
+
import_node_fs6.default.mkdirSync(import_node_path6.default.dirname(file), { recursive: true });
|
|
6097
6370
|
}
|
|
6098
6371
|
const servers = json3[key] && typeof json3[key] === "object" ? json3[key] : {};
|
|
6099
6372
|
const existed = "buoy" in servers;
|
|
6100
6373
|
servers.buoy = entry;
|
|
6101
6374
|
json3[key] = servers;
|
|
6102
|
-
|
|
6375
|
+
import_node_fs6.default.writeFileSync(file, JSON.stringify(json3, null, 2) + "\n");
|
|
6103
6376
|
done.push(`${existed ? "updated" : "added"} \`buoy\` in ${rel(file, cwd)}`);
|
|
6104
6377
|
}
|
|
6105
6378
|
var GUIDE_HEADING = "# Debugging with Buoy";
|
|
6106
6379
|
var DEBUGGING_GUIDE = `${GUIDE_HEADING}
|
|
6107
6380
|
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
- \`run_flow
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
- \`
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
- \`
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
Saying "fixed" is a claim about how the app behaves, so check it the same way
|
|
6141
|
-
you'd check anything else about the app \u2014 by looking at it. Re-run the exact
|
|
6142
|
-
interaction that was broken and report what the numbers did:
|
|
6143
|
-
|
|
6144
|
-
- \`measure_renders\` with the same \`steps\` and \`compareToPrevious: true\` for a
|
|
6145
|
-
performance change \u2014 it prints the before/after delta and flags a change that
|
|
6146
|
-
made things worse.
|
|
6147
|
-
- \`run_flow\` re-driving the broken screen for a behaviour change.
|
|
6148
|
-
|
|
6149
|
-
Two things this catches that reading cannot: a fix that is right but incomplete
|
|
6150
|
-
(the value was wrong in three places and you corrected one), and a fix that
|
|
6151
|
-
moved the cost instead of removing it \u2014 the usual failure mode for re-render
|
|
6152
|
-
work, where a page gets faster to type in and slower to tap.
|
|
6153
|
-
|
|
6154
|
-
If the app changed since anything was last checked, Buoy will say so in a tool
|
|
6155
|
-
result. Whether that's worth acting on is your call \u2014 a doc edit isn't worth a
|
|
6156
|
-
device check, a state-management change is. Set \`BUOY_VERIFY\` in the MCP config
|
|
6157
|
-
to \`always\` (repeat until checked) or \`never\` (silent) to change how often it
|
|
6158
|
-
mentions it.
|
|
6381
|
+
Use Buoy to inspect a connected app when runtime evidence helps the task.
|
|
6382
|
+
Complete account, tool integration and connection setup first. MCP data and
|
|
6383
|
+
action access requires Pro. Follow the user's scope and repository instructions.
|
|
6384
|
+
|
|
6385
|
+
- \`get_triage\`: summarize available device and tool observations, then choose
|
|
6386
|
+
a relevant tool. Missing data is not proof that no problem occurred.
|
|
6387
|
+
- \`run_flow\`: run supported tap, type, wait and assertion steps to reproduce
|
|
6388
|
+
a failure or check a change. Review the flow before running it.
|
|
6389
|
+
- \`get_events\`: inspect retained events from connected tools.
|
|
6390
|
+
- \`get_network_requests\`: inspect captured requests and responses. Capture
|
|
6391
|
+
depends on installed hooks, transport and when recording began.
|
|
6392
|
+
- \`network_conditions\`: take the app offline or add latency to check error
|
|
6393
|
+
and retry paths. Development builds only. Restore \`normal\` when finished.
|
|
6394
|
+
- \`get_console\`: inspect retained console and error output. A terminated or
|
|
6395
|
+
disconnected app may not provide new data.
|
|
6396
|
+
- \`get_storage\`: inspect registered storage backends and available keys.
|
|
6397
|
+
- \`reload_app\`: reload where supported. Confirm the device remains connected;
|
|
6398
|
+
a reload does not guarantee recovery from a crash.
|
|
6399
|
+
- \`get_snapshot\`: inspect the available state of a specific tool.
|
|
6400
|
+
|
|
6401
|
+
After a behavior change, repeat the failing interaction when the environment
|
|
6402
|
+
allows it. Use relevant tests and report what was actually checked. For a
|
|
6403
|
+
performance change, compare \`measure_renders\` runs with the same steps and
|
|
6404
|
+
\`compareToPrevious: true\`; inspect the measurements and their limitations.
|
|
6405
|
+
Use \`run_flow\` for supported UI checks. Do not claim a fix was verified when
|
|
6406
|
+
only source review or a build was performed.
|
|
6407
|
+
|
|
6408
|
+
\`BUOY_VERIFY\` controls verification reminders: \`auto\` uses edit-based
|
|
6409
|
+
reminders, \`always\` repeats until checked, and \`never\` disables them.
|
|
6410
|
+
A reminder is not a test result. Choose checks appropriate to the change;
|
|
6411
|
+
prose-only edits do not require running device actions.
|
|
6159
6412
|
`;
|
|
6160
6413
|
function appendDebuggingGuide(file, cwd, done) {
|
|
6161
6414
|
let existing = "";
|
|
6162
|
-
if (
|
|
6415
|
+
if (import_node_fs6.default.existsSync(file)) {
|
|
6163
6416
|
try {
|
|
6164
|
-
existing =
|
|
6417
|
+
existing = import_node_fs6.default.readFileSync(file, "utf8");
|
|
6165
6418
|
} catch {
|
|
6166
6419
|
done.push(`\u26A0 could not read ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
6167
6420
|
return;
|
|
6168
6421
|
}
|
|
6169
6422
|
if (existing.includes(GUIDE_HEADING)) {
|
|
6170
|
-
done.push(`${rel(file, cwd)} already has the "${GUIDE_HEADING}" block
|
|
6423
|
+
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.`);
|
|
6171
6424
|
return;
|
|
6172
6425
|
}
|
|
6173
6426
|
}
|
|
6174
6427
|
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6175
6428
|
try {
|
|
6176
|
-
|
|
6429
|
+
import_node_fs6.default.writeFileSync(file, existing + separator + DEBUGGING_GUIDE);
|
|
6177
6430
|
done.push(`${existing ? "appended" : "created"} "${GUIDE_HEADING}" block in ${rel(file, cwd)}`);
|
|
6178
6431
|
} catch {
|
|
6179
6432
|
done.push(`\u26A0 could not write ${rel(file, cwd)} \u2014 skipped the debugging guide`);
|
|
6180
6433
|
}
|
|
6181
6434
|
}
|
|
6182
6435
|
function installSkills(skillSrcDir, cwd, done) {
|
|
6183
|
-
if (!
|
|
6436
|
+
if (!import_node_fs6.default.existsSync(skillSrcDir)) {
|
|
6184
6437
|
done.push("\u26A0 no bundled skills found \u2014 skipped");
|
|
6185
6438
|
return;
|
|
6186
6439
|
}
|
|
6187
|
-
const skills =
|
|
6440
|
+
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")));
|
|
6188
6441
|
if (skills.length === 0) {
|
|
6189
6442
|
done.push("\u26A0 no skills with a SKILL.md found \u2014 skipped");
|
|
6190
6443
|
return;
|
|
6191
6444
|
}
|
|
6192
6445
|
for (const skill of skills) {
|
|
6193
|
-
const from =
|
|
6194
|
-
const to =
|
|
6446
|
+
const from = import_node_path6.default.join(skillSrcDir, skill.name);
|
|
6447
|
+
const to = import_node_path6.default.join(cwd, ".claude", "skills", skill.name);
|
|
6195
6448
|
copyDir(from, to);
|
|
6196
|
-
done.push(`installed the ${skill.name} skill \u2192 ${rel(
|
|
6449
|
+
done.push(`installed the ${skill.name} skill \u2192 ${rel(import_node_path6.default.join(to, "SKILL.md"), cwd)}`);
|
|
6197
6450
|
}
|
|
6198
6451
|
}
|
|
6199
6452
|
function copyDir(from, to) {
|
|
6200
|
-
|
|
6201
|
-
for (const entry of
|
|
6202
|
-
const s =
|
|
6203
|
-
const d =
|
|
6453
|
+
import_node_fs6.default.mkdirSync(to, { recursive: true });
|
|
6454
|
+
for (const entry of import_node_fs6.default.readdirSync(from, { withFileTypes: true })) {
|
|
6455
|
+
const s = import_node_path6.default.join(from, entry.name);
|
|
6456
|
+
const d = import_node_path6.default.join(to, entry.name);
|
|
6204
6457
|
if (entry.isDirectory()) copyDir(s, d);
|
|
6205
|
-
else
|
|
6458
|
+
else import_node_fs6.default.copyFileSync(s, d);
|
|
6206
6459
|
}
|
|
6207
6460
|
}
|
|
6208
6461
|
function rel(file, cwd) {
|
|
6209
|
-
const r =
|
|
6462
|
+
const r = import_node_path6.default.relative(cwd, file);
|
|
6210
6463
|
return r && !r.startsWith("..") ? r : file;
|
|
6211
6464
|
}
|
|
6212
6465
|
|