@buoy-gg/mcp 7.0.77 → 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 CHANGED
@@ -93,3 +93,13 @@ See the [Chrome Farm guide](https://buoy.gg/buoy/latest/docs/tools/chrome-farm).
93
93
  ---
94
94
 
95
95
  <sub>Part of <a href="https://buoy.gg">Buoy</a>, developer tools that live inside your app. Proprietary software. © Buoy LLC. <a href="https://buoy.gg/terms">Terms</a></sub>
96
+
97
+ ### Native Profiler
98
+
99
+ Use `native_profile` to start, stop, or take a timed iOS Simulator or Android trace. Use `get_native_profile` to read it by ID. Start a trace, run a flow, then stop in a `finally` step. Needs a Buoy Pro app. iOS needs Xcode on macOS. Android needs adb on Mac or Linux. The report shows hot functions, hot threads, and busy spans. Each span names its source. RSS and frame counts stay unknown when absent. Under 30 frames means too few to judge. Samples cannot find sleeping hangs. [Read the guide](https://buoy.gg/buoy/latest/docs/tools/native-profiler).
100
+
101
+ ### Memory checks
102
+
103
+ `memory_check` starts or stops a route loop. `get_memory_leaks` reads its report. Checks need Pro and a native dev build with `@buoy-gg/memory-leaks`. Clock is optional for timer counts. Routes needs a mounted `RouteTracker` for loops.
104
+
105
+ Start returns a `runId`. Poll for results. The report shows suspects, GC proof and byte growth. A suspect is a clue. An empty list without GC proof does not prove clean. See the [Memory Leaks guide](https://buoy.gg/buoy/latest/docs/tools/memory-leaks).
@@ -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 };