@buoy-gg/mcp 7.0.78 → 7.0.79
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 +10 -0
- package/dist/SyncClient-gqMRmB79.d.mts +374 -0
- package/dist/SyncClient-gqMRmB79.d.ts +374 -0
- package/dist/{chunk-LHGIYW7N.mjs → chunk-3FTJ74HL.mjs} +1 -1
- package/dist/{chunk-XA74T4JN.mjs → chunk-5XATNNKY.mjs} +2 -2
- package/dist/{chunk-ODGXMS2O.mjs → chunk-FYJWWTAC.mjs} +5 -4
- package/dist/chunk-HZEASQD3.mjs +980 -0
- package/dist/{chunk-2IWYZICF.mjs → chunk-MR4NVULE.mjs} +1 -1
- package/dist/{chunk-3STZVJQG.mjs → chunk-SGVMY5TU.mjs} +1 -1
- package/dist/chunk-TMN3LXKB.mjs +42 -0
- package/dist/cli.js +285 -222
- package/dist/cli.mjs +12 -12
- package/dist/flowRun-4VU3KXYX.mjs +2 -0
- package/dist/index.d.mts +9 -370
- package/dist/index.d.ts +9 -370
- package/dist/index.js +231 -168
- package/dist/index.mjs +1 -1
- package/dist/{init-VCQMVZXB.mjs → init-6ANI6DIF.mjs} +1 -1
- package/dist/{main-6OX5577P.mjs → main-XP2I2BCD.mjs} +2 -2
- package/dist/nativeProfiler.d.mts +184 -0
- package/dist/nativeProfiler.d.ts +184 -0
- package/dist/nativeProfiler.js +157 -0
- package/dist/nativeProfiler.mjs +1 -0
- package/dist/{simFarm-R4P6QXRK.mjs → simFarm-AWVHVU6A.mjs} +1 -1
- package/dist/{src-R7ICMP6Q.mjs → src-HRUK7GEE.mjs} +1 -1
- package/package.json +16 -5
- package/dist/chunk-HEPFVYZG.mjs +0 -959
- package/dist/flowRun-XEK65IL7.mjs +0 -2
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { ToolCapability } from '@buoy-gg/sync-broker';
|
|
2
|
+
|
|
3
|
+
type AccessLevel = "free" | "pro";
|
|
4
|
+
|
|
5
|
+
interface SyncClientOptions {
|
|
6
|
+
/** Token supplied by an embedded host. Never read or write a token file. */
|
|
7
|
+
dashboardToken?: string;
|
|
8
|
+
/** Broker URL to connect to. @default http://localhost:42831 */
|
|
9
|
+
url?: string;
|
|
10
|
+
/** Broker port to probe/spawn on. @default 42831 */
|
|
11
|
+
port?: number;
|
|
12
|
+
/**
|
|
13
|
+
* If no broker is already running on the port, spawn one in-process so the
|
|
14
|
+
* MCP server works standalone (no desktop app needed). @default true
|
|
15
|
+
*/
|
|
16
|
+
autoSpawnBroker?: boolean;
|
|
17
|
+
/** Default per-action timeout. @default 15000 */
|
|
18
|
+
actionTimeoutMs?: number;
|
|
19
|
+
/** Optional logger — writes to stderr by default (stdout is the MCP channel). */
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
interface DeviceInfo {
|
|
23
|
+
deviceId: string;
|
|
24
|
+
deviceName: string;
|
|
25
|
+
platform?: string;
|
|
26
|
+
isConnected: boolean;
|
|
27
|
+
/** Whether the connected app reported an active Buoy Pro license. */
|
|
28
|
+
isPro: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The app's tier after admission. New sockets are "checking"; an account
|
|
31
|
+
* lock is "locked". Older apps report only `isPro`, which maps to Pro
|
|
32
|
+
* or locked because keys could only grant Pro before the tier split.
|
|
33
|
+
*
|
|
34
|
+
* Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
|
|
35
|
+
* the free tier" and "no admitted account" are different problems.
|
|
36
|
+
*/
|
|
37
|
+
tier: "checking" | "locked" | "free" | "pro";
|
|
38
|
+
/**
|
|
39
|
+
* More than one app is registered under this id right now (a pinned
|
|
40
|
+
* `externalSync.deviceId` copied onto two devices). Actions sent to it
|
|
41
|
+
* land on an arbitrary one. Broker-computed; false for apps too old to
|
|
42
|
+
* announce a runtime instance id.
|
|
43
|
+
*/
|
|
44
|
+
collision: boolean;
|
|
45
|
+
tools: ToolCapability[];
|
|
46
|
+
}
|
|
47
|
+
/** How the client obtained its broker connection. */
|
|
48
|
+
type BrokerMode = "connected" | "spawned";
|
|
49
|
+
/**
|
|
50
|
+
* A headless "Dashboard" client of the Buoy sync broker — the same role the
|
|
51
|
+
* desktop app plays, but driven programmatically by the MCP server. Implements
|
|
52
|
+
* connect-or-spawn: connects to a running broker (e.g. the desktop app) if one
|
|
53
|
+
* is up, otherwise hosts its own in-process so it works standalone.
|
|
54
|
+
*/
|
|
55
|
+
declare class SyncClient {
|
|
56
|
+
private socket;
|
|
57
|
+
private admitted;
|
|
58
|
+
/** When this client was made: the startup window for {@link awaitBroker}. */
|
|
59
|
+
private readonly createdAt;
|
|
60
|
+
private lockedDevices;
|
|
61
|
+
private hostAccount;
|
|
62
|
+
private broker;
|
|
63
|
+
private readonly url;
|
|
64
|
+
private readonly port;
|
|
65
|
+
private readonly actionTimeoutMs;
|
|
66
|
+
private readonly log;
|
|
67
|
+
/** Latest capabilities announce per deviceId. */
|
|
68
|
+
private capabilities;
|
|
69
|
+
/** Latest device list from the broker's `all-devices-update`. */
|
|
70
|
+
private devices;
|
|
71
|
+
/**
|
|
72
|
+
* The device this session last named explicitly. Agents name a device once,
|
|
73
|
+
* then leave deviceId out on later calls; with several apps connected every
|
|
74
|
+
* one of those calls failed ("Multiple devices connected"), which cost a
|
|
75
|
+
* turn at the start of almost every benchmark run. An explicit id sets it,
|
|
76
|
+
* and so does the project-folder match in resolveDevice. It never switches on its own: if that device goes away, calls
|
|
77
|
+
* without an id fail instead of picking another app.
|
|
78
|
+
*/
|
|
79
|
+
private sessionDevice;
|
|
80
|
+
/** Latest snapshot payload per `${deviceId}::${toolId}`. */
|
|
81
|
+
private snapshots;
|
|
82
|
+
/**
|
|
83
|
+
* Capture-mode watches this client holds, keyed by snapKey. Stored with
|
|
84
|
+
* their addressing because they have to be RE-SENT on every reconnect: the
|
|
85
|
+
* broker keys watch state by socket id, and a reconnect gets a new one.
|
|
86
|
+
*/
|
|
87
|
+
private watching;
|
|
88
|
+
/** In-flight actions awaiting a result, keyed by requestId. */
|
|
89
|
+
private pendingActions;
|
|
90
|
+
/** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
|
|
91
|
+
private snapshotWaiters;
|
|
92
|
+
/** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
|
|
93
|
+
private snapshotAt;
|
|
94
|
+
/** Liveness per device — see {@link noteTimeout}. */
|
|
95
|
+
private health;
|
|
96
|
+
/** Dev-server origin per device — see {@link noteDevServerOrigin}. */
|
|
97
|
+
private devServerOrigins;
|
|
98
|
+
/** Newest crash-report console entry id seen per device — see {@link noteCrashReport}. */
|
|
99
|
+
private crashSeen;
|
|
100
|
+
/** Lookups still working out a device's dev-server folder (origin probe, then lsof). */
|
|
101
|
+
private rootLookups;
|
|
102
|
+
/** Folder each device's dev server runs from — see {@link devServerRoot}. */
|
|
103
|
+
private devServerRoots;
|
|
104
|
+
/** Devices whose origin probe already ran (a production bundle has none). */
|
|
105
|
+
private originProbed;
|
|
106
|
+
/** Last time each device appeared in the broker's device list. */
|
|
107
|
+
private lastSeen;
|
|
108
|
+
private readonly dashboardToken?;
|
|
109
|
+
constructor(opts?: SyncClientOptions);
|
|
110
|
+
private readonly autoSpawn;
|
|
111
|
+
/** Connect to a running broker, or spawn one in-process, then handshake. */
|
|
112
|
+
start(): Promise<BrokerMode>;
|
|
113
|
+
private connectSocket;
|
|
114
|
+
/**
|
|
115
|
+
* Drop everything remembered about devices that have been gone a while.
|
|
116
|
+
*
|
|
117
|
+
* Deliberately NOT on disconnect: a crashed app often drops off the broker,
|
|
118
|
+
* and its cached console snapshot is the crash evidence — throwing that away
|
|
119
|
+
* the instant it disconnects would delete the answer. But device ids change
|
|
120
|
+
* with every simulator erase and fresh dev client, and each one can hold
|
|
121
|
+
* multi-MB snapshots, so a long-lived server has to forget eventually.
|
|
122
|
+
*/
|
|
123
|
+
/** Whether the broker admitted this host's Buoy account. */
|
|
124
|
+
isAdmitted(): boolean;
|
|
125
|
+
private requireAdmission;
|
|
126
|
+
private transportLost;
|
|
127
|
+
private setAdmission;
|
|
128
|
+
private requireDeviceAccount;
|
|
129
|
+
private lockDevice;
|
|
130
|
+
private pruneStaleDevices;
|
|
131
|
+
/** Disconnect and tear down a spawned broker, if any. */
|
|
132
|
+
stop(): Promise<void>;
|
|
133
|
+
/** All devices the broker knows about, merged with their advertised tools. */
|
|
134
|
+
listDevices(): DeviceInfo[];
|
|
135
|
+
/** Connected devices only. */
|
|
136
|
+
connectedDevices(): DeviceInfo[];
|
|
137
|
+
/** Actions advertised for a tool, with the devices offering it. */
|
|
138
|
+
toolActions(toolId: string): {
|
|
139
|
+
deviceId: string;
|
|
140
|
+
deviceName: string;
|
|
141
|
+
actions: string[];
|
|
142
|
+
}[];
|
|
143
|
+
/**
|
|
144
|
+
* This project's own connected app, if the broker has admitted it. Picks
|
|
145
|
+
* nothing for the session; for warming up host tools before the first call.
|
|
146
|
+
*/
|
|
147
|
+
peekProjectDevice(): string | null;
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a concrete target deviceId. If `deviceId` is given it's used as-is.
|
|
150
|
+
* Otherwise, if exactly one device is connected, that one is chosen; if none
|
|
151
|
+
* or many, an informative error is thrown (callers can pass "All" explicitly).
|
|
152
|
+
*/
|
|
153
|
+
resolveDevice(deviceId?: string): string;
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a target device AND require the tier the current tool call needs.
|
|
156
|
+
* Most tools need Buoy Pro; the free ones (access/policy.ts) also take an app
|
|
157
|
+
* on the free tier. The level comes from the call's scope, set centrally in
|
|
158
|
+
* server.ts before any handler runs, so a handler can't pick its own. With no
|
|
159
|
+
* scope (work outside any tool call) the level is Pro. The app's own license
|
|
160
|
+
* check is the source of truth (the device reports it over the broker); this
|
|
161
|
+
* just reads it.
|
|
162
|
+
*
|
|
163
|
+
* `list_devices` deliberately does NOT call this — a user without access can
|
|
164
|
+
* still see their device and its tier, which explains what is blocked.
|
|
165
|
+
*/
|
|
166
|
+
requireProDevice(deviceId?: string): string;
|
|
167
|
+
/**
|
|
168
|
+
* The check under every app call (callAction, getSnapshot, the snapshot
|
|
169
|
+
* cache, lastSync), on top of requireProDevice. A call to a Pro app always
|
|
170
|
+
* passes. A call to any other app passes only inside a free tool call and
|
|
171
|
+
* only for the app calls listed in access/policy.ts, so a Pro tool that
|
|
172
|
+
* forgot requireProDevice still can't act on a free app, and a free tool
|
|
173
|
+
* can't reach Pro data by any path. Work outside any tool call (reconnects,
|
|
174
|
+
* health pings) is plumbing and passes.
|
|
175
|
+
*/
|
|
176
|
+
private checkAppCall;
|
|
177
|
+
/**
|
|
178
|
+
* A free app's tap must land on the app, not on Buoy's own panels: their
|
|
179
|
+
* controls do what Pro tools do. The app side leaves Buoy out of fuzzy
|
|
180
|
+
* matches but lets an exact testID or nativeTag through, so on a free app an
|
|
181
|
+
* exact tap goes through only when the app-only screen list shows that
|
|
182
|
+
* target. Anything this can't prove (a Buoy control, an off-screen target, a
|
|
183
|
+
* failed read, a broadcast to "All") is refused. Resolves to replacement
|
|
184
|
+
* params when it narrows the tap (never edits the caller's object, which a
|
|
185
|
+
* flow reuses on retries). Returns null, without an await, for every call it
|
|
186
|
+
* doesn't apply to.
|
|
187
|
+
*/
|
|
188
|
+
private refuseBuoyPanelTap;
|
|
189
|
+
/**
|
|
190
|
+
* Whether a result for this device may carry Pro's why-data (render names,
|
|
191
|
+
* script names, JS origins). True for a Pro key or the Weekend Pass. A free
|
|
192
|
+
* tool works on a free app too; this decides how much of its result it
|
|
193
|
+
* gets (access/freeView.ts).
|
|
194
|
+
*/
|
|
195
|
+
fullResultsFor(deviceId: string): boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Whether this device was seen on a tier good enough for the current call,
|
|
198
|
+
* connected or not. For host controls that keep working after the app
|
|
199
|
+
* quits or crashes (see hostKit), and for the last-sync fallback.
|
|
200
|
+
*/
|
|
201
|
+
deviceWasAllowed(deviceId: string): boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Where a device stands for `level` (the current call's by default):
|
|
204
|
+
* connected and allowed ("ready"), not connected ("offline"), connected but
|
|
205
|
+
* its account not announced yet ("pending"), or announced on a tier without
|
|
206
|
+
* this access ("denied").
|
|
207
|
+
*/
|
|
208
|
+
accessState(deviceId: string, level?: AccessLevel): "ready" | "offline" | "pending" | "denied";
|
|
209
|
+
private healthOf;
|
|
210
|
+
/** The device answered — it's alive. */
|
|
211
|
+
private noteOk;
|
|
212
|
+
/**
|
|
213
|
+
* The device didn't answer. A crashed app never answers again, so after the
|
|
214
|
+
* first miss we stop paying the full timeout on every subsequent call.
|
|
215
|
+
*/
|
|
216
|
+
private noteTimeout;
|
|
217
|
+
/** True once a device has missed TWO calls and hasn't answered since. */
|
|
218
|
+
isUnresponsive(deviceId: string): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Full timeout normally; a short one for a device already not answering.
|
|
221
|
+
*
|
|
222
|
+
* A single missed call does not trigger the short budget — one slow
|
|
223
|
+
* `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
|
|
224
|
+
* the full budget (they are the recovery path).
|
|
225
|
+
*
|
|
226
|
+
* Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
|
|
227
|
+
* budget again so the short timeout cannot become self-sustaining.
|
|
228
|
+
*/
|
|
229
|
+
private timeoutFor;
|
|
230
|
+
/**
|
|
231
|
+
* Plain-language diagnosis for a device that isn't answering, including the
|
|
232
|
+
* app's last words if the console tool got a crash flush out in time.
|
|
233
|
+
*
|
|
234
|
+
* A silent timeout is the worst possible answer here: the app is on a redbox,
|
|
235
|
+
* the reason is already captured, and without this the caller reads "timed
|
|
236
|
+
* out" and keeps retrying the same dead device.
|
|
237
|
+
*/
|
|
238
|
+
unresponsiveNote(deviceId: string): string;
|
|
239
|
+
/**
|
|
240
|
+
* A fresh crash report from the console tool (its crash flush runs as the
|
|
241
|
+
* app's React tree dies) means actions waiting on that device will likely
|
|
242
|
+
* never be answered: navigate to a screen that throws used to sit out its
|
|
243
|
+
* full 15 s. Cut their wait to the short leash instead of failing them at
|
|
244
|
+
* once, and only after a ping goes unanswered: an ErrorBoundary can catch a
|
|
245
|
+
* render error and the app lives.
|
|
246
|
+
*/
|
|
247
|
+
private noteCrashReport;
|
|
248
|
+
/**
|
|
249
|
+
* The newest error line the console tool synced for this device, if any.
|
|
250
|
+
* Reads the cached console snapshot rather than the device (which, by the
|
|
251
|
+
* time this matters, is not answering).
|
|
252
|
+
*/
|
|
253
|
+
private lastFatalFor;
|
|
254
|
+
/** Age of the cached snapshot for a tool, in ms (null = never received). */
|
|
255
|
+
snapshotAgeMs(toolId: string, deviceId: string): number | null;
|
|
256
|
+
/**
|
|
257
|
+
* Remember where a device's bundle came from, reported by core's `app`
|
|
258
|
+
* adapter while the app is healthy. It is the only way to reload an app that
|
|
259
|
+
* has crashed: the in-app reload needs the React tree that the crash
|
|
260
|
+
* destroyed, but the dev server's reload is handled natively.
|
|
261
|
+
*/
|
|
262
|
+
noteDevServerOrigin(deviceId: string, origin: unknown): void;
|
|
263
|
+
/**
|
|
264
|
+
* Ask a freshly-announced device where its bundle came from, once.
|
|
265
|
+
*
|
|
266
|
+
* Deliberately eager: this is the piece of information that can ONLY be
|
|
267
|
+
* collected while the app is healthy, and is only ever needed after it has
|
|
268
|
+
* stopped answering. One cheap round-trip per device buys the ability to
|
|
269
|
+
* recover a crashed app instead of telling the caller to go do it by hand.
|
|
270
|
+
*/
|
|
271
|
+
private learnDevServerOrigin;
|
|
272
|
+
private trackRootLookup;
|
|
273
|
+
/**
|
|
274
|
+
* Wait (briefly) for project-folder lookups still running, so an agent's
|
|
275
|
+
* first call right after apps connect can still be matched to this
|
|
276
|
+
* project's app instead of failing with "Multiple devices connected". Only
|
|
277
|
+
* worth it when no device is chosen yet and several apps are connected.
|
|
278
|
+
*/
|
|
279
|
+
/**
|
|
280
|
+
* Right after the MCP starts, the broker may not have admitted this client
|
|
281
|
+
* yet: a first call then failed with "A verified Buoy account ... required"
|
|
282
|
+
* although the account is fine. Every call gives admission (and the first
|
|
283
|
+
* device list) a moment; once admitted this returns at once.
|
|
284
|
+
*/
|
|
285
|
+
awaitBroker(): Promise<void>;
|
|
286
|
+
settleDeviceChoice(maxMs?: number): Promise<void>;
|
|
287
|
+
/** The dev-server origin last reported by this device, if any. */
|
|
288
|
+
devServerOrigin(deviceId: string): string | null;
|
|
289
|
+
/**
|
|
290
|
+
* Keep the device RECORDING this tool, without asking it to stream.
|
|
291
|
+
*
|
|
292
|
+
* A dashboard watches for as long as its panel is open; this client is
|
|
293
|
+
* request/response, so it used to watch and unwatch around every single
|
|
294
|
+
* read. That works for a tool whose state just IS (the query cache, the env
|
|
295
|
+
* table) and is useless for one that records a timeline: the unwatch drops
|
|
296
|
+
* the device's subscriber count to zero, which stops capture, so nothing is
|
|
297
|
+
* ever recorded between two reads. The list comes back empty and reads as
|
|
298
|
+
* "the app made no requests" — a wrong conclusion that is very expensive to
|
|
299
|
+
* chase, because everything about the app looks fine.
|
|
300
|
+
*
|
|
301
|
+
* Capture-mode watches are held for the life of the connection and released
|
|
302
|
+
* on disconnect (the broker also drops them for us). They cost the device's
|
|
303
|
+
* ring buffer, not the socket: no snapshot is pushed, and reads still go
|
|
304
|
+
* through `request-devtool-state`.
|
|
305
|
+
*/
|
|
306
|
+
private ensureWatching;
|
|
307
|
+
private sendWatch;
|
|
308
|
+
/**
|
|
309
|
+
* Re-arm every held watch after a reconnect.
|
|
310
|
+
*
|
|
311
|
+
* The broker tracks watches per SOCKET, so a reconnect (broker restart, a
|
|
312
|
+
* dropped wifi link) silently discards all of ours while `watching` still
|
|
313
|
+
* says we hold them — `ensureWatching` would then short-circuit forever and
|
|
314
|
+
* the device would never record again. Silent and permanent: the exact
|
|
315
|
+
* failure this whole capture-mode change exists to remove.
|
|
316
|
+
*/
|
|
317
|
+
private rearmWatches;
|
|
318
|
+
/** Each device's current connection (socket id + nonce), to spot a reload. */
|
|
319
|
+
private deviceConnections;
|
|
320
|
+
/**
|
|
321
|
+
* Re-arm the watches for a device that came back on a NEW connection.
|
|
322
|
+
*
|
|
323
|
+
* A relaunch or JS reload gives the app a fresh socket and fresh adapters,
|
|
324
|
+
* whose capture starts switched off. Our watches still say "held", so
|
|
325
|
+
* `ensureWatching` short-circuited and the device never recorded again: after
|
|
326
|
+
* one restart, the storage timeline stayed empty for the rest of the session
|
|
327
|
+
* (found by the BK storage suite — every case after a relaunch read nothing).
|
|
328
|
+
*/
|
|
329
|
+
private rearmReconnectedDevices;
|
|
330
|
+
/** Stop streaming a tool (releases the device-side backpressure). */
|
|
331
|
+
unwatch(toolId: string, targetDeviceId: string): void;
|
|
332
|
+
/** Last snapshot received for a tool/device, if any. */
|
|
333
|
+
cachedSnapshot(toolId: string, deviceId: string): unknown;
|
|
334
|
+
/**
|
|
335
|
+
* Get a tool's current state: arm capture (once, for the connection), ask
|
|
336
|
+
* for state explicitly, and resolve with the snapshot that comes back.
|
|
337
|
+
* Falls back to the last cached snapshot if the request times out.
|
|
338
|
+
*
|
|
339
|
+
* The capture watch is deliberately NOT released afterwards — see
|
|
340
|
+
* `ensureWatching`. It costs the device nothing on the socket, and dropping
|
|
341
|
+
* it is what made every timeline tool look permanently empty from here.
|
|
342
|
+
*/
|
|
343
|
+
getSnapshot(toolId: string, deviceId?: string, opts?: {
|
|
344
|
+
timeoutMs?: number;
|
|
345
|
+
silent?: boolean;
|
|
346
|
+
}): Promise<{
|
|
347
|
+
deviceId: string;
|
|
348
|
+
payload: unknown;
|
|
349
|
+
stale?: string;
|
|
350
|
+
}>;
|
|
351
|
+
/**
|
|
352
|
+
* The last snapshot a device sent for a tool, kept after the device drops
|
|
353
|
+
* off. A fatal render error tears down the app's React tree, and Buoy's
|
|
354
|
+
* connection with it; @buoy-gg/console flushes its logs out first, so this
|
|
355
|
+
* is where the app's last words are once it can no longer be asked.
|
|
356
|
+
*/
|
|
357
|
+
lastSync(deviceId: string, toolId: string): {
|
|
358
|
+
payload: unknown;
|
|
359
|
+
ageMs: number | null;
|
|
360
|
+
} | undefined;
|
|
361
|
+
/** Header line for data served from cache because the device went quiet. */
|
|
362
|
+
private staleNote;
|
|
363
|
+
private waitForSnapshot;
|
|
364
|
+
/**
|
|
365
|
+
* Invoke a remote action on a device and await its result. Pass deviceId to
|
|
366
|
+
* target one device, or "All" to broadcast (resolves with the first responder).
|
|
367
|
+
*/
|
|
368
|
+
callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
|
|
369
|
+
timeoutMs?: number;
|
|
370
|
+
silent?: boolean;
|
|
371
|
+
}): Promise<unknown>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export { type BrokerMode as B, type DeviceInfo as D, SyncClient as S, type SyncClientOptions as a };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as A}from"module";const G=A(import.meta.url);import{PACKAGE_NAME as l,VERSION as v}from"./chunk-
|
|
1
|
+
import{createRequire as A}from"module";const G=A(import.meta.url);import{PACKAGE_NAME as l,VERSION as v}from"./chunk-SGVMY5TU.mjs";import{spawnSync as m}from"child_process";import s from"fs";import E from"os";import c from"path";function g(){return{BUOY_VERIFY:"auto"}}var w="https://registry.npmjs.org";var f=3e3;function R(e={}){const t=e.cwd??process.cwd();const r=e.skillSrcDir??c.resolve(__dirname,"../skill");const n=e.log??(a=>process.stdout.write(a+"\n"));const o=k(e,n);const i=[];d(c.join(t,".mcp.json"),"mcpServers",o,t,i);s.mkdirSync(c.join(t,".cursor"),{recursive:true});d(c.join(t,".cursor","mcp.json"),"mcpServers",o,t,i);if(s.existsSync(c.join(t,".vscode"))){d(c.join(t,".vscode","mcp.json"),"servers",o,t,i)}D(r,t,i);x(c.join(t,"CLAUDE.md"),t,i);x(c.join(t,"AGENTS.md"),t,i);n("Buoy MCP configuration written:");for(const a of i)n(" \u2022 "+a);n("");n("Next steps:");n(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");n(" 2. Sign in: run `npx --package=@buoy-gg/core buoy login` in your app's folder. Then open an app with the required Buoy tools and sync integration.");n(" 3. Confirm the device and tools are available. Free accounts get the basic MCP tools; Pro adds the rest.");n("");n(`Want your AI to use the web too? Run \`npx -y ${l}@latest init --web\` for Buoy Web (free, no account).`);if(o.command==="npx"){n("");n(`The editor launches the server with \`npx -y ${l}@latest\`, so it stays`);n(`Registry access may be needed. Re-run \`npx -y ${l}@latest init\` to refresh configuration.`)}else if(!e.cliPath){n("");n(`This uses a pinned local install (no network on startup). To update it later,`);n(`To update the local package, run: npx -y ${o.args[0]?.replace(/dist[/\\]cli\.js$/,"")??""}@latest init --local`)}}function k(e,t){if(e.cliPath){return{type:"stdio",command:"node",args:[e.cliPath],env:g()}}const r=e.mode??"auto";if(r==="npx"){return y()}if(r==="local"){return $(e,t)}const n=I();if(n.reachable){return y()}t(`\u26A0 npm registry ${n.registry?`(${n.registry}) `:""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`);return $(e,t)}function y(){return{type:"stdio",command:"npx",args:["-y",`${l}@latest`],env:g()}}function $(e,t){const r=e.installDir??process.env.BUOY_MCP_HOME??c.join(E.homedir(),".buoy-mcp");const n=e.registry??w;const o=C(r,n,t);if(o){return{type:"stdio",command:"node",args:[o],env:g()}}t("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");return y()}function C(e,t,r){const n=`${l}@${v==="0.0.0-dev"?"latest":v}`;s.mkdirSync(e,{recursive:true});const o=process.platform==="win32"?"npm.cmd":"npm";r(`Installing ${n} into ${e} from ${t} \u2026`);const i=m(o,["install",n,"--prefix",e,"--registry",t,`--@buoy-gg:registry=${t}`,"--no-audit","--no-fund","--loglevel=error"],{stdio:["ignore","ignore","inherit"],encoding:"utf8"});if(i.status!==0){r(`\u26A0 npm install exited with code ${i.status??"?"}${i.error?` (${i.error.message})`:""}.`);return null}const a=c.join(e,"node_modules",l,"dist","cli.js");if(!s.existsSync(a)){r(`\u26A0 install finished but ${a} is missing.`);return null}return a}function I(){const e=P();if(!e)return{reachable:false,registry:null};if(/^https?:\/\/registry\.npmjs\.org\/?$/i.test(e)){return{reachable:true,registry:e}}return{reachable:T(e),registry:e}}function P(){const e=S("@buoy-gg:registry");if(e)return e;const t=S("registry");return t??w}function S(e){const t=process.platform==="win32"?"npm.cmd":"npm";const r=m(t,["config","get",e],{encoding:"utf8",timeout:f});if(r.status!==0||typeof r.stdout!=="string")return null;const n=r.stdout.trim();if(!n||n==="undefined"||n==="null")return null;return n}function T(e){const t=m(process.execPath,["-e",F,e,String(f)],{encoding:"utf8",timeout:f+2e3});return t.status===0}var F=`
|
|
2
2
|
const url = process.argv[1];
|
|
3
3
|
const ms = Number(process.argv[2]) || 3000;
|
|
4
4
|
const c = new AbortController();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as U}from"module";const X=U(import.meta.url);import{FarmError as k,agentLabel as E,connectFarm as O,detectAgent as $,findDataContainer as G,nodeLauncher as j,readBuoyInstallId as C}from"./chunk-
|
|
1
|
+
import{createRequire as U}from"module";const X=U(import.meta.url);import{FarmError as k,agentLabel as E,connectFarm as O,detectAgent as $,findDataContainer as G,nodeLauncher as j,readBuoyInstallId as C}from"./chunk-FYJWWTAC.mjs";import{readEntitlement as N}from"./chunk-5NOZVPWB.mjs";import L from"os";import z from"path";import{z as o}from"zod";function A(t){if(N().admitted)return true;if(!t)return false;if(t.isAdmitted?.())return true;return t.connectedDevices().some(i=>i.isPro===true||i.tier==="free")}function P(t){return`\`${t}\` needs a Buoy account. A free one works.
|
|
2
2
|
|
|
3
3
|
Tell the user to sign in to Buoy Desktop, or run \`npx --package=@buoy-gg/core buoy login\` in the project. Then run this again.`}var W=t=>({content:[{type:"text",text:t}]});var f=t=>({isError:true,content:[{type:"text",text:t}]});var u=t=>W(JSON.stringify(t,null,2));var x;var I=t=>A(x)?null:f(P(t));var v=null;var R=()=>void 0;function _(){if(!v){v=$({clientName:R()}).catch(()=>void 0).then(t=>O({label:process.env.BUOY_AGENT_LABEL||(t?E(t):"Buoy MCP"),agent:t,launcher:j(process.argv[1],["farmd"])})).catch(t=>{v=null;throw t})}return v}async function l(t,i,e,r){return(await _()).call(t,i,{timeoutMs:e,requestId:r})}var c=[];function M(t){for(let i=c.length-1;i>=0;i--)if(t(c[i]))c.splice(i,1)}async function H(t,i){const e=()=>c.find(n=>n.buoyDeviceId&&t.endsWith(n.buoyDeviceId));let r=e();if(!r&&c.length){await K().catch(()=>void 0);r=e()}if(!r)return null;try{return{checked:true,...await l("fairness",{leaseId:r.leaseId,epoch:r.epoch,windows:i},3e4)}}catch(n){if(n instanceof k&&n.code==="BAD_REQUEST"&&/Unknown method/.test(n.message)){return{checked:false,why:"This farm is too old to check fairness. Restart Buoy Desktop when the farm is idle."}}return{checked:false,why:n instanceof k?`${n.code}: ${n.message}`:String(n?.message??n)}}}async function K(){const t=await l("status");const i=new Set(c.map(e=>e.leaseId));for(const e of t.android?.copies??[]){if(!e.leaseId||!i.has(e.leaseId))continue;let r=c.find(n=>n.udid===e.udid);if(!r){const n=c.find(s=>s.leaseId===e.leaseId);r={...n,udid:e.udid,buoyDeviceId:void 0};c.push(r)}if(e.buoyDeviceId)r.buoyDeviceId=e.buoyDeviceId}for(const e of t.sims){if(!e.leaseId||!i.has(e.leaseId))continue;let r=c.find(h=>h.udid===e.udid);if(!r){const h=c.find(b=>b.leaseId===e.leaseId);r={...h,udid:e.udid,buoyDeviceId:void 0};c.push(r)}if(e.buoyDeviceId){r.buoyDeviceId=e.buoyDeviceId;continue}const n=t.templates.find(h=>h.id===e.templateId)?.bundleId;const s=z.join(L.homedir(),"Library/Developer/CoreSimulator/Devices",e.udid,"data");const a=n?await G(s,n):null;const d=a?await C(a):null;if(d)r.buoyDeviceId=d}}function y(t){if(t instanceof k&&t.code==="RUN_STOPPED"){return f('RUN_STOPPED: The person stopped this run in Buoy Desktop (reason=user). Stop work on it. Tell them: "You stopped this run in Sims." Start a new run only if they ask.')}if(t instanceof k)return f(`${t.code}: ${t.message}${t.next?`
|
|
4
|
-
Next: ${t.next}`:""}`);return f(`Sim Farm error: ${t?.message??t}`)}var D={leaseId:o.string().describe("The lease id from sim_farm_acquire."),epoch:o.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};var S=o.string().optional().describe("Any unique string for this call. Repeating the same call with the same requestId returns the first result instead of acting twice.");var T=o.string().describe("The runId from sim_farm_acquire.");async function Y(t,i,e){const r=new Map;const n=Date.now()+e;for(;;){for(const s of i){if(r.has(s))continue;const a=t.connectedDevices().find(d=>d.deviceId.endsWith(s)&&d.tier!=="checking");if(a)r.set(s,a.deviceId)}if(r.size===i.length||Date.now()>=n)return r;await new Promise(s=>setTimeout(s,500))}}async function F(t){const i=await l("status");const e=t==="android"?i.android?.templates??[]:i.templates??[];return e.filter(r=>r.usable).sort((r,n)=>String(r.createdAt).localeCompare(String(n.createdAt))).pop()?.id}async function q(){(await _()).require("android")}var B=o.enum(["ios","android"]).optional().describe("ios (default) or android. Android copies are emulators started from a saved snapshot.");function J(t,i){R=()=>t.server.getClientVersion()?.name;x=i;t.registerTool("sim_farm_status",{description:"Show the Sim Farm: how many farm simulators exist, which are awake or in use and by whom, free memory and disk, how many more sims fit, templates, leases, and recent events. Farm sims are slim iOS simulators (about 1.5 GB each instead of 5) that the farm makes from a template. `android` lists Android templates (with `roomFor` and `costGB` per copy) and running Android copies: slim emulators that start from a saved snapshot in about 2 s with the app on them. Apps launch as a real phone (an iPhone SE, 2nd gen, by default): they see its RAM and cores and get its memory warnings. Each sim's `phone` shows the app's peak memory, and `overLimit: true` means a real phone would have closed the app. A safety cap closes an app that keeps growing. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:o.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:e})=>{try{return u(await l("status",{outside:!!e}))}catch(r){return y(r)}});t.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and, optionally, a seed folder (signed-in app data); clones are made from it in about 4 s each. Dev-client apps (Expo) need `launchUrl` so they open on Metro instead of the launcher. `grow` / `shrink` change how many sims exist. Use `dryRun` first: it returns the plan, what slim mode turns off (push, StoreKit, app links, widgets, Spotlight\u2026) and whether the Mac has room. You own the pool: Buoy Desktop has no size control. sim_farm_acquire makes sims as needed; `shrink` the ones you no longer need so they stop using disk space.",inputSchema:{mode:o.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size (iOS only)."),platform:B,apkPath:o.string().optional().describe("Android create: path to the app's .apk. A debug build lets the farm read each copy's Buoy id."),phone:o.string().optional().describe("Android create: the phone copies act like (RAM, cores, screen, app memory limit). Default android-3gb. See `farm android phone list`."),reverse:o.array(o.string().regex(/^\d{1,5}(:\d{1,5})?$/)).optional().describe('Android create: ports each copy forwards to this Mac, e.g. ["8081:8802"] so a dev build loads JS from Metro on 8802.'),templateId:o.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:o.number().int().min(1).max(64).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:o.string().optional().describe("For create: path to a simulator .app build."),seedDir:o.string().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:o.string().optional().describe("For create: URL opened after each launch, e.g. exp+my-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081."),deviceType:o.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:o.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:o.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:o.number().int().min(1).max(65535).optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:o.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId:S}},async e=>{const r=I("sim_farm_setup");if(r)return r;try{if(e.platform==="android"){await q();if(e.mode!=="create")return f("Android copies have no pool to grow or shrink: sim_farm_acquire starts them and release closes them.");if(!e.apkPath)return f("Android create needs apkPath.");const a={platform:"android",apk:e.apkPath,phoneId:e.phone,reverse:e.reverse,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(e.dryRun)return u({wouldCreateTemplate:a,note:"Needs the Android SDK first: npx @buoy-gg/mcp farm android setup (a person accepts Google's license there).",budget:await l("budget",{ramGB:4.3,diskGB:4})});return u(await l("setup",a,30*6e4,e.requestId))}if(e.mode==="create"){const a={appPath:e.appPath,seedDir:e.seedDir,deviceType:e.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:e.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:e.profile??"slim",metroPort:e.metroPort??8802,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(!a.appPath)return f("create needs appPath.");if(a.seedDir===void 0)return f("create needs seedDir, or seedDir: null for an app with no seed.");if(e.dryRun)return u({wouldCreateTemplate:a,budget:await l("budget",{ramGB:6,diskGB:4})});return u(await l("setup",a,15*6e4,e.requestId))}const n=e.templateId??await F("ios");if(!n)return f("No usable template yet. Run sim_farm_setup with mode create first.");const s=e.count??1;if(e.dryRun){const a=await l("budget",{ramGB:0,diskGB:e.mode==="grow"?2*s:0});return u({mode:e.mode,templateId:n,count:s,roomForMoreSims:a.roomFor,ok:a.ok,reason:a.reason})}return u(await l(e.mode,{templateId:n,count:s},30*6e4,e.requestId))}catch(n){return y(n)}});t.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. The person watches your work live in Buoy Desktop, so give a clear `goal` and a `labels` entry per sim. Returns a run (runId, deadlineAt), a lease (leaseId + epoch) and exact sims (UDID, name, and buoyDeviceId once the app ran: the 8-character tail that its list_devices id ends with; with launch, also deviceId, the full id to pass to other tools). Parked sims are booted and new ones cloned as needed, within the Mac's memory and disk limits. This MCP keeps the lease alive while it runs; release it with sim_farm_finish when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run. While working, send sim_farm_report when a sim starts, passes or fails. The run stops at deadlineAt unless you ask for more time with sim_farm_extend. If any call returns RUN_STOPPED, the person stopped you: stop and tell them.",inputSchema:{count:o.number().int().min(1).max(64).describe("How many sims. There is no fixed cap: the farm gives as many as this Mac's memory and disk allow. Use allowPartial to take what fits."),goal:o.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'. With runId, the run's goal is used if you leave this out."),labels:o.array(o.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:o.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:o.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:o.string().optional().describe("Old name for goal. Use goal."),templateId:o.string().optional().describe("Defaults to the newest usable template of the platform."),platform:B,launch:o.boolean().optional().describe("Launch the app on each sim and wait (up to about 30 s) until it joins Buoy. Each sim then has deviceId for other tools."),allowPartial:o.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:o.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:o.number().int().min(1).max(1440).optional().describe("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId:S}},async e=>{const r=I("sim_farm_acquire");if(r)return r;try{if(e.platform==="android")await q();const n=e.templateId??await F(e.platform??"ios");if(!n){return f(e.platform==="android"?"No usable Android template yet. Run sim_farm_setup with platform android, mode create and apkPath.":"No usable template yet. Run sim_farm_setup with mode create first.")}let s=(e.goal??e.purpose??"").trim();if(!s&&e.runId)s=((await l("status")).runs??[]).find(m=>m.id===e.runId)?.goal??"";if(!s)return f("Give a goal: one short sentence about what you are doing.");const a=await _();const d=await a.acquire({templateId:n,count:e.count,purpose:s,goal:s,labels:e.labels,kind:e.kind,runId:e.runId,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});for(const m of d.sims){const w=c.findIndex(g=>g.udid===m.udid);if(w>=0)c.splice(w,1);c.push({leaseId:d.leaseId,epoch:d.epoch,runId:d.runId,udid:m.udid,buoyDeviceId:m.buoyDeviceId??void 0})}let h=d;if(e.launch&&i){const m=d.sims.map(p=>p.buoyDeviceId).filter(p=>!!p);const w=m.length?await Y(i,m,3e4):new Map;h={...d,sims:d.sims.map(p=>({...p,deviceId:p.buoyDeviceId&&w.get(p.buoyDeviceId)||null}))};const g=d.sims.filter(p=>p.buoyDeviceId&&!w.has(p.buoyDeviceId));if(g.length){h.warning=`The app started on ${g.map(p=>p.name).join(", ")} but has not joined Buoy after 30 s. Run list_devices again in a moment, or take a screenshot with sim_farm_action to see the screen.`}}const b=e.launch?d.sims.filter(m=>!m.buoyDeviceId):[];if(b.length){const m=(await l("status").catch(()=>null))?.templates?.find(g=>g.id===n);const w=m?.metroPort?` on port ${m.metroPort}`:"";return u({...h,warning:`The app did not report a Buoy id on ${b.map(g=>g.name).join(", ")}. It may still be loading, or Metro${w} may be down or broken. Check Metro${w}, then take a screenshot with sim_farm_action to see the screen.`})}return u(h)}catch(n){return y(n)}});t.registerTool("sim_farm_release",{description:"Give one lease's sims back. `fast` (default) stops the app and restores its seeded data in about 2 s; `full` rebuilds each sim from the template. Use `full` after anything changed OS settings (permissions, keychain, location) or after a crash. Releasing your last lease ends the run, unless `keepRun` is true. To end a run with a result, use sim_farm_finish instead.",inputSchema:{...D,reset:o.enum(["fast","full"]).optional(),keepRun:o.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async e=>{try{const r=await _();const n=await r.release(e.leaseId,e.epoch,e.reset??"fast",e.keepRun===true);M(s=>s.leaseId===e.leaseId);return u(n)}catch(r){return y(r)}});t.registerTool("sim_farm_action",{description:"Act on sims in your lease: `screenshot` (returns file paths), `open-url` (deep link, pass `url`), `launch` the app, `reset` (restore seeded app data), or `rebuild` (fresh clone; the UDID changes, the result maps old to new). On an Android copy, `reset` and `rebuild` both swap in a fresh copy: the id changes and the result maps old to new. `install` puts a new .apk (pass `apkPath`) on Android copies; it is gone when the copy closes.",inputSchema:{...D,udids:o.array(o.string()).min(1).describe("UDIDs from your lease."),action:o.enum(["screenshot","open-url","launch","reset","rebuild","install"]),url:o.string().optional().describe("For open-url."),apkPath:o.string().optional().describe("For install (Android copies)."),requestId:S}},async e=>{const r=I("sim_farm_action");if(r)return r;try{const n=await l("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.action==="install"?e.apkPath:e.url},20*6e4,e.requestId);if(e.action==="reset"||e.action==="rebuild"){for(const[s,a]of Object.entries(n)){const d=c.find(h=>h.udid===s);if(d&&typeof a==="string"&&a!=="reset"&&a!==s)Object.assign(d,{udid:a,buoyDeviceId:void 0})}}return u(n)}catch(n){return y(n)}});t.registerTool("sim_farm_lock",{description:"Hold or free a Mac-wide lock tied to your lease. `perf-slot` is for fair speed tests: while you hold it the farm boots, clones and screenshots nothing else, and it records how busy the Mac is every second. It waits only for farm work in flight, not for other runs to end, so other sims and other apps may still use the Mac. run_benchmark_batch on your farm sim then marks each run fair or unfair from that record; run the unfair cases again. Measure only while you hold it. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...D,name:o.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:o.boolean().describe("true = take it, false = free it."),waitSeconds:o.number().int().min(0).max(1800).optional()}},async e=>{const r=I("sim_farm_lock");if(r)return r;try{const n={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return u(await l(e.hold?"lock":"unlock",n,(e.waitSeconds??0)*1e3+3e4))}catch(n){return y(n)}});t.registerTool("sim_farm_report",{description:"Tell the person how your run is going. They watch it live in Buoy Desktop. Send one when a sim starts work, passes, fails or needs help, with a short note. `counts` only when you know the full task list. Reports never add time. Pass an increasing `sequence` so a retry is never counted twice.",inputSchema:{runId:T,udid:o.string().optional().describe("The sim this is about. Leave out for the whole run."),state:o.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:o.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:o.object({done:o.number().int().min(0),total:o.number().int().min(0),passed:o.number().int().min(0).optional(),failed:o.number().int().min(0).optional()}).optional(),resultSource:o.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:o.number().int().min(0).optional()}},async e=>{try{return u(await l("report",e,3e4))}catch(r){return y(r)}});t.registerTool("sim_farm_extend",{description:"Ask for more time on your run before deadlineAt. The farm never goes past the run's cap (capAt). It says how much you got.",inputSchema:{runId:T,minutes:o.number().int().min(1).max(1440).describe("Minutes from now.")}},async e=>{try{return u(await l("extend",e))}catch(r){return y(r)}});t.registerTool("sim_farm_finish",{description:"End your run with a result. Gives back every sim it still holds. The person sees the summary in Buoy Desktop, so make it one or two plain sentences, e.g. 'Idea 3 won: 52 \u2192 58 fps.'",inputSchema:{runId:T,outcome:o.enum(["passed","failed","mixed","unknown"]).optional(),summary:o.string().max(600).optional()}},async e=>{try{const r=await l("finish",e,6e4,`fin-${e.runId}`);M(n=>n.runId===e.runId);return u(r)}catch(r){return y(r)}})}async function Q(){const{startFarmd:t}=await import("./src-R7ICMP6Q.mjs");const i=await t({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!i)process.exit(0);for(const e of["SIGTERM","SIGINT"])process.on(e,()=>void i.stop(e))}export{A as hasBuoyAccount,P as accountRequired,H as farmFairness,J as registerSimFarmTools,Q as runFarmd};
|
|
4
|
+
Next: ${t.next}`:""}`);return f(`Sim Farm error: ${t?.message??t}`)}var D={leaseId:o.string().describe("The lease id from sim_farm_acquire."),epoch:o.number().int().describe("The epoch from sim_farm_acquire. A stale epoch is refused.")};var S=o.string().optional().describe("Any unique string for this call. Repeating the same call with the same requestId returns the first result instead of acting twice.");var T=o.string().describe("The runId from sim_farm_acquire.");async function Y(t,i,e){const r=new Map;const n=Date.now()+e;for(;;){for(const s of i){if(r.has(s))continue;const a=t.connectedDevices().find(d=>d.deviceId.endsWith(s)&&d.tier!=="checking");if(a)r.set(s,a.deviceId)}if(r.size===i.length||Date.now()>=n)return r;await new Promise(s=>setTimeout(s,500))}}async function F(t){const i=await l("status");const e=t==="android"?i.android?.templates??[]:i.templates??[];return e.filter(r=>r.usable).sort((r,n)=>String(r.createdAt).localeCompare(String(n.createdAt))).pop()?.id}async function q(){(await _()).require("android")}var B=o.enum(["ios","android"]).optional().describe("ios (default) or android. Android copies are emulators started from a saved snapshot.");function J(t,i){R=()=>t.server.getClientVersion()?.name;x=i;t.registerTool("sim_farm_status",{description:"Show the Sim Farm: how many farm simulators exist, which are awake or in use and by whom, free memory and disk, how many more sims fit, templates, leases, and recent events. Farm sims are slim iOS simulators (about 1.5 GB each instead of 5) that the farm makes from a template. `android` lists Android templates (with `roomFor` and `costGB` per copy) and running Android copies: slim emulators that start from a saved snapshot in about 2 s with the app on them. Apps launch as a real phone (an iPhone SE, 2nd gen, by default): they see its RAM and cores and get its memory warnings. Each sim's `phone` shows the app's peak memory, and `overLimit: true` means a real phone would have closed the app. A safety cap closes an app that keeps growing. Start here before sim_farm_acquire. Set `outside` to also list this Mac's other simulators, which the farm never touches.",inputSchema:{outside:o.boolean().optional().describe("Also list simulators the farm does not own.")}},async({outside:e})=>{try{return u(await l("status",{outside:!!e}))}catch(r){return y(r)}});t.registerTool("sim_farm_setup",{description:"Create or resize the Sim Farm. `create` makes a template sim from an app build and, optionally, a seed folder (signed-in app data); clones are made from it in about 4 s each. Dev-client apps (Expo) need `launchUrl` so they open on Metro instead of the launcher. `grow` / `shrink` change how many sims exist. Use `dryRun` first: it returns the plan, what slim mode turns off (push, StoreKit, app links, widgets, Spotlight\u2026) and whether the Mac has room. You own the pool: Buoy Desktop has no size control. sim_farm_acquire makes sims as needed; `shrink` the ones you no longer need so they stop using disk space.",inputSchema:{mode:o.enum(["create","grow","shrink"]).describe("create = new template; grow/shrink = pool size (iOS only)."),platform:B,apkPath:o.string().optional().describe("Android create: path to the app's .apk. A debug build lets the farm read each copy's Buoy id."),phone:o.string().optional().describe("Android create: the phone copies act like (RAM, cores, screen, app memory limit). Default android-3gb. See `farm android phone list`."),reverse:o.array(o.string().regex(/^\d{1,5}(:\d{1,5})?$/)).optional().describe('Android create: ports each copy forwards to this Mac, e.g. ["8081:8802"] so a dev build loads JS from Metro on 8802.'),templateId:o.string().optional().describe("For grow/shrink. Defaults to the newest usable template."),count:o.number().int().min(1).max(64).optional().describe("For grow/shrink: how many sims to add or remove."),appPath:o.string().optional().describe("For create: path to a simulator .app build."),seedDir:o.string().nullable().optional().describe("For create: seed folder (Documents/, AsyncStorage/, Preferences/, seed.json). null = no seed."),launchUrl:o.string().optional().describe("For create: URL opened after each launch, e.g. exp+my-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081."),deviceType:o.string().optional().describe("For create: a simctl device type id. Default iPhone 17 Pro."),runtime:o.string().optional().describe("For create: a simctl runtime id. Default iOS 26.3."),profile:o.enum(["slim","stock"]).optional().describe("For create: slim (default) or stock."),metroPort:o.number().int().min(1).max(65535).optional().describe("For create: the Metro port the app loads from. Default 8802."),dryRun:o.boolean().optional().describe("Return the plan and its cost without changing anything."),requestId:S}},async e=>{const r=I("sim_farm_setup");if(r)return r;try{if(e.platform==="android"){await q();if(e.mode!=="create")return f("Android copies have no pool to grow or shrink: sim_farm_acquire starts them and release closes them.");if(!e.apkPath)return f("Android create needs apkPath.");const a={platform:"android",apk:e.apkPath,phoneId:e.phone,reverse:e.reverse,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(e.dryRun)return u({wouldCreateTemplate:a,note:"Needs the Android SDK first: npx @buoy-gg/mcp farm android setup (a person accepts Google's license there).",budget:await l("budget",{ramGB:4.3,diskGB:4})});return u(await l("setup",a,30*6e4,e.requestId))}if(e.mode==="create"){const a={appPath:e.appPath,seedDir:e.seedDir,deviceType:e.deviceType??"com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro",runtime:e.runtime??"com.apple.CoreSimulator.SimRuntime.iOS-26-3",profile:e.profile??"slim",metroPort:e.metroPort??8802,...e.launchUrl?{launchUrl:e.launchUrl}:{}};if(!a.appPath)return f("create needs appPath.");if(a.seedDir===void 0)return f("create needs seedDir, or seedDir: null for an app with no seed.");if(e.dryRun)return u({wouldCreateTemplate:a,budget:await l("budget",{ramGB:6,diskGB:4})});return u(await l("setup",a,15*6e4,e.requestId))}const n=e.templateId??await F("ios");if(!n)return f("No usable template yet. Run sim_farm_setup with mode create first.");const s=e.count??1;if(e.dryRun){const a=await l("budget",{ramGB:0,diskGB:e.mode==="grow"?2*s:0});return u({mode:e.mode,templateId:n,count:s,roomForMoreSims:a.roomFor,ok:a.ok,reason:a.reason})}return u(await l(e.mode,{templateId:n,count:s},30*6e4,e.requestId))}catch(n){return y(n)}});t.registerTool("sim_farm_acquire",{description:"Take farm simulators for your work. The person watches your work live in Buoy Desktop, so give a clear `goal` and a `labels` entry per sim. Returns a run (runId, deadlineAt), a lease (leaseId + epoch) and exact sims (UDID, name, and buoyDeviceId once the app ran: the 8-character tail that its list_devices id ends with; with launch, also deviceId, the full id to pass to other tools). Parked sims are booted and new ones cloned as needed, within the Mac's memory and disk limits. This MCP keeps the lease alive while it runs; release it with sim_farm_finish when done. Only ever target the returned UDIDs: never `booted`, which picks a random simulator when many run. While working, send sim_farm_report when a sim starts, passes or fails. The run stops at deadlineAt unless you ask for more time with sim_farm_extend. If any call returns RUN_STOPPED, the person stopped you: stop and tell them.",inputSchema:{count:o.number().int().min(1).max(64).describe("How many sims. There is no fixed cap: the farm gives as many as this Mac's memory and disk allow. Use allowPartial to take what fits."),goal:o.string().max(200).optional().describe("One short sentence the person sees on the run card, e.g. 'Make the menu list scroll faster'. With runId, the run's goal is used if you leave this out."),labels:o.array(o.string().max(80)).optional().describe("One label per sim, in order, e.g. ['Idea 1: memo rows', 'Idea 2: flat list']."),kind:o.enum(["try-ideas","run-tests","reproduce-bug","explore"]).optional().describe("What sort of work this is."),runId:o.string().optional().describe("Add these sims to a run you already have, instead of starting a new one."),purpose:o.string().optional().describe("Old name for goal. Use goal."),templateId:o.string().optional().describe("Defaults to the newest usable template of the platform."),platform:B,launch:o.boolean().optional().describe("Launch the app on each sim and wait (up to about 30 s) until it joins Buoy. Each sim then has deviceId for other tools."),allowPartial:o.boolean().optional().describe("Return fewer sims than asked instead of failing."),waitSeconds:o.number().int().min(0).max(1800).optional().describe("Wait this long for sims to free up."),ttlMinutes:o.number().int().min(1).max(1440).optional().describe("How long the run may go. Default 15. Agents are capped (1 hour by default); ask for more later with sim_farm_extend."),requestId:S}},async e=>{const r=I("sim_farm_acquire");if(r)return r;try{if(e.platform==="android")await q();const n=e.templateId??await F(e.platform??"ios");if(!n){return f(e.platform==="android"?"No usable Android template yet. Run sim_farm_setup with platform android, mode create and apkPath.":"No usable template yet. Run sim_farm_setup with mode create first.")}let s=(e.goal??e.purpose??"").trim();if(!s&&e.runId)s=((await l("status")).runs??[]).find(m=>m.id===e.runId)?.goal??"";if(!s)return f("Give a goal: one short sentence about what you are doing.");const a=await _();const d=await a.acquire({templateId:n,count:e.count,purpose:s,goal:s,labels:e.labels,kind:e.kind,runId:e.runId,launch:e.launch,allowPartial:e.allowPartial,waitMs:(e.waitSeconds??0)*1e3,ttlMs:(e.ttlMinutes??15)*6e4,requestId:e.requestId});for(const m of d.sims){const w=c.findIndex(g=>g.udid===m.udid);if(w>=0)c.splice(w,1);c.push({leaseId:d.leaseId,epoch:d.epoch,runId:d.runId,udid:m.udid,buoyDeviceId:m.buoyDeviceId??void 0})}let h=d;if(e.launch&&i){const m=d.sims.map(p=>p.buoyDeviceId).filter(p=>!!p);const w=m.length?await Y(i,m,3e4):new Map;h={...d,sims:d.sims.map(p=>({...p,deviceId:p.buoyDeviceId&&w.get(p.buoyDeviceId)||null}))};const g=d.sims.filter(p=>p.buoyDeviceId&&!w.has(p.buoyDeviceId));if(g.length){h.warning=`The app started on ${g.map(p=>p.name).join(", ")} but has not joined Buoy after 30 s. Run list_devices again in a moment, or take a screenshot with sim_farm_action to see the screen.`}}const b=e.launch?d.sims.filter(m=>!m.buoyDeviceId):[];if(b.length){const m=(await l("status").catch(()=>null))?.templates?.find(g=>g.id===n);const w=m?.metroPort?` on port ${m.metroPort}`:"";return u({...h,warning:`The app did not report a Buoy id on ${b.map(g=>g.name).join(", ")}. It may still be loading, or Metro${w} may be down or broken. Check Metro${w}, then take a screenshot with sim_farm_action to see the screen.`})}return u(h)}catch(n){return y(n)}});t.registerTool("sim_farm_release",{description:"Give one lease's sims back. `fast` (default) stops the app and restores its seeded data in about 2 s; `full` rebuilds each sim from the template. Use `full` after anything changed OS settings (permissions, keychain, location) or after a crash. Releasing your last lease ends the run, unless `keepRun` is true. To end a run with a result, use sim_farm_finish instead.",inputSchema:{...D,reset:o.enum(["fast","full"]).optional(),keepRun:o.boolean().optional().describe("Keep the run open to take more sims into it later.")}},async e=>{try{const r=await _();const n=await r.release(e.leaseId,e.epoch,e.reset??"fast",e.keepRun===true);M(s=>s.leaseId===e.leaseId);return u(n)}catch(r){return y(r)}});t.registerTool("sim_farm_action",{description:"Act on sims in your lease: `screenshot` (returns file paths), `open-url` (deep link, pass `url`), `launch` the app, `reset` (restore seeded app data), or `rebuild` (fresh clone; the UDID changes, the result maps old to new). On an Android copy, `reset` and `rebuild` both swap in a fresh copy: the id changes and the result maps old to new. `install` puts a new .apk (pass `apkPath`) on Android copies; it is gone when the copy closes.",inputSchema:{...D,udids:o.array(o.string()).min(1).describe("UDIDs from your lease."),action:o.enum(["screenshot","open-url","launch","reset","rebuild","install"]),url:o.string().optional().describe("For open-url."),apkPath:o.string().optional().describe("For install (Android copies)."),requestId:S}},async e=>{const r=I("sim_farm_action");if(r)return r;try{const n=await l("action",{leaseId:e.leaseId,epoch:e.epoch,udids:e.udids,action:e.action,arg:e.action==="install"?e.apkPath:e.url},20*6e4,e.requestId);if(e.action==="reset"||e.action==="rebuild"){for(const[s,a]of Object.entries(n)){const d=c.find(h=>h.udid===s);if(d&&typeof a==="string"&&a!=="reset"&&a!==s)Object.assign(d,{udid:a,buoyDeviceId:void 0})}}return u(n)}catch(n){return y(n)}});t.registerTool("sim_farm_lock",{description:"Hold or free a Mac-wide lock tied to your lease. `perf-slot` is for fair speed tests: while you hold it the farm boots, clones and screenshots nothing else, and it records how busy the Mac is every second. It waits only for farm work in flight, not for other runs to end, so other sims and other apps may still use the Mac. run_benchmark_batch on your farm sim then marks each run fair or unfair from that record; run the unfair cases again. Measure only while you hold it. Other names guard shared test fixtures (e.g. `ask-buoy-gateway`, `shop`). Locks end with the lease.",inputSchema:{...D,name:o.string().regex(/^[a-z0-9-]{1,40}$/).describe("Lock name, e.g. perf-slot."),hold:o.boolean().describe("true = take it, false = free it."),waitSeconds:o.number().int().min(0).max(1800).optional()}},async e=>{const r=I("sim_farm_lock");if(r)return r;try{const n={leaseId:e.leaseId,epoch:e.epoch,name:e.name,waitMs:(e.waitSeconds??0)*1e3};return u(await l(e.hold?"lock":"unlock",n,(e.waitSeconds??0)*1e3+3e4))}catch(n){return y(n)}});t.registerTool("sim_farm_report",{description:"Tell the person how your run is going. They watch it live in Buoy Desktop. Send one when a sim starts work, passes, fails or needs help, with a short note. `counts` only when you know the full task list. Reports never add time. Pass an increasing `sequence` so a retry is never counted twice.",inputSchema:{runId:T,udid:o.string().optional().describe("The sim this is about. Leave out for the whole run."),state:o.enum(["setting-up","working","waiting","passed","failed","needs-help"]).optional(),note:o.string().max(200).optional().describe("Short and plain, e.g. 'Idea 3: 58 fps, best so far'."),counts:o.object({done:o.number().int().min(0),total:o.number().int().min(0),passed:o.number().int().min(0).optional(),failed:o.number().int().min(0).optional()}).optional(),resultSource:o.enum(["agent","test"]).optional().describe("'test' when a test runner decided pass or fail, 'agent' when you judged it. Default agent."),sequence:o.number().int().min(0).optional()}},async e=>{try{return u(await l("report",e,3e4))}catch(r){return y(r)}});t.registerTool("sim_farm_extend",{description:"Ask for more time on your run before deadlineAt. The farm never goes past the run's cap (capAt). It says how much you got.",inputSchema:{runId:T,minutes:o.number().int().min(1).max(1440).describe("Minutes from now.")}},async e=>{try{return u(await l("extend",e))}catch(r){return y(r)}});t.registerTool("sim_farm_finish",{description:"End your run with a result. Gives back every sim it still holds. The person sees the summary in Buoy Desktop, so make it one or two plain sentences, e.g. 'Idea 3 won: 52 \u2192 58 fps.'",inputSchema:{runId:T,outcome:o.enum(["passed","failed","mixed","unknown"]).optional(),summary:o.string().max(600).optional()}},async e=>{try{const r=await l("finish",e,6e4,`fin-${e.runId}`);M(n=>n.runId===e.runId);return u(r)}catch(r){return y(r)}})}async function Q(){const{startFarmd:t}=await import("./src-HRUK7GEE.mjs");const i=await t({hostLabel:"buoy-mcp",idleExitMs:10*6e4,onExit:()=>process.exit(0)});if(!i)process.exit(0);for(const e of["SIGTERM","SIGINT"])process.on(e,()=>void i.stop(e))}export{A as hasBuoyAccount,P as accountRequired,H as farmFairness,J as registerSimFarmTools,Q as runFarmd};
|