@buoy-gg/mcp 7.0.78 → 7.0.80

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/dist/cli.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as S}from"module";const N=S(import.meta.url);import{SyncClient as v,createMcpServer as R,stopAllHidStreamsNow as B,stopAllRecordingsNow as k}from"./chunk-HEPFVYZG.mjs";import{runInit as O}from"./chunk-LHGIYW7N.mjs";import"./chunk-3STZVJQG.mjs";import"./chunk-XA74T4JN.mjs";import"./chunk-ODGXMS2O.mjs";import"./chunk-5NOZVPWB.mjs";import"./chunk-D5K6WO6B.mjs";import"./chunk-OP23W722.mjs";import"./chunk-DVRR7W22.mjs";import"./chunk-MTTWR4T7.mjs";import{StdioServerTransport as T}from"@modelcontextprotocol/sdk/server/stdio.js";var $=console.log;console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function P(){const e=process.env.BUOY_BROKER_PORT;const r=new v({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const n=R(r);const t=new T;let c=false;const s=o=>{if(c)return;c=true;process.stderr.write(`[buoy-mcp] shutting down (${o})
3
- `);k();B();setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await r.stop().catch(()=>{});await n.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>s("stdin closed"));process.stdin.once("close",()=>s("stdin closed"));n.server.onclose=()=>s("transport closed");process.on("SIGINT",()=>s("SIGINT"));process.on("SIGTERM",()=>s("SIGTERM"));process.on("SIGHUP",()=>s("SIGHUP"));await n.connect(t);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
4
- `);r.start().then(o=>process.stderr.write(`[buoy-mcp] broker ${o}
5
- `)).catch(o=>process.stderr.write(`[buoy-mcp] broker connection failed: ${o?.message??o} \u2014 tools will report no devices until it recovers
6
- `))}async function E(){const e=process.env.BUOY_BROKER_PORT;const r=new v({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:false});const{hasProAccess:n}=await import("./policy-UNODNYFR.mjs");try{const t=await Promise.race([r.start().then(()=>true,()=>false),new Promise(s=>setTimeout(()=>s(false),4e3))]);if(!t)return{account:false,pro:false};let c=r.isAdmitted();for(let s=0;s<10;s++){const o=r.connectedDevices();if(o.some(i=>n(i)))return{account:true,pro:true};c||=o.some(i=>i.tier==="free");await new Promise(i=>setTimeout(i,300))}return{account:c,pro:false}}finally{await Promise.race([r.stop().catch(()=>{}),new Promise(t=>setTimeout(t,2e3))])}}if(process.argv[2]==="web"){void import("./main-6OX5577P.mjs").then(e=>e.runWebServer()).catch(e=>{process.stderr.write(`[buoy-web] fatal: ${e?.message??e}
7
- `);process.exit(1)})}else if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const r=e.find(a=>!a.startsWith("--"));if(!r)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:n}=await import("fs");const{runWebBench:t,formatWebBenchMarkdown:c}=await import("./webBench-BEMJWKB3.mjs");const{readEntitlement:s,entitlementHasPro:o}=await import("./camera-6HTJDU4Y.mjs");const{freeWebBench:i,lockedLines:y,nothingLocked:h}=await import("./freeView-3LHV3RBG.mjs");const l=s();const m=o(l)?{account:true,pro:true}:await E();const d=o(l)||m.pro;if(!l.admitted&&!m.account){throw new Error("web-bench needs a Buoy account. A free one works. Sign in to Buoy Desktop, or run `npx --package=@buoy-gg/core buoy login` in the project.")}const p=JSON.parse(n(r,"utf8"));if(p.traceDir&&!d)throw new Error("traceDir (Chrome traces) needs Buoy Pro. See https://buoy.gg/pricing.");let w=h();const u=await t({...p,durationMs:p.durationMs??p.perCaseDurationMs,redact:d?void 0:a=>{const b=i(a);w=b.locked;return b.result}},a=>process.stderr.write(`[web-bench] ${a}
2
+ import{createRequire as S}from"module";const I=S(import.meta.url);import{createMcpServer as R,stopAllHidStreamsNow as B,stopAllRecordingsNow as k}from"./chunk-BKCCZNDR.mjs";import"./chunk-DVRR7W22.mjs";import{runInit as O}from"./chunk-2324LPBJ.mjs";import"./chunk-23AXQYJ4.mjs";import{SyncClient as b,stopNativeProfiles as P}from"./chunk-TMN3LXKB.mjs";import"./chunk-5XATNNKY.mjs";import"./chunk-FYJWWTAC.mjs";import"./chunk-5NOZVPWB.mjs";import"./chunk-D5K6WO6B.mjs";import"./chunk-OP23W722.mjs";import"./chunk-MTTWR4T7.mjs";import{StdioServerTransport as T}from"@modelcontextprotocol/sdk/server/stdio.js";var $=console.log;console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function E(){const e=process.env.BUOY_BROKER_PORT;const r=new b({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const n=R(r);const t=new T;let c=false;const o=s=>{if(c)return;c=true;process.stderr.write(`[buoy-mcp] shutting down (${s})
3
+ `);k();B();setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await P();await r.stop().catch(()=>{});await n.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>o("stdin closed"));process.stdin.once("close",()=>o("stdin closed"));n.server.onclose=()=>o("transport closed");process.on("SIGINT",()=>o("SIGINT"));process.on("SIGTERM",()=>o("SIGTERM"));process.on("SIGHUP",()=>o("SIGHUP"));await n.connect(t);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
4
+ `);r.start().then(s=>process.stderr.write(`[buoy-mcp] broker ${s}
5
+ `)).catch(s=>process.stderr.write(`[buoy-mcp] broker connection failed: ${s?.message??s} \u2014 tools will report no devices until it recovers
6
+ `))}async function N(){const e=process.env.BUOY_BROKER_PORT;const r=new b({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:false});const{hasProAccess:n}=await import("./policy-UNODNYFR.mjs");try{const t=await Promise.race([r.start().then(()=>true,()=>false),new Promise(o=>setTimeout(()=>o(false),4e3))]);if(!t)return{account:false,pro:false};let c=r.isAdmitted();for(let o=0;o<10;o++){const s=r.connectedDevices();if(s.some(i=>n(i)))return{account:true,pro:true};c||=s.some(i=>i.tier==="free");await new Promise(i=>setTimeout(i,300))}return{account:c,pro:false}}finally{await Promise.race([r.stop().catch(()=>{}),new Promise(t=>setTimeout(t,2e3))])}}if(process.argv[2]==="web"){void import("./main-QEH33AS3.mjs").then(e=>e.runWebServer()).catch(e=>{process.stderr.write(`[buoy-web] fatal: ${e?.message??e}
7
+ `);process.exit(1)})}else if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const r=e.find(a=>!a.startsWith("--"));if(!r)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:n}=await import("fs");const{runWebBench:t,formatWebBenchMarkdown:c}=await import("./webBench-BEMJWKB3.mjs");const{readEntitlement:o,entitlementHasPro:s}=await import("./camera-6HTJDU4Y.mjs");const{freeWebBench:i,lockedLines:y,nothingLocked:h}=await import("./freeView-3LHV3RBG.mjs");const l=o();const m=s(l)?{account:true,pro:true}:await N();const d=s(l)||m.pro;if(!l.admitted&&!m.account){throw new Error("web-bench needs a Buoy account. A free one works. Sign in to Buoy Desktop, or run `npx --package=@buoy-gg/core buoy login` in the project.")}const p=JSON.parse(n(r,"utf8"));if(p.traceDir&&!d)throw new Error("traceDir (Chrome traces) needs Buoy Pro. See https://buoy.gg/pricing.");let w=h();const u=await t({...p,durationMs:p.durationMs??p.perCaseDurationMs,redact:d?void 0:a=>{const v=i(a);w=v.locked;return v.result}},a=>process.stderr.write(`[web-bench] ${a}
8
8
  `));const f=y(w);if(f)process.stderr.write(`${f}
9
9
  `);const g=u.status==="failed"||u.browserLeftRunning?1:0;const x=(e.includes("--json")?JSON.stringify(u,null,2):c(u))+"\n";setTimeout(()=>process.exit(g),5e3).unref();process.stdout.write(x,()=>process.exit(g))}catch(e){process.stderr.write(`[buoy-mcp] web-bench failed: ${e?.message??e}
10
- `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="flow"&&process.argv[3]==="run"){void(async()=>{try{const{parseFlowRunArgs:e,runFlows:r,formatFlowResults:n}=await import("./flowRun-XEK65IL7.mjs");const t=e(process.argv.slice(4));const c=await r(t);const s=t.json?JSON.stringify(c,null,2):n(c);const o=c.every(i=>i.passed)?0:1;setTimeout(()=>process.exit(o),5e3).unref();process.stdout.write(`${s}
11
- `,()=>process.exit(o))}catch(e){process.stderr.write(`[buoy-mcp] flow run failed: ${e?.message??e}
12
- `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="farm"){console.log=$;void import("./src-R7ICMP6Q.mjs").then(e=>e.runFarmCommand(process.argv.slice(3),{prog:"npx @buoy-gg/mcp farm",entry:process.argv[1],chromeKeeperArgs:["farm","chrome","keeper"]})).catch(e=>{const r=e;process.stderr.write(`${r?.message??e}${r?.next?`
10
+ `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="flow"&&process.argv[3]==="run"){void(async()=>{try{const{parseFlowRunArgs:e,runFlows:r,formatFlowResults:n}=await import("./flowRun-DYM3QAEL.mjs");const t=e(process.argv.slice(4));const c=await r(t);const o=t.json?JSON.stringify(c,null,2):n(c);const s=c.every(i=>i.passed)?0:1;setTimeout(()=>process.exit(s),5e3).unref();process.stdout.write(`${o}
11
+ `,()=>process.exit(s))}catch(e){process.stderr.write(`[buoy-mcp] flow run failed: ${e?.message??e}
12
+ `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="farm"){console.log=$;void import("./src-HRUK7GEE.mjs").then(e=>e.runFarmCommand(process.argv.slice(3),{prog:"npx @buoy-gg/mcp farm",entry:process.argv[1],chromeKeeperArgs:["farm","chrome","keeper"]})).catch(e=>{const r=e;process.stderr.write(`${r?.message??e}${r?.next?`
13
13
  ${r.next}`:""}
14
- `);process.exit(1)})}else if(process.argv[2]==="farmd"){void import("./simFarm-R4P6QXRK.mjs").then(e=>e.runFarmd()).catch(e=>{process.stderr.write(`[buoy-mcp] farmd failed: ${e?.message??e}
15
- `);process.exit(1)})}else if(process.argv[2]==="init"){void(async()=>{try{const e=process.argv.slice(3);let r="auto";if(e.includes("--local"))r="local";else if(e.includes("--npx"))r="npx";const n=e.indexOf("--registry");const t=n!==-1?e[n+1]:void 0;if(e.includes("--web")){const{runWebInit:c}=await import("./init-VCQMVZXB.mjs");c({mode:r,registry:t})}else O({mode:r,registry:t});process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] init failed: ${e?.message??e}
16
- `);process.exit(1)}})()}else{P().catch(e=>{process.stderr.write(`[buoy-mcp] fatal: ${e?.message??e}
14
+ `);process.exit(1)})}else if(process.argv[2]==="farmd"){void import("./simFarm-AWVHVU6A.mjs").then(e=>e.runFarmd()).catch(e=>{process.stderr.write(`[buoy-mcp] farmd failed: ${e?.message??e}
15
+ `);process.exit(1)})}else if(process.argv[2]==="init"){void(async()=>{try{const e=process.argv.slice(3);let r="auto";if(e.includes("--local"))r="local";else if(e.includes("--npx"))r="npx";const n=e.indexOf("--registry");const t=n!==-1?e[n+1]:void 0;if(e.includes("--web")){const{runWebInit:c}=await import("./init-HT6H7SCW.mjs");c({mode:r,registry:t})}else O({mode:r,registry:t});process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] init failed: ${e?.message??e}
16
+ `);process.exit(1)}})()}else{E().catch(e=>{process.stderr.write(`[buoy-mcp] fatal: ${e?.message??e}
17
17
  `);process.exit(1)})}
@@ -0,0 +1,2 @@
1
+ import{createRequire as v}from"module";const E=v(import.meta.url);import{createMcpServer as y}from"./chunk-BKCCZNDR.mjs";import"./chunk-DVRR7W22.mjs";import"./chunk-23AXQYJ4.mjs";import{SyncClient as R}from"./chunk-TMN3LXKB.mjs";import"./chunk-5XATNNKY.mjs";import"./chunk-FYJWWTAC.mjs";import"./chunk-5NOZVPWB.mjs";import"./chunk-D5K6WO6B.mjs";import"./chunk-OP23W722.mjs";import"./chunk-MTTWR4T7.mjs";import{promises as S}from"fs";import $ from"path";import{Client as _}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as B}from"@modelcontextprotocol/sdk/inMemory.js";function I(e){const o={names:[]};for(let n=0;n<e.length;n++){const t=e[n];if(t==="--device")o.deviceId=e[++n];else if(t==="--update-snapshots")o.updateSnapshots=true;else if(t==="--json")o.json=true;else if(t==="--wait")o.waitMs=Number(e[++n])*1e3;else if(t==="--all")continue;else if(!t.startsWith("--"))o.names.push(t);else throw new Error(`Unknown option ${t}.`)}return o}async function P(){const e=$.join(process.cwd(),".buoy","flows");return(await S.readdir(e).catch(()=>[])).filter(o=>o.endsWith(".json")).map(o=>o.replace(/\.json$/,"")).sort()}async function g(e,o=n=>process.stderr.write(`${n}
2
+ `)){const n=e.names.length?e.names:await P();if(n.length===0)throw new Error('No saved flows. Save one with run_flow and save:"name", or pass a flow name.');const t=process.env.BUOY_BROKER_PORT;const s=new R({url:process.env.BUOY_BROKER_URL,port:t?Number(t):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const w=y(s);const[d,f]=B.createLinkedPair();const i=new _({name:"buoy-flow-run",version:"1"});try{await w.connect(f);await i.connect(d);await s.start();const a=Date.now()+(e.waitMs??6e4);while(Date.now()<a&&!s.connectedDevices().some(r=>(r.isPro||r.tier==="free")&&(!e.deviceId||r.deviceId===e.deviceId))){await new Promise(r=>setTimeout(r,500))}if(!s.connectedDevices().some(r=>r.isPro||r.tier==="free")){throw new Error("No app on a Buoy account is connected. Start the app (with Buoy) on a simulator or device, then run again.")}const m=[];for(const r of n){const p=Date.now();o(`\u25B6 ${r}`);const u=await i.callTool({name:"run_flow",arguments:{flow:r,include:"none",...e.deviceId?{deviceId:e.deviceId}:{},...e.updateSnapshots?{updateSnapshots:true}:{}}},void 0,{timeout:10*6e4});const h=(u.content??[]).filter(c=>c.type==="text").map(c=>c.text).join("\n");const l=u.isError!==true;m.push({name:r,passed:l,ms:Date.now()-p,report:h});o(`${l?"\u2713":"\u2717"} ${r} (${((Date.now()-p)/1e3).toFixed(1)}s)`)}return m}finally{await i.close().catch(()=>void 0);await Promise.race([s.stop().catch(()=>void 0),new Promise(a=>setTimeout(a,2e3))])}}function j(e){const o=e.filter(t=>!t.passed);const n=[`${e.length-o.length} of ${e.length} flows passed.`];for(const t of o){n.push("",`\u2717 ${t.name}`,t.report.trim(),` Rerun: npx @buoy-gg/mcp flow run ${t.name}`)}return n.join("\n")}export{j as formatFlowResults,I as parseFlowRunArgs,g as runFlows};
package/dist/index.d.mts CHANGED
@@ -1,373 +1,7 @@
1
- import { ToolCapability } from '@buoy-gg/sync-broker';
1
+ import { S as SyncClient } from './SyncClient-gqMRmB79.mjs';
2
+ export { B as BrokerMode, D as DeviceInfo, a as SyncClientOptions } from './SyncClient-gqMRmB79.mjs';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
-
4
- type AccessLevel = "free" | "pro";
5
-
6
- interface SyncClientOptions {
7
- /** Broker URL to connect to. @default http://localhost:42831 */
8
- url?: string;
9
- /** Broker port to probe/spawn on. @default 42831 */
10
- port?: number;
11
- /**
12
- * If no broker is already running on the port, spawn one in-process so the
13
- * MCP server works standalone (no desktop app needed). @default true
14
- */
15
- autoSpawnBroker?: boolean;
16
- /** Default per-action timeout. @default 15000 */
17
- actionTimeoutMs?: number;
18
- /** Optional logger — writes to stderr by default (stdout is the MCP channel). */
19
- log?: (msg: string) => void;
20
- }
21
- interface DeviceInfo {
22
- deviceId: string;
23
- deviceName: string;
24
- platform?: string;
25
- isConnected: boolean;
26
- /** Whether the connected app reported an active Buoy Pro license. */
27
- isPro: boolean;
28
- /**
29
- * The app's tier after admission. New sockets are "checking"; an account
30
- * lock is "locked". Older apps report only `isPro`, which maps to Pro
31
- * or locked because keys could only grant Pro before the tier split.
32
- *
33
- * Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
34
- * the free tier" and "no admitted account" are different problems.
35
- */
36
- tier: "checking" | "locked" | "free" | "pro";
37
- /**
38
- * More than one app is registered under this id right now (a pinned
39
- * `externalSync.deviceId` copied onto two devices). Actions sent to it
40
- * land on an arbitrary one. Broker-computed; false for apps too old to
41
- * announce a runtime instance id.
42
- */
43
- collision: boolean;
44
- tools: ToolCapability[];
45
- }
46
- /** How the client obtained its broker connection. */
47
- type BrokerMode = "connected" | "spawned";
48
- /**
49
- * A headless "Dashboard" client of the Buoy sync broker — the same role the
50
- * desktop app plays, but driven programmatically by the MCP server. Implements
51
- * connect-or-spawn: connects to a running broker (e.g. the desktop app) if one
52
- * is up, otherwise hosts its own in-process so it works standalone.
53
- */
54
- declare class SyncClient {
55
- private socket;
56
- private admitted;
57
- /** When this client was made: the startup window for {@link awaitBroker}. */
58
- private readonly createdAt;
59
- private lockedDevices;
60
- private hostAccount;
61
- private broker;
62
- private readonly url;
63
- private readonly port;
64
- private readonly actionTimeoutMs;
65
- private readonly log;
66
- /** Latest capabilities announce per deviceId. */
67
- private capabilities;
68
- /** Latest device list from the broker's `all-devices-update`. */
69
- private devices;
70
- /**
71
- * The device this session last named explicitly. Agents name a device once,
72
- * then leave deviceId out on later calls; with several apps connected every
73
- * one of those calls failed ("Multiple devices connected"), which cost a
74
- * turn at the start of almost every benchmark run. An explicit id sets it,
75
- * and so does the project-folder match in resolveDevice. It never switches on its own: if that device goes away, calls
76
- * without an id fail instead of picking another app.
77
- */
78
- private sessionDevice;
79
- /** Latest snapshot payload per `${deviceId}::${toolId}`. */
80
- private snapshots;
81
- /**
82
- * Capture-mode watches this client holds, keyed by snapKey. Stored with
83
- * their addressing because they have to be RE-SENT on every reconnect: the
84
- * broker keys watch state by socket id, and a reconnect gets a new one.
85
- */
86
- private watching;
87
- /** In-flight actions awaiting a result, keyed by requestId. */
88
- private pendingActions;
89
- /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
90
- private snapshotWaiters;
91
- /** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
92
- private snapshotAt;
93
- /** Liveness per device — see {@link noteTimeout}. */
94
- private health;
95
- /** Dev-server origin per device — see {@link noteDevServerOrigin}. */
96
- private devServerOrigins;
97
- /** Newest crash-report console entry id seen per device — see {@link noteCrashReport}. */
98
- private crashSeen;
99
- /** Lookups still working out a device's dev-server folder (origin probe, then lsof). */
100
- private rootLookups;
101
- /** Folder each device's dev server runs from — see {@link devServerRoot}. */
102
- private devServerRoots;
103
- /** Devices whose origin probe already ran (a production bundle has none). */
104
- private originProbed;
105
- /** Last time each device appeared in the broker's device list. */
106
- private lastSeen;
107
- constructor(opts?: SyncClientOptions);
108
- private readonly autoSpawn;
109
- /** Connect to a running broker, or spawn one in-process, then handshake. */
110
- start(): Promise<BrokerMode>;
111
- private connectSocket;
112
- /**
113
- * Drop everything remembered about devices that have been gone a while.
114
- *
115
- * Deliberately NOT on disconnect: a crashed app often drops off the broker,
116
- * and its cached console snapshot is the crash evidence — throwing that away
117
- * the instant it disconnects would delete the answer. But device ids change
118
- * with every simulator erase and fresh dev client, and each one can hold
119
- * multi-MB snapshots, so a long-lived server has to forget eventually.
120
- */
121
- /** Whether the broker admitted this host's Buoy account. */
122
- isAdmitted(): boolean;
123
- private requireAdmission;
124
- private transportLost;
125
- private setAdmission;
126
- private requireDeviceAccount;
127
- private lockDevice;
128
- private pruneStaleDevices;
129
- /** Disconnect and tear down a spawned broker, if any. */
130
- stop(): Promise<void>;
131
- /** All devices the broker knows about, merged with their advertised tools. */
132
- listDevices(): DeviceInfo[];
133
- /** Connected devices only. */
134
- connectedDevices(): DeviceInfo[];
135
- /** Actions advertised for a tool, with the devices offering it. */
136
- toolActions(toolId: string): {
137
- deviceId: string;
138
- deviceName: string;
139
- actions: string[];
140
- }[];
141
- /**
142
- * This project's own connected app, if the broker has admitted it. Picks
143
- * nothing for the session; for warming up host tools before the first call.
144
- */
145
- peekProjectDevice(): string | null;
146
- /**
147
- * Resolve a concrete target deviceId. If `deviceId` is given it's used as-is.
148
- * Otherwise, if exactly one device is connected, that one is chosen; if none
149
- * or many, an informative error is thrown (callers can pass "All" explicitly).
150
- */
151
- resolveDevice(deviceId?: string): string;
152
- /**
153
- * Resolve a target device AND require the tier the current tool call needs.
154
- * Most tools need Buoy Pro; the free ones (access/policy.ts) also take an app
155
- * on the free tier. The level comes from the call's scope, set centrally in
156
- * server.ts before any handler runs, so a handler can't pick its own. With no
157
- * scope (work outside any tool call) the level is Pro. The app's own license
158
- * check is the source of truth (the device reports it over the broker); this
159
- * just reads it.
160
- *
161
- * `list_devices` deliberately does NOT call this — a user without access can
162
- * still see their device and its tier, which explains what is blocked.
163
- */
164
- requireProDevice(deviceId?: string): string;
165
- /**
166
- * The check under every app call (callAction, getSnapshot, the snapshot
167
- * cache, lastSync), on top of requireProDevice. A call to a Pro app always
168
- * passes. A call to any other app passes only inside a free tool call and
169
- * only for the app calls listed in access/policy.ts, so a Pro tool that
170
- * forgot requireProDevice still can't act on a free app, and a free tool
171
- * can't reach Pro data by any path. Work outside any tool call (reconnects,
172
- * health pings) is plumbing and passes.
173
- */
174
- private checkAppCall;
175
- /**
176
- * A free app's tap must land on the app, not on Buoy's own panels: their
177
- * controls do what Pro tools do. The app side leaves Buoy out of fuzzy
178
- * matches but lets an exact testID or nativeTag through, so on a free app an
179
- * exact tap goes through only when the app-only screen list shows that
180
- * target. Anything this can't prove (a Buoy control, an off-screen target, a
181
- * failed read, a broadcast to "All") is refused. Resolves to replacement
182
- * params when it narrows the tap (never edits the caller's object, which a
183
- * flow reuses on retries). Returns null, without an await, for every call it
184
- * doesn't apply to.
185
- */
186
- private refuseBuoyPanelTap;
187
- /**
188
- * Whether a result for this device may carry Pro's why-data (render names,
189
- * script names, JS origins). True for a Pro key or the Weekend Pass. A free
190
- * tool works on a free app too; this decides how much of its result it
191
- * gets (access/freeView.ts).
192
- */
193
- fullResultsFor(deviceId: string): boolean;
194
- /**
195
- * Whether this device was seen on a tier good enough for the current call,
196
- * connected or not. For host controls that keep working after the app
197
- * quits or crashes (see hostKit), and for the last-sync fallback.
198
- */
199
- deviceWasAllowed(deviceId: string): boolean;
200
- /**
201
- * Where a device stands for `level` (the current call's by default):
202
- * connected and allowed ("ready"), not connected ("offline"), connected but
203
- * its account not announced yet ("pending"), or announced on a tier without
204
- * this access ("denied").
205
- */
206
- accessState(deviceId: string, level?: AccessLevel): "ready" | "offline" | "pending" | "denied";
207
- private healthOf;
208
- /** The device answered — it's alive. */
209
- private noteOk;
210
- /**
211
- * The device didn't answer. A crashed app never answers again, so after the
212
- * first miss we stop paying the full timeout on every subsequent call.
213
- */
214
- private noteTimeout;
215
- /** True once a device has missed TWO calls and hasn't answered since. */
216
- isUnresponsive(deviceId: string): boolean;
217
- /**
218
- * Full timeout normally; a short one for a device already not answering.
219
- *
220
- * A single missed call does not trigger the short budget — one slow
221
- * `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
222
- * the full budget (they are the recovery path).
223
- *
224
- * Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
225
- * budget again so the short timeout cannot become self-sustaining.
226
- */
227
- private timeoutFor;
228
- /**
229
- * Plain-language diagnosis for a device that isn't answering, including the
230
- * app's last words if the console tool got a crash flush out in time.
231
- *
232
- * A silent timeout is the worst possible answer here: the app is on a redbox,
233
- * the reason is already captured, and without this the caller reads "timed
234
- * out" and keeps retrying the same dead device.
235
- */
236
- unresponsiveNote(deviceId: string): string;
237
- /**
238
- * A fresh crash report from the console tool (its crash flush runs as the
239
- * app's React tree dies) means actions waiting on that device will likely
240
- * never be answered: navigate to a screen that throws used to sit out its
241
- * full 15 s. Cut their wait to the short leash instead of failing them at
242
- * once, and only after a ping goes unanswered: an ErrorBoundary can catch a
243
- * render error and the app lives.
244
- */
245
- private noteCrashReport;
246
- /**
247
- * The newest error line the console tool synced for this device, if any.
248
- * Reads the cached console snapshot rather than the device (which, by the
249
- * time this matters, is not answering).
250
- */
251
- private lastFatalFor;
252
- /** Age of the cached snapshot for a tool, in ms (null = never received). */
253
- snapshotAgeMs(toolId: string, deviceId: string): number | null;
254
- /**
255
- * Remember where a device's bundle came from, reported by core's `app`
256
- * adapter while the app is healthy. It is the only way to reload an app that
257
- * has crashed: the in-app reload needs the React tree that the crash
258
- * destroyed, but the dev server's reload is handled natively.
259
- */
260
- noteDevServerOrigin(deviceId: string, origin: unknown): void;
261
- /**
262
- * Ask a freshly-announced device where its bundle came from, once.
263
- *
264
- * Deliberately eager: this is the piece of information that can ONLY be
265
- * collected while the app is healthy, and is only ever needed after it has
266
- * stopped answering. One cheap round-trip per device buys the ability to
267
- * recover a crashed app instead of telling the caller to go do it by hand.
268
- */
269
- private learnDevServerOrigin;
270
- private trackRootLookup;
271
- /**
272
- * Wait (briefly) for project-folder lookups still running, so an agent's
273
- * first call right after apps connect can still be matched to this
274
- * project's app instead of failing with "Multiple devices connected". Only
275
- * worth it when no device is chosen yet and several apps are connected.
276
- */
277
- /**
278
- * Right after the MCP starts, the broker may not have admitted this client
279
- * yet: a first call then failed with "A verified Buoy account ... required"
280
- * although the account is fine. Every call gives admission (and the first
281
- * device list) a moment; once admitted this returns at once.
282
- */
283
- awaitBroker(): Promise<void>;
284
- settleDeviceChoice(maxMs?: number): Promise<void>;
285
- /** The dev-server origin last reported by this device, if any. */
286
- devServerOrigin(deviceId: string): string | null;
287
- /**
288
- * Keep the device RECORDING this tool, without asking it to stream.
289
- *
290
- * A dashboard watches for as long as its panel is open; this client is
291
- * request/response, so it used to watch and unwatch around every single
292
- * read. That works for a tool whose state just IS (the query cache, the env
293
- * table) and is useless for one that records a timeline: the unwatch drops
294
- * the device's subscriber count to zero, which stops capture, so nothing is
295
- * ever recorded between two reads. The list comes back empty and reads as
296
- * "the app made no requests" — a wrong conclusion that is very expensive to
297
- * chase, because everything about the app looks fine.
298
- *
299
- * Capture-mode watches are held for the life of the connection and released
300
- * on disconnect (the broker also drops them for us). They cost the device's
301
- * ring buffer, not the socket: no snapshot is pushed, and reads still go
302
- * through `request-devtool-state`.
303
- */
304
- private ensureWatching;
305
- private sendWatch;
306
- /**
307
- * Re-arm every held watch after a reconnect.
308
- *
309
- * The broker tracks watches per SOCKET, so a reconnect (broker restart, a
310
- * dropped wifi link) silently discards all of ours while `watching` still
311
- * says we hold them — `ensureWatching` would then short-circuit forever and
312
- * the device would never record again. Silent and permanent: the exact
313
- * failure this whole capture-mode change exists to remove.
314
- */
315
- private rearmWatches;
316
- /** Each device's current connection (socket id + nonce), to spot a reload. */
317
- private deviceConnections;
318
- /**
319
- * Re-arm the watches for a device that came back on a NEW connection.
320
- *
321
- * A relaunch or JS reload gives the app a fresh socket and fresh adapters,
322
- * whose capture starts switched off. Our watches still say "held", so
323
- * `ensureWatching` short-circuited and the device never recorded again: after
324
- * one restart, the storage timeline stayed empty for the rest of the session
325
- * (found by the BK storage suite — every case after a relaunch read nothing).
326
- */
327
- private rearmReconnectedDevices;
328
- /** Stop streaming a tool (releases the device-side backpressure). */
329
- unwatch(toolId: string, targetDeviceId: string): void;
330
- /** Last snapshot received for a tool/device, if any. */
331
- cachedSnapshot(toolId: string, deviceId: string): unknown;
332
- /**
333
- * Get a tool's current state: arm capture (once, for the connection), ask
334
- * for state explicitly, and resolve with the snapshot that comes back.
335
- * Falls back to the last cached snapshot if the request times out.
336
- *
337
- * The capture watch is deliberately NOT released afterwards — see
338
- * `ensureWatching`. It costs the device nothing on the socket, and dropping
339
- * it is what made every timeline tool look permanently empty from here.
340
- */
341
- getSnapshot(toolId: string, deviceId?: string, opts?: {
342
- timeoutMs?: number;
343
- silent?: boolean;
344
- }): Promise<{
345
- deviceId: string;
346
- payload: unknown;
347
- stale?: string;
348
- }>;
349
- /**
350
- * The last snapshot a device sent for a tool, kept after the device drops
351
- * off. A fatal render error tears down the app's React tree, and Buoy's
352
- * connection with it; @buoy-gg/console flushes its logs out first, so this
353
- * is where the app's last words are once it can no longer be asked.
354
- */
355
- lastSync(deviceId: string, toolId: string): {
356
- payload: unknown;
357
- ageMs: number | null;
358
- } | undefined;
359
- /** Header line for data served from cache because the device went quiet. */
360
- private staleNote;
361
- private waitForSnapshot;
362
- /**
363
- * Invoke a remote action on a device and await its result. Pass deviceId to
364
- * target one device, or "All" to broadcast (resolves with the first responder).
365
- */
366
- callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
367
- timeoutMs?: number;
368
- silent?: boolean;
369
- }): Promise<unknown>;
370
- }
4
+ import '@buoy-gg/sync-broker';
371
5
 
372
6
  declare function createMcpServer(sync: SyncClient): McpServer;
373
7
 
@@ -998,4 +632,9 @@ declare const DASHBOARD_NAME = "Dashboard";
998
632
  /** Target token meaning "every connected device". */
999
633
  declare const ALL_DEVICES = "All";
1000
634
 
1001
- export { ALL_DEVICES, BROKER_PORT, BROKER_URL, type BrokerMode, DASHBOARD_NAME, DEFAULT_ACTION_TIMEOUT_MS, DEFAULT_SNAPSHOT_TIMEOUT_MS, type DeviceInfo, type InitOptions, PACKAGE_NAME, SyncClient, type SyncClientOptions, VERSION, buildAutomationConfig, compareReportStats, createMcpServer, estimateBatchMs, formatRankingMarkdown, getUpdateNotice, rankBatchFromIndex, registerConsoleTools, registerEventsTools, registerGenericTools, registerJotaiTools, registerPerfMonitorTools, registerReactQueryTools, registerReduxTools, registerRoutesTools, registerScreenshotTools, registerStorageTools, registerZustandTools, runInit };
635
+ /** Runs on the chosen device. A suspect is a clue, not proof of a leak. */
636
+ declare function registerMemoryLeaksTools(server: McpServer, sync: SyncClient): void;
637
+
638
+ declare function registerOtaTools(server: McpServer, sync: SyncClient): void;
639
+
640
+ export { ALL_DEVICES, BROKER_PORT, BROKER_URL, DASHBOARD_NAME, DEFAULT_ACTION_TIMEOUT_MS, DEFAULT_SNAPSHOT_TIMEOUT_MS, type InitOptions, PACKAGE_NAME, SyncClient, VERSION, buildAutomationConfig, compareReportStats, createMcpServer, estimateBatchMs, formatRankingMarkdown, getUpdateNotice, rankBatchFromIndex, registerConsoleTools, registerEventsTools, registerGenericTools, registerJotaiTools, registerMemoryLeaksTools, registerOtaTools, registerPerfMonitorTools, registerReactQueryTools, registerReduxTools, registerRoutesTools, registerScreenshotTools, registerStorageTools, registerZustandTools, runInit };