@buoy-gg/mcp 7.0.42 → 7.0.43

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.
@@ -1,5 +1,5 @@
1
1
  import{DEFAULT_BROKER_PORT as gr}from"@buoy-gg/sync-broker";var Ze=gr;var Dt=`http://localhost:${Ze}`;var Mt=15e3;var vr=2500;var wr=1200;var Rt=2e4;var br=3e4;var Ot=5e3;var yr=15*6e4;var Xe="Dashboard";var J="All";import{readFileSync as Ct,writeFileSync as $r,mkdirSync as kr,renameSync as _r,unlinkSync as Sr}from"fs";import{dirname as xr,join as Ft,resolve as Ar}from"path";import{homedir as Tr}from"os";import{createHash as Ir}from"crypto";import{validateLicense as Dr,isAcceptedValidation as Mr,isTransientLicenseError as Rr,resolveTier as Or,isRecognizedAccount as Cr,isBuoyToken as Fr,verifyFreeToken as Er,resolveAccountAccess as Br,signCache as Et,verifyCacheSignature as qr,LICENSE_LAUNCH_VALIDATION_TTL as Bt,LICENSE_REVALIDATION_INTERVAL as De,LICENSE_TRANSIENT_RETRY_DELAYS as jr,KEYGEN_CONFIG as Pr}from"@buoy-gg/license/headless";var qt=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];function jt(e,n=process.env){for(const t of qt)if(n[t]!==void 0)return n[t].trim().toUpperCase()||null;for(const t of[".env.development.local",".env.local",".env"]){let r;try{r=Ct(Ft(e,t),"utf8")}catch{continue}const s=new Map;for(const o of r.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const i=a[2].trim();const c=i.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);s.set(a[1],c?c[2]:i.split("#")[0].trim())}for(const o of qt)if(s.has(o))return s.get(o).trim().toUpperCase()||null}return null}var Nr=class{constructor(e,n={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>Br({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted";this.projectDir=Ar(n.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=Ir("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=n.cacheFile??Ft(Tr(),".buoy",`account-${t}.json`)}start(){if(this.timer)return;try{const e=JSON.parse(Ct(this.cacheFile,"utf8"));if(e.valid&&qr(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;if(this.timer)clearInterval(this.timer);this.timer=null;this.key=null;this.cache=null;this.notify()}notify(){const e=this.hasAccess();if(e===this.lastAdmission)return;this.lastAdmission=e;this.changed()}async tick(){const e=jt(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasAccess()&&Date.now()-this.cache.lastValidatedAt<Bt&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+Bt;return}this.busy=true;const n=this.generation;try{if(Fr(e)){const i=Er(e);if(!i){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=Et({licenseKey:e,licenseId:`token:${i.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+De;return}const t=await Dr(e);if(n!==this.generation||jt(this.projectDir)!==e)return;const r=t.data?.relationships?.product?.data?.id;const s=t.meta.code==="EXPIRED"&&(Cr(t.data)||this.cache?.licenseKey===e);if(r&&r!==Pr.productId||!s&&!Mr(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+De;return}const o=s?"free":Or(t.data);const a=Date.now();this.cache=Et({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:s?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+De}catch(t){if(n!==this.generation)return;const r=Rr(t)?jr[this.retry++]:void 0;this.nextCheck=Date.now()+(r??De)}finally{this.busy=false;this.notify()}}persistCache(){try{kr(xr(this.cacheFile),{recursive:true,mode:448});$r(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});_r(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{Sr(this.cacheFile)}catch{}}};import Lr from"http";import{randomUUID as Ur}from"crypto";import{io as zr}from"socket.io-client";import{createBroker as Jr}from"@buoy-gg/sync-broker";function Wr(e,n){return e==="app"&&(n==="ping"||n==="reloadApp")}function Pt(e){return e>=2}function Vr(e){if(Wr(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!Pt(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function Kr(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var Hr=class{constructor(e={}){this.socket=null;this.admitted=false;this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.port=e.port??Ze;this.url=e.url??(e.port?`http://localhost:${e.port}`:Dt);this.actionTimeoutMs=e.actionTimeoutMs??Mt;this.log=e.log??(n=>process.stderr.write(`[buoy-mcp] ${n}
2
- `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await Nt(this.url);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new Nr(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=Jr({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await Gr(this.url);n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return new Promise((e,n)=>{const t=zr(this.url,{transports:["websocket"],query:{deviceName:Xe},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const s=()=>{if(r)return;r=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(r)return;r=true;clearTimeout(o);n(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=ee(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const s of r)s(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===J){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-yr;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(r)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(r))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==Xe&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=this.connectedDevices();const t=n.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(n.length>0){throw new Error(`Devices are connected (${n.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const r=t.filter(o=>o.tools.length>0);if(r.length===1)return r[0].deviceId;if(r.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=r.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const n=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${n(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===J){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return J}const r=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===r);if(!s?.isPro)throw new Error(t(s));return r}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return Pt(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=Vr({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:vr,recoveryProbeMs:Rt,toolId:n,action:t});if(o)r.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=Kr({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:br});const s=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const n=this.snapshots.get(ee(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const s=t[r];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(ee(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){this.devServerOrigins.set(e,n)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(ee(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=ee(n,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:n});this.sendWatch(e,n)}sendWatch(e,n){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(n))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:n}of this.watching.values()){this.sendWatch(e,n)}}unwatch(e,n){const t=ee(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(ee(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const s=ee(r,e);this.ensureWatching(e,r);let o=t.timeoutMs??Ot;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const d=this.healthOf(r);if(Date.now()-d.lastFullBudgetAt>Rt){d.lastFullBudgetAt=Date.now()}else{o=wr}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const i=await a;this.requireAdmission();this.requireDeviceAccount(r);if(i!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:i}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},n);const o=a=>{clearTimeout(s);t(a)};r.push(o);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,s={}){this.requireAdmission();const o=r===J?J:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const a=Ur();const i=s.timeoutMs??this.timeoutFor(o,e,n);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==J&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${n}" got no answer in ${i}ms. `+(o===J?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==J&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};var ee=(e,n)=>`${e}::${n}`;function Nt(e,n=800){return new Promise(t=>{const r=Lr.get(e,s=>{s.resume();t((s.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function Gr(e,n=20){for(let t=0;t<n;t++){if(await Nt(e,300))return;await Yr(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var Yr=e=>new Promise(n=>setTimeout(n,e));var ae="7.0.42"?"7.0.42":"0.0.0-dev";var te="@buoy-gg/mcp";import{promises as Lt}from"fs";import Qr from"os";import Zr from"path";var Xr=24*60*60*1e3;var es=1500;var ts=`https://registry.npmjs.org/${te}/latest`;var Ut=Zr.join(Qr.tmpdir(),"buoy-mcp-update-check.json");function ns(e,n){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const r=t(e);const s=t(n);for(let o=0;o<3;o++){const a=r[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function rs(){try{const e=await Lt.readFile(Ut,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function ss(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),es);try{const r=await fetch(ts,{signal:n.signal});if(!r.ok)return null;const s=await r.json();const o=typeof s.version==="string"?s.version:null;if(o){await Lt.writeFile(Ut,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function os(e){const n=await rs();if(n&&e-n.checkedAt<Xr)return n.latest;return ss(e)}async function zt(e=Date.now()){if(ae==="0.0.0-dev")return null;try{const n=await os(e);if(n&&ns(n,ae)){return`A newer buoy-mcp is available (${n}; you're on ${ae}). Update by re-running: npx -y ${te}@latest init \u2014 then restart your editor.`}}catch{}return null}import{z as W}from"zod";function $e(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function et(e){return{isError:true,content:[{type:"text",text:e}]}}function Jt(e,n){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await zt();const r=n.listDevices();if(r.length===0){return $e({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=r.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return $e({devices:r,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:W.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const r=n.toolActions(t);if(r.length===0){return et(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return $e({toolId:t,offerings:r})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:W.string().describe("The tool id to read state from."),deviceId:W.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:W.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:r,timeoutMs:s})=>{try{const o=n.requireProDevice(r);const{payload:a}=await n.getSnapshot(t,o,{timeoutMs:s});return $e({toolId:t,deviceId:o,snapshot:a})}catch(o){return et(Wt(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:W.string().describe('The tool id, e.g. "perf-monitor".'),action:W.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:W.record(W.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:W.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:W.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:r,params:s,deviceId:o,timeoutMs:a})=>{try{const i=n.requireProDevice(o);const c=await n.callAction(t,r,s,i,{timeoutMs:a});return $e({toolId:t,action:r,ok:true,result:c??null})}catch(i){return et(Wt(i))}})}function Wt(e){return e instanceof Error?e.message:String(e)}var ge=(e,n,t)=>Math.max(n,Math.min(t,e));var as="__batch_warmup__";var is={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var ve={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function we(e,n,t){return e??n??t}function Me(e,n={}){const t=[];const r=(e.targetRoute||"").trim();if(!r)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=r==="/"?"/_buoy_bounce":"/";if(s===r){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((l,m)=>{const $=(l.name||"").trim()||`case ${m+1}`;return{id:`case-${m}`,name:$,params:l.params??{},route:l.route}});const a=ge(Math.round(we(e.runsPerCase,n.runsPerCase,ve.runsPerCase)),1,10);const i=ge(Math.round(we(e.coolDownMs,n.coolDownMs,ve.coolDownMs)),0,3e4);const c=ge(Math.round(we(e.discardWarmupRuns,n.discardWarmupRuns,ve.discardWarmupRuns)),0,a-1);const d=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&d===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(a===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:r,bounceRoute:s,perCaseDurationMs:ge(Math.round(we(e.perCaseDurationMs,n.perCaseDurationMs,ve.perCaseDurationMs)),1e3,6e4),settleMs:ge(Math.round(we(e.settleMs,n.settleMs,ve.settleMs)),0,1e4),navTimeoutMs:ge(Math.round(we(e.navTimeoutMs,n.navTimeoutMs,ve.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:d,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:u,warnings:t}}function Vt(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const r=tt(e);const s=r/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(r/1e3)}s`;const a=[`**${n(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const i=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${n(i)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function tt(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let r=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)r+=e.cases.length*3e3;return r}function cs(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){n.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){n.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)n.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)n.push("JS thread stalls");return n}function Re(e,n,t,r=[]){const s=e.filter(h=>h.batchId===n);const o=new Map(r.map(h=>[h.index,h.reason]));const a=Array.from(new Set(s.map(h=>h.batchIndex??0))).sort((h,f)=>h-f);const i=h=>{const f=s.filter(g=>(g.batchIndex??0)===h);if(f.length===0)return void 0;return f.find(g=>g.isMedianRun===true)??f.find(g=>g.runIndex===0||g.runIndex===void 0)??f[0]};const c=h=>typeof h==="number"?h:null;const d=a.map(h=>{const f=i(h);const g=s.filter(O=>(O.batchIndex??0)===h).length;const v=f?.name??t?.[h]?.name??`case ${h+1}`;const S=o.get(h)??f?.batchFailureReason;const x=Boolean(S)||!f;const M={batchIndex:h,caseName:v,params:f?.params??{},runs:g,failed:x,failureReason:x?S??"no run recorded":void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};return{...M,flags:x?[]:cs(M)}}).filter(h=>h.caseName!==as).map((h,f)=>({...h,batchIndex:f}));const u=d[0];if(u){for(const h of d){if(h===u)continue;if(h.uiFps!=null&&u.uiFps!=null)h.deltaUiFps=ie(h.uiFps-u.uiFps,1);if(h.jsFps!=null&&u.jsFps!=null)h.deltaJsFps=ie(h.jsFps-u.jsFps,1)}}const l=d.filter(h=>!h.failed);const m=(h,f)=>{let g;let v=NaN;for(const S of l){const x=h(S);if(x==null)continue;if(g===void 0||f(x,v)){g=S;v=x}}return g?.caseName};const $=h=>h.jsJank==null&&h.uiJank==null?null:(h.jsJank??0)+(h.uiJank??0);const b={uiFps:m(h=>h.uiFps,(h,f)=>h>f),jsFps:m(h=>h.jsFps,(h,f)=>h>f),lowestCpu:m(h=>h.cpu,(h,f)=>h<f),lowestMem:m(h=>h.memMb,(h,f)=>h<f),fewestJank:m($,(h,f)=>h<f)};const p=l.filter(h=>h.flags.length>0).map(h=>({caseName:h.caseName,flags:h.flags}));const w=l.filter(h=>h.flags.length===0).sort((h,f)=>(f.uiFps??0)-(h.uiFps??0)||(h.cpu??Infinity)-(f.cpu??Infinity)||(h.memMb??Infinity)-(f.memMb??Infinity)).map(h=>h.caseName);return{batchId:n,baselineName:u?.caseName??null,deviceMaxRefreshRate:l.find(h=>h.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:d,leaders:b,flagged:p,balanced:w}}function Oe(e){const n=(l,m=1)=>l==null?"\u2014":l.toFixed(m);const t=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.toFixed(1)})`;const r=[];r.push(`### Benchmark batch \`${e.batchId}\``);r.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);r.push("");const s=e.rows.some(l=>l.thermalState);const o=s?" Thermal |":"";const a=s?"-------|":"";r.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);r.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const l of e.rows){const m=s?` ${is[l.thermalState??"unknown"]??"\u2014"} |`:"";if(l.failed){r.push(`| ${l.batchIndex} | ${l.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${m} \u274C FAILED (${l.failureReason}) |`);continue}const $=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"\u2713 clean";r.push(`| ${l.batchIndex} | ${l.caseName} | ${n(l.uiFps)}${t(l.deltaUiFps)} | ${n(l.jsFps)}${t(l.deltaJsFps)} | ${n(l.cpu)} | ${n(l.memMb)} | ${n(l.jsJank,0)} | ${n(l.uiJank,0)} |${m} ${$} |`)}const i=e.rows.filter(l=>l.thermalState==="serious"||l.thermalState==="critical");if(i.length){r.push("");r.push(`**\u{1F525} Thermally throttled: ${i.map(l=>l.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}r.push("");const c=e.leaders;const d=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)r.push(`**Per-metric best:** ${d}`);r.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){r.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(l=>!l.failed&&(l.renderCommits!=null||l.topRenderers.length>0)).map(l=>{const m=l.topRenderers.map(p=>`${p.name} \xD7${p.renders} (${p.totalMs}ms${p.cause?`, ${p.cause}`:""})`).join(", ");const $=l.renderCommits!=null?`${l.renderCommits} commits`:"";const b=l.renderWasted!=null?`${l.renderWasted} wasted`:"";return`- **${l.caseName}** \u2014 ${[$,b,m].filter(Boolean).join(": ")}`});if(u.length){r.push("");r.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");r.push(...u)}r.push("");r.push(`_Deltas vs baseline (case #0). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return r.join("\n")}function Kt(e,n){const t=(r,s)=>({a:{avg:ie(r.avg,1),p95:ie(r.p95,1)},b:{avg:ie(s.avg,1),p95:ie(s.p95,1)},deltaAvg:ie(s.avg-r.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...ls(e.renders,n.renders)}}function ls(e,n){if(!e&&!n)return null;const t=r=>r??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function ie(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}import{z as D}from"zod";import{z as us}from"zod";var k=e=>({content:[{type:"text",text:e}]});var y=e=>({isError:true,content:[{type:"text",text:e}]});var I=e=>e instanceof Error?e.message:String(e);var A=us.string().optional().describe("Target device id (auto-selected if one is connected).");function ds(e,n){return typeof e==="number"&&e>0?e:n}function hs(e){return e?`${e}
2
+ `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await Nt(this.url);let n="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new Nr(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=Jr({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await Gr(this.url);n="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return n}connectSocket(){return new Promise((e,n)=>{const t=zr(this.url,{transports:["websocket"],query:{deviceName:Xe},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let r=false;const s=()=>{if(r)return;r=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(r)return;r=true;n(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(r)return;r=true;clearTimeout(o);n(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=ee(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,n=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!n)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const r of t)r(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const n=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(n)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,r]of this.snapshotWaiters)if(t.startsWith(n)){this.snapshotWaiters.delete(t);for(const s of r)s(void 0)}for(const[t,r]of this.pendingActions)if(r.target===e||r.target===J){clearTimeout(r.timer);this.pendingActions.delete(t);r.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-yr;for(const[n,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===n))continue;this.lastSeen.delete(n);this.capabilities.delete(n);this.health.delete(n);this.devServerOrigins.delete(n);this.originProbed.delete(n);const r=`${n}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(r)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(r))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const n of e)n(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==Xe&&e.deviceId).map(e=>{const n=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:n?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(n=>{const t=n.tools.find(r=>r.toolId===e);return t?{deviceId:n.deviceId,deviceName:n.deviceName,actions:t.actions}:null}).filter(n=>n!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const n=this.connectedDevices();const t=n.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(n.length>0){throw new Error(`Devices are connected (${n.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const r=t.filter(o=>o.tools.length>0);if(r.length===1)return r[0].deviceId;if(r.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=r.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const n=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${n(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===J){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return J}const r=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===r);if(!s?.isPro)throw new Error(t(s));return r}healthOf(e){let n=this.health.get(e);if(!n){n={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,n)}return n}noteOk(e){const n=this.healthOf(e);n.consecutiveTimeouts=0;n.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return Pt(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,n,t){const r=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=Vr({consecutiveTimeouts:r.consecutiveTimeouts,lastFullBudgetAt:r.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:vr,recoveryProbeMs:Rt,toolId:n,action:t});if(o)r.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const n=this.healthOf(e);const t=n.lastOkAt?Math.round((Date.now()-n.lastOkAt)/1e3):null;const r=Kr({consecutiveTimeouts:n.consecutiveTimeouts,lastOkAt:n.lastOkAt,now:Date.now(),unresponsiveAfterMs:br});const s=[`The app on "${e}" is not responding (${n.consecutiveTimeouts} call${n.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,r?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const n=this.snapshots.get(ee(e,"console"));const t=n?.entries;if(!Array.isArray(t))return null;for(let r=t.length-1;r>=0;r--){const s=t[r];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,n){this.requireAdmission();const t=this.snapshotAt.get(ee(n,e));return t?Date.now()-t:null}noteDevServerOrigin(e,n){if(typeof n==="string"&&/^https?:\/\//.test(n)){this.devServerOrigins.set(e,n)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const n=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!n)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const n=this.devServerOrigins.get(e);if(n)return n;const t=this.snapshots.get(ee(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,n){const t=ee(n,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:n});this.sendWatch(e,n)}sendWatch(e,n){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(n))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:n}of this.watching.values()){this.sendWatch(e,n)}}unwatch(e,n){const t=ee(n,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:n,watching:false})}cachedSnapshot(e,n){this.requireAdmission();this.requireDeviceAccount(n);return this.snapshots.get(ee(n,e))}async getSnapshot(e,n,t={}){const r=this.resolveDevice(n);const s=ee(r,e);this.ensureWatching(e,r);let o=t.timeoutMs??Ot;if(t.timeoutMs===void 0&&this.isUnresponsive(r)){const d=this.healthOf(r);if(Date.now()-d.lastFullBudgetAt>Rt){d.lastFullBudgetAt=Date.now()}else{o=wr}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:r});const i=await a;this.requireAdmission();this.requireDeviceAccount(r);if(i!==void 0){if(!t.silent)this.noteOk(r);return{deviceId:r,payload:i}}if(!t.silent)this.noteTimeout(r);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:r,payload:c,stale:this.staleNote(e,r)}}throw new Error(`Timed out waiting for "${e}" state from ${r} (is the tool active?). ${this.unresponsiveNote(r)}`)}staleNote(e,n){const t=this.snapshotAgeMs(e,n);const r=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${r}) \u2014 ${this.unresponsiveNote(n)}`}waitForSnapshot(e,n){return new Promise(t=>{const r=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},n);const o=a=>{clearTimeout(s);t(a)};r.push(o);this.snapshotWaiters.set(e,r)})}async callAction(e,n,t,r,s={}){this.requireAdmission();const o=r===J?J:this.resolveDevice(r);if(!this.socket)throw new Error("SyncClient is not connected");const a=Ur();const i=s.timeoutMs??this.timeoutFor(o,e,n);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==J&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${n}" got no answer in ${i}ms. `+(o===J?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:n,params:t??{}})});if(o!==J&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${n}" failed on device`)}return c.result}};var ee=(e,n)=>`${e}::${n}`;function Nt(e,n=800){return new Promise(t=>{const r=Lr.get(e,s=>{s.resume();t((s.statusCode??0)>0)});r.setTimeout(n,()=>{r.destroy();t(false)});r.on("error",()=>t(false))})}async function Gr(e,n=20){for(let t=0;t<n;t++){if(await Nt(e,300))return;await Yr(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var Yr=e=>new Promise(n=>setTimeout(n,e));var ae="7.0.43"?"7.0.43":"0.0.0-dev";var te="@buoy-gg/mcp";import{promises as Lt}from"fs";import Qr from"os";import Zr from"path";var Xr=24*60*60*1e3;var es=1500;var ts=`https://registry.npmjs.org/${te}/latest`;var Ut=Zr.join(Qr.tmpdir(),"buoy-mcp-update-check.json");function ns(e,n){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const r=t(e);const s=t(n);for(let o=0;o<3;o++){const a=r[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function rs(){try{const e=await Lt.readFile(Ut,"utf8");const n=JSON.parse(e);if(typeof n.checkedAt==="number"&&typeof n.latest==="string"){return n}}catch{}return null}async function ss(e){const n=new AbortController;const t=setTimeout(()=>n.abort(),es);try{const r=await fetch(ts,{signal:n.signal});if(!r.ok)return null;const s=await r.json();const o=typeof s.version==="string"?s.version:null;if(o){await Lt.writeFile(Ut,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function os(e){const n=await rs();if(n&&e-n.checkedAt<Xr)return n.latest;return ss(e)}async function zt(e=Date.now()){if(ae==="0.0.0-dev")return null;try{const n=await os(e);if(n&&ns(n,ae)){return`A newer buoy-mcp is available (${n}; you're on ${ae}). Update by re-running: npx -y ${te}@latest init \u2014 then restart your editor.`}}catch{}return null}import{z as W}from"zod";function $e(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function et(e){return{isError:true,content:[{type:"text",text:e}]}}function Jt(e,n){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await zt();const r=n.listDevices();if(r.length===0){return $e({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=r.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return $e({devices:r,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:W.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const r=n.toolActions(t);if(r.length===0){return et(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return $e({toolId:t,offerings:r})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:W.string().describe("The tool id to read state from."),deviceId:W.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:W.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:r,timeoutMs:s})=>{try{const o=n.requireProDevice(r);const{payload:a}=await n.getSnapshot(t,o,{timeoutMs:s});return $e({toolId:t,deviceId:o,snapshot:a})}catch(o){return et(Wt(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:W.string().describe('The tool id, e.g. "perf-monitor".'),action:W.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:W.record(W.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:W.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:W.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:r,params:s,deviceId:o,timeoutMs:a})=>{try{const i=n.requireProDevice(o);const c=await n.callAction(t,r,s,i,{timeoutMs:a});return $e({toolId:t,action:r,ok:true,result:c??null})}catch(i){return et(Wt(i))}})}function Wt(e){return e instanceof Error?e.message:String(e)}var ge=(e,n,t)=>Math.max(n,Math.min(t,e));var as="__batch_warmup__";var is={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var ve={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function we(e,n,t){return e??n??t}function Me(e,n={}){const t=[];const r=(e.targetRoute||"").trim();if(!r)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=r==="/"?"/_buoy_bounce":"/";if(s===r){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((l,m)=>{const $=(l.name||"").trim()||`case ${m+1}`;return{id:`case-${m}`,name:$,params:l.params??{},route:l.route}});const a=ge(Math.round(we(e.runsPerCase,n.runsPerCase,ve.runsPerCase)),1,10);const i=ge(Math.round(we(e.coolDownMs,n.coolDownMs,ve.coolDownMs)),0,3e4);const c=ge(Math.round(we(e.discardWarmupRuns,n.discardWarmupRuns,ve.discardWarmupRuns)),0,a-1);const d=e.shuffleCases??n.shuffleCases??o.length>3;if(o.length>3&&d===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(a===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:r,bounceRoute:s,perCaseDurationMs:ge(Math.round(we(e.perCaseDurationMs,n.perCaseDurationMs,ve.perCaseDurationMs)),1e3,6e4),settleMs:ge(Math.round(we(e.settleMs,n.settleMs,ve.settleMs)),0,1e4),navTimeoutMs:ge(Math.round(we(e.navTimeoutMs,n.navTimeoutMs,ve.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??n.discardWarmupCase??true,shuffleCases:d,reloadBetweenCases:e.reloadBetweenCases??n.reloadBetweenCases??false,captureRenders:e.captureRenders??n.captureRenders??true};return{config:u,warnings:t}}function Vt(e){const n=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const r=tt(e);const s=r/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(r/1e3)}s`;const a=[`**${n(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const i=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${n(i)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function tt(e){const n=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+n;let r=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)r+=e.cases.length*3e3;return r}function cs(e){const n=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){n.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){n.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)n.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)n.push("JS thread stalls");return n}function Re(e,n,t,r=[]){const s=e.filter(h=>h.batchId===n);const o=new Map(r.map(h=>[h.index,h.reason]));const a=Array.from(new Set(s.map(h=>h.batchIndex??0))).sort((h,f)=>h-f);const i=h=>{const f=s.filter(g=>(g.batchIndex??0)===h);if(f.length===0)return void 0;return f.find(g=>g.isMedianRun===true)??f.find(g=>g.runIndex===0||g.runIndex===void 0)??f[0]};const c=h=>typeof h==="number"?h:null;const d=a.map(h=>{const f=i(h);const g=s.filter(O=>(O.batchIndex??0)===h).length;const v=f?.name??t?.[h]?.name??`case ${h+1}`;const S=o.get(h)??f?.batchFailureReason;const x=Boolean(S)||!f;const M={batchIndex:h,caseName:v,params:f?.params??{},runs:g,failed:x,failureReason:x?S??"no run recorded":void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};return{...M,flags:x?[]:cs(M)}}).filter(h=>h.caseName!==as).map((h,f)=>({...h,batchIndex:f}));const u=d[0];if(u){for(const h of d){if(h===u)continue;if(h.uiFps!=null&&u.uiFps!=null)h.deltaUiFps=ie(h.uiFps-u.uiFps,1);if(h.jsFps!=null&&u.jsFps!=null)h.deltaJsFps=ie(h.jsFps-u.jsFps,1)}}const l=d.filter(h=>!h.failed);const m=(h,f)=>{let g;let v=NaN;for(const S of l){const x=h(S);if(x==null)continue;if(g===void 0||f(x,v)){g=S;v=x}}return g?.caseName};const $=h=>h.jsJank==null&&h.uiJank==null?null:(h.jsJank??0)+(h.uiJank??0);const b={uiFps:m(h=>h.uiFps,(h,f)=>h>f),jsFps:m(h=>h.jsFps,(h,f)=>h>f),lowestCpu:m(h=>h.cpu,(h,f)=>h<f),lowestMem:m(h=>h.memMb,(h,f)=>h<f),fewestJank:m($,(h,f)=>h<f)};const p=l.filter(h=>h.flags.length>0).map(h=>({caseName:h.caseName,flags:h.flags}));const w=l.filter(h=>h.flags.length===0).sort((h,f)=>(f.uiFps??0)-(h.uiFps??0)||(h.cpu??Infinity)-(f.cpu??Infinity)||(h.memMb??Infinity)-(f.memMb??Infinity)).map(h=>h.caseName);return{batchId:n,baselineName:u?.caseName??null,deviceMaxRefreshRate:l.find(h=>h.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:d,leaders:b,flagged:p,balanced:w}}function Oe(e){const n=(l,m=1)=>l==null?"\u2014":l.toFixed(m);const t=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.toFixed(1)})`;const r=[];r.push(`### Benchmark batch \`${e.batchId}\``);r.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);r.push("");const s=e.rows.some(l=>l.thermalState);const o=s?" Thermal |":"";const a=s?"-------|":"";r.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);r.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const l of e.rows){const m=s?` ${is[l.thermalState??"unknown"]??"\u2014"} |`:"";if(l.failed){r.push(`| ${l.batchIndex} | ${l.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${m} \u274C FAILED (${l.failureReason}) |`);continue}const $=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"\u2713 clean";r.push(`| ${l.batchIndex} | ${l.caseName} | ${n(l.uiFps)}${t(l.deltaUiFps)} | ${n(l.jsFps)}${t(l.deltaJsFps)} | ${n(l.cpu)} | ${n(l.memMb)} | ${n(l.jsJank,0)} | ${n(l.uiJank,0)} |${m} ${$} |`)}const i=e.rows.filter(l=>l.thermalState==="serious"||l.thermalState==="critical");if(i.length){r.push("");r.push(`**\u{1F525} Thermally throttled: ${i.map(l=>l.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}r.push("");const c=e.leaders;const d=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)r.push(`**Per-metric best:** ${d}`);r.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){r.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(l=>!l.failed&&(l.renderCommits!=null||l.topRenderers.length>0)).map(l=>{const m=l.topRenderers.map(p=>`${p.name} \xD7${p.renders} (${p.totalMs}ms${p.cause?`, ${p.cause}`:""})`).join(", ");const $=l.renderCommits!=null?`${l.renderCommits} commits`:"";const b=l.renderWasted!=null?`${l.renderWasted} wasted`:"";return`- **${l.caseName}** \u2014 ${[$,b,m].filter(Boolean).join(": ")}`});if(u.length){r.push("");r.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");r.push(...u)}r.push("");r.push(`_Deltas vs baseline (case #0). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return r.join("\n")}function Kt(e,n){const t=(r,s)=>({a:{avg:ie(r.avg,1),p95:ie(r.p95,1)},b:{avg:ie(s.avg,1),p95:ie(s.p95,1)},deltaAvg:ie(s.avg-r.avg,1)});return{a:e.name,b:n.name,uiFps:t(e.stats.uiFps,n.stats.uiFps),jsFps:t(e.stats.jsFps,n.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,n.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,n.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:n.stats.jsJankFrames,delta:n.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:n.stats.uiJankFrames,delta:n.stats.uiJankFrames-e.stats.uiJankFrames},...ls(e.renders,n.renders)}}function ls(e,n){if(!e&&!n)return null;const t=r=>r??0;return{renders:{total:{a:t(e?.totalRenders),b:t(n?.totalRenders),delta:t(n?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(n?.totalCommits),delta:t(n?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(n?.wastedRenders),delta:t(n?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:n?.causes??{}}}}}function ie(e,n){const t=Math.pow(10,n);return Math.round(e*t)/t}import{z as D}from"zod";import{z as us}from"zod";var k=e=>({content:[{type:"text",text:e}]});var y=e=>({isError:true,content:[{type:"text",text:e}]});var I=e=>e instanceof Error?e.message:String(e);var A=us.string().optional().describe("Target device id (auto-selected if one is connected).");function ds(e,n){return typeof e==="number"&&e>0?e:n}function hs(e){return e?`${e}
3
3
 
4
4
  `:""}function Ht(e){return`No ${e.toolId} result from the device. The ${e.toolId} tool may be inactive, or the app needs a newer Buoy (${e.mcpTool} requires the ${e.action} action).`}function ke(e,n){return async t=>{try{const r=e.requireProDevice(t.deviceId);return await n(t,r)}catch(r){return y(I(r))}}}var j="perf-monitor";var _e=1500;var Se=e=>new Promise(n=>setTimeout(n,e));var Gt=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var ps=D.object({name:D.string().describe("Display name for this variant (becomes the saved run's name)."),params:D.record(D.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:D.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function nt(e,n){try{const t=await e.callAction(j,"getAutomationConfig",{},n);if(!t)return{};const{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}=t;return{perCaseDurationMs:r,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}}catch{return{}}}async function G(e,n){const{payload:t}=await e.getSnapshot(j,n,{timeoutMs:4e3});return t}function rt(e){const n=new Set;for(const t of e??[])if(t.batchId)n.add(t.batchId);return n}function Yt(e,n){return(e??[]).filter(t=>t.batchId===n).length}function ms(e){let n=null;for(const t of e??[]){if(t.batchId&&(!n||t.createdAt>n.at))n={id:t.batchId,at:t.createdAt}}return n?.id??null}function ce(e,n){const t=new Map;for(const r of e??[]){if(r.batchId===n&&r.batchFailureReason&&!t.has(r.batchIndex??0)){t.set(r.batchIndex??0,r.batchFailureReason)}}return[...t.entries()].map(([r,s])=>({index:r,reason:s}))}function fs(e){const n=e.cases.length+(e.discardWarmupCase===true?1:0);return n*(e.runsPerCase??1)}var gs=1e4;function vs(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function Qt(e,n,t,r=gs){const s=Date.now();while(Date.now()-s<r){const o=await G(e,n).catch(()=>null);if(o&&!vs(o.live?.automation?.phase))return true;await Se(_e)}t(`automation still busy after ${Math.round(r/1e3)}s`);return false}async function ws(e,n,t){const{preBatchIds:r,expectedRuns:s,maxWaitMs:o,log:a}=t;const i=Date.now();let c="";let d=false;let u=0;while(Date.now()-i<o){let $;try{$=await G(e,n)}catch{await Se(_e);continue}const b=$.index??[];const p=$.live?.automation?.phase??"idle";const w=$.live?.automationCompleted;if(w?.phase==="cancelled"&&!r.has(w.batchId)){return{batchId:w.batchId,failures:[],cancelled:true}}if(w?.phase==="done"&&!r.has(w.batchId)){return{batchId:w.batchId,failures:w.failures??ce(b,w.batchId)}}if(p!=="idle")d=true;if(p!==c){c=p;a(`automation phase: ${p}`)}const h=[...rt(b)].find(f=>!r.has(f));if(h){const f=Yt(b,h);if(s>0&&f>=s){await Qt(e,n,a);a(`batch ${h} complete (${f}/${s} runs saved)`);return{batchId:h,failures:ce(b,h)}}if(p==="idle"&&d&&f>0){if(++u>=3){a(`batch ${h} settled (${f} runs, automation idle)`);return{batchId:h,failures:ce(b,h)}}}else{u=0}}await Se(_e)}const l=await G(e,n).catch(()=>null);const m=l?[...rt(l.index)].find($=>!r.has($)):void 0;if(m&&l)return{batchId:m,failures:ce(l.index,m),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}function Zt(e,n){e.registerTool("run_benchmark_batch",{description:"Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",inputSchema:{targetRoute:D.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:D.array(ps).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:D.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:D.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:D.number().optional().describe("How long to record each run. Default 5000."),settleMs:D.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:D.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:D.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:D.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:D.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:D.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:D.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:D.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),maxWaitMs:D.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const r=s=>process.stderr.write(`[buoy-mcp] ${s}
5
5
  `);try{const s=n.requireProDevice(t.deviceId);const o=await nt(n,s);const{config:a,warnings:i}=Me(t,o);const c=tt(a);const d=t.maxWaitMs??c*3+6e4;const u=fs(a);if(!await Qt(n,s,r)){return y("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await n.callAction(j,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(j,"setEnabled",{enabled:true},s).catch(()=>{});await n.callAction(j,"refreshIndex",{},s).catch(()=>{});const l=await G(n,s);const m=rt(l.index);r(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${s}`);await n.callAction(j,"startAutomation",{config:a},s);const $=await ws(n,s,{preBatchIds:m,expectedRuns:u,maxWaitMs:d,log:r});if($.cancelled){await n.callAction(j,"acknowledgeAutomation",{},s).catch(()=>{});return y(`Batch ${$.batchId} was cancelled before completing.`)}await n.callAction(j,"refreshIndex",{},s).catch(()=>{});const b=await G(n,s);const p=Re(b.index??[],$.batchId,a.cases,$.failures);await n.callAction(j,"acknowledgeAutomation",{},s).catch(()=>{});await n.callAction(j,"setEnabled",{enabled:false},s).catch(()=>{});const w=Oe(p);const h={batchId:$.batchId,leaders:p.leaders,balanced:p.balanced,flagged:p.flagged,rows:p.rows,failures:$.failures,warnings:i,estimateMs:c,...$.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return k(`${w}
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __copyProps=(e,r,t,n)=>{if(r&&typeof r==="object"||typeof r==="function"){for(let s of __getOwnPropNames(r))if(!__hasOwnProp.call(e,s)&&s!==t)__defProp(e,s,{get:()=>r[s],enumerable:!(n=__getOwnPropDesc(r,s))||n.enumerable})}return e};var __toESM=(e,r,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(r||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));var import_stdio=require("@modelcontextprotocol/sdk/server/stdio.js");var import_node_fs=require("fs");var import_node_path=require("path");var import_node_os=require("os");var import_node_crypto=require("crypto");var import_headless=require("@buoy-gg/license/headless");var KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];function readProjectKey(e,r=process.env){for(const t of KEY_NAMES)if(r[t]!==void 0)return r[t].trim().toUpperCase()||null;for(const t of[".env.development.local",".env.local",".env"]){let n;try{n=(0,import_node_fs.readFileSync)((0,import_node_path.join)(e,t),"utf8")}catch{continue}const s=new Map;for(const o of n.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const i=a[2].trim();const c=i.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);s.set(a[1],c?c[2]:i.split("#")[0].trim())}for(const o of KEY_NAMES)if(s.has(o))return s.get(o).trim().toUpperCase()||null}return null}var NodeAccount=class{constructor(e,r={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>(0,import_headless.resolveAccountAccess)({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted";this.projectDir=(0,import_node_path.resolve)(r.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=r.cacheFile??(0,import_node_path.join)((0,import_node_os.homedir)(),".buoy",`account-${t}.json`)}start(){if(this.timer)return;try{const e=JSON.parse((0,import_node_fs.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;if(this.timer)clearInterval(this.timer);this.timer=null;this.key=null;this.cache=null;this.notify()}notify(){const e=this.hasAccess();if(e===this.lastAdmission)return;this.lastAdmission=e;this.changed()}async tick(){const e=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const r=this.generation;try{if((0,import_headless.isBuoyToken)(e)){const i=(0,import_headless.verifyFreeToken)(e);if(!i){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${i.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(r!==this.generation||readProjectKey(this.projectDir)!==e)return;const n=t.data?.relationships?.product?.data?.id;const s=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(n&&n!==import_headless.KEYGEN_CONFIG.productId||!s&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const o=s?"free":(0,import_headless.resolveTier)(t.data);const a=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:s?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(r!==this.generation)return;const n=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(n??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs.mkdirSync)((0,import_node_path.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs.unlinkSync)(this.cacheFile)}catch{}}};var import_node_http=__toESM(require("http"));var import_node_crypto2=require("crypto");var import_socket=require("socket.io-client");var import_sync_broker2=require("@buoy-gg/sync-broker");var import_sync_broker=require("@buoy-gg/sync-broker");var BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;var BROKER_URL=`http://localhost:${BROKER_PORT}`;var DEFAULT_ACTION_TIMEOUT_MS=15e3;var UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;var UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;var RECOVERY_PROBE_MS=2e4;var UNRESPONSIVE_AFTER_MS=3e4;var DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;var DEVICE_RETENTION_MS=15*6e4;var DASHBOARD_NAME="Dashboard";var ALL_DEVICES="All";function isRecoveryAction(e,r){return e==="app"&&(r==="ping"||r==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(r=>process.stderr.write(`[buoy-mcp] ${r}
3
- `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let r="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);r="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return r}connectSocket(){return new Promise((e,r)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let n=false;const s=()=>{if(n)return;n=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(n)return;n=true;r(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(n)return;n=true;clearTimeout(o);r(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,r=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!r)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const n of t)n(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const r=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(r)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,n]of this.snapshotWaiters)if(t.startsWith(r)){this.snapshotWaiters.delete(t);for(const s of n)s(void 0)}for(const[t,n]of this.pendingActions)if(n.target===e||n.target===ALL_DEVICES){clearTimeout(n.timer);this.pendingActions.delete(t);n.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[r,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===r))continue;this.lastSeen.delete(r);this.capabilities.delete(r);this.health.delete(r);this.devServerOrigins.delete(r);this.originProbed.delete(r);const n=`${r}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(n)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(n))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const r of e)r(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const r=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:r?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(r=>{const t=r.tools.find(n=>n.toolId===e);return t?{deviceId:r.deviceId,deviceName:r.deviceName,actions:t.actions}:null}).filter(r=>r!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const r=this.connectedDevices();const t=r.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(r.length>0){throw new Error(`Devices are connected (${r.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const n=t.filter(o=>o.tools.length>0);if(n.length===1)return n[0].deviceId;if(n.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=n.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const r=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${r(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const n=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===n);if(!s?.isPro)throw new Error(t(s));return n}healthOf(e){let r=this.health.get(e);if(!r){r={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,r)}return r}noteOk(e){const r=this.healthOf(e);r.consecutiveTimeouts=0;r.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,r,t){const n=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:n.consecutiveTimeouts,lastFullBudgetAt:n.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:r,action:t});if(o)n.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const r=this.healthOf(e);const t=r.lastOkAt?Math.round((Date.now()-r.lastOkAt)/1e3):null;const n=diagnoseCrashed({consecutiveTimeouts:r.consecutiveTimeouts,lastOkAt:r.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${r.consecutiveTimeouts} call${r.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,n?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const r=this.snapshots.get(snapKey(e,"console"));const t=r?.entries;if(!Array.isArray(t))return null;for(let n=t.length-1;n>=0;n--){const s=t[n];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,r){this.requireAdmission();const t=this.snapshotAt.get(snapKey(r,e));return t?Date.now()-t:null}noteDevServerOrigin(e,r){if(typeof r==="string"&&/^https?:\/\//.test(r)){this.devServerOrigins.set(e,r)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const r=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!r)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const r=this.devServerOrigins.get(e);if(r)return r;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,r){const t=snapKey(r,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:r});this.sendWatch(e,r)}sendWatch(e,r){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(r))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:r}of this.watching.values()){this.sendWatch(e,r)}}unwatch(e,r){const t=snapKey(r,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:false})}cachedSnapshot(e,r){this.requireAdmission();this.requireDeviceAccount(r);return this.snapshots.get(snapKey(r,e))}async getSnapshot(e,r,t={}){const n=this.resolveDevice(r);const s=snapKey(n,e);this.ensureWatching(e,n);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(n)){const d=this.healthOf(n);if(Date.now()-d.lastFullBudgetAt>RECOVERY_PROBE_MS){d.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:n});const i=await a;this.requireAdmission();this.requireDeviceAccount(n);if(i!==void 0){if(!t.silent)this.noteOk(n);return{deviceId:n,payload:i}}if(!t.silent)this.noteTimeout(n);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:n,payload:c,stale:this.staleNote(e,n)}}throw new Error(`Timed out waiting for "${e}" state from ${n} (is the tool active?). ${this.unresponsiveNote(n)}`)}staleNote(e,r){const t=this.snapshotAgeMs(e,r);const n=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${n}) \u2014 ${this.unresponsiveNote(r)}`}waitForSnapshot(e,r){return new Promise(t=>{const n=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},r);const o=a=>{clearTimeout(s);t(a)};n.push(o);this.snapshotWaiters.set(e,n)})}async callAction(e,r,t,n,s={}){this.requireAdmission();const o=n===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(n);if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto2.randomUUID)();const i=s.timeoutMs??this.timeoutFor(o,e,r);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${r}" got no answer in ${i}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:r,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${r}" failed on device`)}return c.result}};var snapKey=(e,r)=>`${e}::${r}`;function probeBroker(e,r=800){return new Promise(t=>{const n=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});n.setTimeout(r,()=>{n.destroy();t(false)});n.on("error",()=>t(false))})}async function waitForBroker(e,r=20){for(let t=0;t<r;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(r=>setTimeout(r,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod27=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.42"?"7.0.42":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,r){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const n=t(e);const s=t(r);for(let o=0;o<3;o++){const a=n[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const r=JSON.parse(e);if(typeof r.checkedAt==="number"&&typeof r.latest==="string"){return r}}catch{}return null}async function fetchLatest(e){const r=new AbortController;const t=setTimeout(()=>r.abort(),FETCH_TIMEOUT_MS);try{const n=await fetch(REGISTRY_URL,{signal:r.signal});if(!n.ok)return null;const s=await n.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const r=await readCache();if(r&&e-r.checkedAt<CHECK_TTL_MS)return r.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const r=await resolveLatest(e);if(r&&isNewer(r,VERSION)){return`A newer buoy-mcp is available (${r}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,r){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const n=r.listDevices();if(n.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=n.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return json({devices:n,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const n=r.toolActions(t);if(n.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:n})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:n,timeoutMs:s})=>{try{const o=r.requireProDevice(n);const{payload:a}=await r.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:a})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:n,params:s,deviceId:o,timeoutMs:a})=>{try{const i=r.requireProDevice(o);const c=await r.callAction(t,n,s,i,{timeoutMs:a});return json({toolId:t,action:n,ok:true,result:c??null})}catch(i){return fail(errMsg(i))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,r){return typeof e==="number"&&e>0?e:r}function staleHeader(e){return e?`${e}
3
+ `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let r="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);r="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return r}connectSocket(){return new Promise((e,r)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let n=false;const s=()=>{if(n)return;n=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(n)return;n=true;r(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(n)return;n=true;clearTimeout(o);r(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,r=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!r)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const n of t)n(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const r=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(r)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,n]of this.snapshotWaiters)if(t.startsWith(r)){this.snapshotWaiters.delete(t);for(const s of n)s(void 0)}for(const[t,n]of this.pendingActions)if(n.target===e||n.target===ALL_DEVICES){clearTimeout(n.timer);this.pendingActions.delete(t);n.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[r,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===r))continue;this.lastSeen.delete(r);this.capabilities.delete(r);this.health.delete(r);this.devServerOrigins.delete(r);this.originProbed.delete(r);const n=`${r}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(n)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(n))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const r of e)r(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const r=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:r?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(r=>{const t=r.tools.find(n=>n.toolId===e);return t?{deviceId:r.deviceId,deviceName:r.deviceName,actions:t.actions}:null}).filter(r=>r!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const r=this.connectedDevices();const t=r.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(r.length>0){throw new Error(`Devices are connected (${r.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const n=t.filter(o=>o.tools.length>0);if(n.length===1)return n[0].deviceId;if(n.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=n.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const r=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${r(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const n=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===n);if(!s?.isPro)throw new Error(t(s));return n}healthOf(e){let r=this.health.get(e);if(!r){r={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,r)}return r}noteOk(e){const r=this.healthOf(e);r.consecutiveTimeouts=0;r.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,r,t){const n=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:n.consecutiveTimeouts,lastFullBudgetAt:n.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:r,action:t});if(o)n.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const r=this.healthOf(e);const t=r.lastOkAt?Math.round((Date.now()-r.lastOkAt)/1e3):null;const n=diagnoseCrashed({consecutiveTimeouts:r.consecutiveTimeouts,lastOkAt:r.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${r.consecutiveTimeouts} call${r.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,n?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const r=this.snapshots.get(snapKey(e,"console"));const t=r?.entries;if(!Array.isArray(t))return null;for(let n=t.length-1;n>=0;n--){const s=t[n];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,r){this.requireAdmission();const t=this.snapshotAt.get(snapKey(r,e));return t?Date.now()-t:null}noteDevServerOrigin(e,r){if(typeof r==="string"&&/^https?:\/\//.test(r)){this.devServerOrigins.set(e,r)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const r=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!r)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const r=this.devServerOrigins.get(e);if(r)return r;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,r){const t=snapKey(r,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:r});this.sendWatch(e,r)}sendWatch(e,r){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(r))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:r}of this.watching.values()){this.sendWatch(e,r)}}unwatch(e,r){const t=snapKey(r,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:false})}cachedSnapshot(e,r){this.requireAdmission();this.requireDeviceAccount(r);return this.snapshots.get(snapKey(r,e))}async getSnapshot(e,r,t={}){const n=this.resolveDevice(r);const s=snapKey(n,e);this.ensureWatching(e,n);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(n)){const d=this.healthOf(n);if(Date.now()-d.lastFullBudgetAt>RECOVERY_PROBE_MS){d.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:n});const i=await a;this.requireAdmission();this.requireDeviceAccount(n);if(i!==void 0){if(!t.silent)this.noteOk(n);return{deviceId:n,payload:i}}if(!t.silent)this.noteTimeout(n);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:n,payload:c,stale:this.staleNote(e,n)}}throw new Error(`Timed out waiting for "${e}" state from ${n} (is the tool active?). ${this.unresponsiveNote(n)}`)}staleNote(e,r){const t=this.snapshotAgeMs(e,r);const n=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${n}) \u2014 ${this.unresponsiveNote(r)}`}waitForSnapshot(e,r){return new Promise(t=>{const n=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},r);const o=a=>{clearTimeout(s);t(a)};n.push(o);this.snapshotWaiters.set(e,n)})}async callAction(e,r,t,n,s={}){this.requireAdmission();const o=n===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(n);if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto2.randomUUID)();const i=s.timeoutMs??this.timeoutFor(o,e,r);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${r}" got no answer in ${i}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:r,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${r}" failed on device`)}return c.result}};var snapKey=(e,r)=>`${e}::${r}`;function probeBroker(e,r=800){return new Promise(t=>{const n=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});n.setTimeout(r,()=>{n.destroy();t(false)});n.on("error",()=>t(false))})}async function waitForBroker(e,r=20){for(let t=0;t<r;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(r=>setTimeout(r,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod27=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.43"?"7.0.43":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,r){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const n=t(e);const s=t(r);for(let o=0;o<3;o++){const a=n[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const r=JSON.parse(e);if(typeof r.checkedAt==="number"&&typeof r.latest==="string"){return r}}catch{}return null}async function fetchLatest(e){const r=new AbortController;const t=setTimeout(()=>r.abort(),FETCH_TIMEOUT_MS);try{const n=await fetch(REGISTRY_URL,{signal:r.signal});if(!n.ok)return null;const s=await n.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const r=await readCache();if(r&&e-r.checkedAt<CHECK_TTL_MS)return r.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const r=await resolveLatest(e);if(r&&isNewer(r,VERSION)){return`A newer buoy-mcp is available (${r}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,r){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const n=r.listDevices();if(n.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=n.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return json({devices:n,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const n=r.toolActions(t);if(n.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:n})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:n,timeoutMs:s})=>{try{const o=r.requireProDevice(n);const{payload:a}=await r.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:a})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:n,params:s,deviceId:o,timeoutMs:a})=>{try{const i=r.requireProDevice(o);const c=await r.callAction(t,n,s,i,{timeoutMs:a});return json({toolId:t,action:n,ok:true,result:c??null})}catch(i){return fail(errMsg(i))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,r){return typeof e==="number"&&e>0?e:r}function staleHeader(e){return e?`${e}
4
4
 
5
5
  `:""}function inactiveToolMessage(e){return`No ${e.toolId} result from the device. The ${e.toolId} tool may be inactive, or the app needs a newer Buoy (${e.mcpTool} requires the ${e.action} action).`}function withDevice(e,r){return async t=>{try{const n=e.requireProDevice(t.deviceId);return await r(t,n)}catch(n){return fail2(errMsg2(n))}}}var PERF_TOOL_ID="perf-monitor";var clamp=(e,r,t)=>Math.max(r,Math.min(t,e));var WARMUP_CASE_NAME="__batch_warmup__";var THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function pick(e,r,t){return e??r??t}function buildAutomationConfig(e,r={}){const t=[];const n=(e.targetRoute||"").trim();if(!n)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=n==="/"?"/_buoy_bounce":"/";if(s===n){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((l,m)=>{const y=(l.name||"").trim()||`case ${m+1}`;return{id:`case-${m}`,name:y,params:l.params??{},route:l.route}});const a=clamp(Math.round(pick(e.runsPerCase,r.runsPerCase,FALLBACK.runsPerCase)),1,10);const i=clamp(Math.round(pick(e.coolDownMs,r.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const c=clamp(Math.round(pick(e.discardWarmupRuns,r.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,a-1);const d=e.shuffleCases??r.shuffleCases??o.length>3;if(o.length>3&&d===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(a===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:n,bounceRoute:s,perCaseDurationMs:clamp(Math.round(pick(e.perCaseDurationMs,r.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp(Math.round(pick(e.settleMs,r.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp(Math.round(pick(e.navTimeoutMs,r.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??r.discardWarmupCase??true,shuffleCases:d,reloadBetweenCases:e.reloadBetweenCases??r.reloadBetweenCases??false,captureRenders:e.captureRenders??r.captureRenders??true};return{config:u,warnings:t}}function describeBenchmarkSettings(e){const r=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const n=estimateBatchMs(e);const s=n/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(n/1e3)}s`;const a=[`**${r(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const i=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${r(i)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function estimateBatchMs(e){const r=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+r;let n=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)n+=e.cases.length*3e3;return n}function computeFlags(e){const r=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){r.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){r.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)r.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)r.push("JS thread stalls");return r}function rankBatchFromIndex(e,r,t,n=[]){const s=e.filter(h=>h.batchId===r);const o=new Map(n.map(h=>[h.index,h.reason]));const a=Array.from(new Set(s.map(h=>h.batchIndex??0))).sort((h,f)=>h-f);const i=h=>{const f=s.filter(g=>(g.batchIndex??0)===h);if(f.length===0)return void 0;return f.find(g=>g.isMedianRun===true)??f.find(g=>g.runIndex===0||g.runIndex===void 0)??f[0]};const c=h=>typeof h==="number"?h:null;const d=a.map(h=>{const f=i(h);const g=s.filter(x=>(x.batchIndex??0)===h).length;const v=f?.name??t?.[h]?.name??`case ${h+1}`;const $=o.get(h)??f?.batchFailureReason;const _=Boolean($)||!f;const k={batchIndex:h,caseName:v,params:f?.params??{},runs:g,failed:_,failureReason:_?$??"no run recorded":void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};return{...k,flags:_?[]:computeFlags(k)}}).filter(h=>h.caseName!==WARMUP_CASE_NAME).map((h,f)=>({...h,batchIndex:f}));const u=d[0];if(u){for(const h of d){if(h===u)continue;if(h.uiFps!=null&&u.uiFps!=null)h.deltaUiFps=round(h.uiFps-u.uiFps,1);if(h.jsFps!=null&&u.jsFps!=null)h.deltaJsFps=round(h.jsFps-u.jsFps,1)}}const l=d.filter(h=>!h.failed);const m=(h,f)=>{let g;let v=NaN;for(const $ of l){const _=h($);if(_==null)continue;if(g===void 0||f(_,v)){g=$;v=_}}return g?.caseName};const y=h=>h.jsJank==null&&h.uiJank==null?null:(h.jsJank??0)+(h.uiJank??0);const b={uiFps:m(h=>h.uiFps,(h,f)=>h>f),jsFps:m(h=>h.jsFps,(h,f)=>h>f),lowestCpu:m(h=>h.cpu,(h,f)=>h<f),lowestMem:m(h=>h.memMb,(h,f)=>h<f),fewestJank:m(y,(h,f)=>h<f)};const p=l.filter(h=>h.flags.length>0).map(h=>({caseName:h.caseName,flags:h.flags}));const w=l.filter(h=>h.flags.length===0).sort((h,f)=>(f.uiFps??0)-(h.uiFps??0)||(h.cpu??Infinity)-(f.cpu??Infinity)||(h.memMb??Infinity)-(f.memMb??Infinity)).map(h=>h.caseName);return{batchId:r,baselineName:u?.caseName??null,deviceMaxRefreshRate:l.find(h=>h.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:d,leaders:b,flagged:p,balanced:w}}function formatRankingMarkdown(e){const r=(l,m=1)=>l==null?"\u2014":l.toFixed(m);const t=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.toFixed(1)})`;const n=[];n.push(`### Benchmark batch \`${e.batchId}\``);n.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);n.push("");const s=e.rows.some(l=>l.thermalState);const o=s?" Thermal |":"";const a=s?"-------|":"";n.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);n.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const l of e.rows){const m=s?` ${THERMAL_MARK[l.thermalState??"unknown"]??"\u2014"} |`:"";if(l.failed){n.push(`| ${l.batchIndex} | ${l.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${m} \u274C FAILED (${l.failureReason}) |`);continue}const y=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"\u2713 clean";n.push(`| ${l.batchIndex} | ${l.caseName} | ${r(l.uiFps)}${t(l.deltaUiFps)} | ${r(l.jsFps)}${t(l.deltaJsFps)} | ${r(l.cpu)} | ${r(l.memMb)} | ${r(l.jsJank,0)} | ${r(l.uiJank,0)} |${m} ${y} |`)}const i=e.rows.filter(l=>l.thermalState==="serious"||l.thermalState==="critical");if(i.length){n.push("");n.push(`**\u{1F525} Thermally throttled: ${i.map(l=>l.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}n.push("");const c=e.leaders;const d=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)n.push(`**Per-metric best:** ${d}`);n.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(l=>!l.failed&&(l.renderCommits!=null||l.topRenderers.length>0)).map(l=>{const m=l.topRenderers.map(p=>`${p.name} \xD7${p.renders} (${p.totalMs}ms${p.cause?`, ${p.cause}`:""})`).join(", ");const y=l.renderCommits!=null?`${l.renderCommits} commits`:"";const b=l.renderWasted!=null?`${l.renderWasted} wasted`:"";return`- **${l.caseName}** \u2014 ${[y,b,m].filter(Boolean).join(": ")}`});if(u.length){n.push("");n.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");n.push(...u)}n.push("");n.push(`_Deltas vs baseline (case #0). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return n.join("\n")}function compareReportStats(e,r){const t=(n,s)=>({a:{avg:round(n.avg,1),p95:round(n.p95,1)},b:{avg:round(s.avg,1),p95:round(s.p95,1)},deltaAvg:round(s.avg-n.avg,1)});return{a:e.name,b:r.name,uiFps:t(e.stats.uiFps,r.stats.uiFps),jsFps:t(e.stats.jsFps,r.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,r.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,r.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:r.stats.jsJankFrames,delta:r.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:r.stats.uiJankFrames,delta:r.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,r.renders)}}function compareRenders(e,r){if(!e&&!r)return null;const t=n=>n??0;return{renders:{total:{a:t(e?.totalRenders),b:t(r?.totalRenders),delta:t(r?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(r?.totalCommits),delta:t(r?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(r?.wastedRenders),delta:t(r?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:r?.causes??{}}}}}function round(e,r){const t=Math.pow(10,r);return Math.round(e*t)/t}var POLL_INTERVAL_MS=1500;var delay2=e=>new Promise(r=>setTimeout(r,e));var json2=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var caseSchema=import_zod3.z.object({name:import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),params:import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function readDeviceSettings(e,r){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},r);if(!t)return{};const{perCaseDurationMs:n,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}=t;return{perCaseDurationMs:n,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}}catch{return{}}}async function readPerfSnapshot(e,r){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,r,{timeoutMs:4e3});return t}function batchIdsIn(e){const r=new Set;for(const t of e??[])if(t.batchId)r.add(t.batchId);return r}function runCountFor(e,r){return(e??[]).filter(t=>t.batchId===r).length}function newestBatchId(e){let r=null;for(const t of e??[]){if(t.batchId&&(!r||t.createdAt>r.at))r={id:t.batchId,at:t.createdAt}}return r?.id??null}function failuresFromIndex(e,r){const t=new Map;for(const n of e??[]){if(n.batchId===r&&n.batchFailureReason&&!t.has(n.batchIndex??0)){t.set(n.batchIndex??0,n.batchFailureReason)}}return[...t.entries()].map(([n,s])=>({index:n,reason:s}))}function expectedRunsFor(e){const r=e.cases.length+(e.discardWarmupCase===true?1:0);return r*(e.runsPerCase??1)}var SETTLE_WAIT_MS=1e4;function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,r,t,n=SETTLE_WAIT_MS){const s=Date.now();while(Date.now()-s<n){const o=await readPerfSnapshot(e,r).catch(()=>null);if(o&&!isRunnerBusy(o.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(n/1e3)}s`);return false}async function waitForBatchByIndex(e,r,t){const{preBatchIds:n,expectedRuns:s,maxWaitMs:o,log:a}=t;const i=Date.now();let c="";let d=false;let u=0;while(Date.now()-i<o){let y;try{y=await readPerfSnapshot(e,r)}catch{await delay2(POLL_INTERVAL_MS);continue}const b=y.index??[];const p=y.live?.automation?.phase??"idle";const w=y.live?.automationCompleted;if(w?.phase==="cancelled"&&!n.has(w.batchId)){return{batchId:w.batchId,failures:[],cancelled:true}}if(w?.phase==="done"&&!n.has(w.batchId)){return{batchId:w.batchId,failures:w.failures??failuresFromIndex(b,w.batchId)}}if(p!=="idle")d=true;if(p!==c){c=p;a(`automation phase: ${p}`)}const h=[...batchIdsIn(b)].find(f=>!n.has(f));if(h){const f=runCountFor(b,h);if(s>0&&f>=s){await waitForRunnerSettled(e,r,a);a(`batch ${h} complete (${f}/${s} runs saved)`);return{batchId:h,failures:failuresFromIndex(b,h)}}if(p==="idle"&&d&&f>0){if(++u>=3){a(`batch ${h} settled (${f} runs, automation idle)`);return{batchId:h,failures:failuresFromIndex(b,h)}}}else{u=0}}await delay2(POLL_INTERVAL_MS)}const l=await readPerfSnapshot(e,r).catch(()=>null);const m=l?[...batchIdsIn(l.index)].find(y=>!n.has(y)):void 0;if(m&&l)return{batchId:m,failures:failuresFromIndex(l.index,m),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}function registerPerfMonitorTools(e,r){e.registerTool("run_benchmark_batch",{description:"Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",inputSchema:{targetRoute:import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),maxWaitMs:import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const n=s=>process.stderr.write(`[buoy-mcp] ${s}
6
6
  `);try{const s=r.requireProDevice(t.deviceId);const o=await readDeviceSettings(r,s);const{config:a,warnings:i}=buildAutomationConfig(t,o);const c=estimateBatchMs(a);const d=t.maxWaitMs??c*3+6e4;const u=expectedRunsFor(a);if(!await waitForRunnerSettled(r,s,n)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const l=await readPerfSnapshot(r,s);const m=batchIdsIn(l.index);n(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${s}`);await r.callAction(PERF_TOOL_ID,"startAutomation",{config:a},s);const y=await waitForBatchByIndex(r,s,{preBatchIds:m,expectedRuns:u,maxWaitMs:d,log:n});if(y.cancelled){await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});return fail2(`Batch ${y.batchId} was cancelled before completing.`)}await r.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const b=await readPerfSnapshot(r,s);const p=rankBatchFromIndex(b.index??[],y.batchId,a.cases,y.failures);await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},s).catch(()=>{});const w=formatRankingMarkdown(p);const h={batchId:y.batchId,leaders:p.leaders,balanced:p.balanced,flagged:p.flagged,rows:p.rows,failures:y.failures,warnings:i,estimateMs:c,...y.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return text(`${w}
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{SyncClient as i,createMcpServer as p,runInit as a}from"./chunk-OOYI5S34.mjs";import{StdioServerTransport as l}from"@modelcontextprotocol/sdk/server/stdio.js";console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function d(){const e=process.env.BUOY_BROKER_PORT;const s=new i({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const o=p(s);const c=new l;let n=false;const r=t=>{if(n)return;n=true;process.stderr.write(`[buoy-mcp] shutting down (${t})
2
+ import{SyncClient as i,createMcpServer as p,runInit as a}from"./chunk-FLZAEJH4.mjs";import{StdioServerTransport as l}from"@modelcontextprotocol/sdk/server/stdio.js";console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function d(){const e=process.env.BUOY_BROKER_PORT;const s=new i({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const o=p(s);const c=new l;let n=false;const r=t=>{if(n)return;n=true;process.stderr.write(`[buoy-mcp] shutting down (${t})
3
3
  `);setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await s.stop().catch(()=>{});await o.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>r("stdin closed"));process.stdin.once("close",()=>r("stdin closed"));o.server.onclose=()=>r("transport closed");process.on("SIGINT",()=>r("SIGINT"));process.on("SIGTERM",()=>r("SIGTERM"));process.on("SIGHUP",()=>r("SIGHUP"));await o.connect(c);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
4
4
  `);s.start().then(t=>process.stderr.write(`[buoy-mcp] broker ${t}
5
5
  `)).catch(t=>process.stderr.write(`[buoy-mcp] broker connection failed: ${t?.message??t} \u2014 tools will report no devices until it recovers
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var t in r)__defProp(e,t,{get:r[t],enumerable:true})};var __copyProps=(e,r,t,n)=>{if(r&&typeof r==="object"||typeof r==="function"){for(let s of __getOwnPropNames(r))if(!__hasOwnProp.call(e,s)&&s!==t)__defProp(e,s,{get:()=>r[s],enumerable:!(n=__getOwnPropDesc(r,s))||n.enumerable})}return e};var __toESM=(e,r,t)=>(t=e!=null?__create(__getProtoOf(e)):{},__copyProps(r||!e||!e.__esModule?__defProp(t,"default",{value:e,enumerable:true}):t,e));var __toCommonJS=e=>__copyProps(__defProp({},"__esModule",{value:true}),e);var src_exports={};__export(src_exports,{ALL_DEVICES:()=>ALL_DEVICES,BROKER_PORT:()=>BROKER_PORT,BROKER_URL:()=>BROKER_URL,DASHBOARD_NAME:()=>DASHBOARD_NAME,DEFAULT_ACTION_TIMEOUT_MS:()=>DEFAULT_ACTION_TIMEOUT_MS,DEFAULT_SNAPSHOT_TIMEOUT_MS:()=>DEFAULT_SNAPSHOT_TIMEOUT_MS,PACKAGE_NAME:()=>PACKAGE_NAME,SyncClient:()=>SyncClient,VERSION:()=>VERSION,buildAutomationConfig:()=>buildAutomationConfig,compareReportStats:()=>compareReportStats,createMcpServer:()=>createMcpServer,estimateBatchMs:()=>estimateBatchMs,formatRankingMarkdown:()=>formatRankingMarkdown,getUpdateNotice:()=>getUpdateNotice,rankBatchFromIndex:()=>rankBatchFromIndex,registerConsoleTools:()=>registerConsoleTools,registerEventsTools:()=>registerEventsTools,registerGenericTools:()=>registerGenericTools,registerJotaiTools:()=>registerJotaiTools,registerPerfMonitorTools:()=>registerPerfMonitorTools,registerReactQueryTools:()=>registerReactQueryTools,registerReduxTools:()=>registerReduxTools,registerRoutesTools:()=>registerRoutesTools,registerScreenshotTools:()=>registerScreenshotTools,registerStorageTools:()=>registerStorageTools,registerZustandTools:()=>registerZustandTools,runInit:()=>runInit});module.exports=__toCommonJS(src_exports);var import_node_fs=require("fs");var import_node_path=require("path");var import_node_os=require("os");var import_node_crypto=require("crypto");var import_headless=require("@buoy-gg/license/headless");var KEY_NAMES=["BUOY_LICENSE_KEY","BUOY_KEY","EXPO_PUBLIC_BUOY_KEY","VITE_BUOY_KEY","REACT_APP_BUOY_KEY"];function readProjectKey(e,r=process.env){for(const t of KEY_NAMES)if(r[t]!==void 0)return r[t].trim().toUpperCase()||null;for(const t of[".env.development.local",".env.local",".env"]){let n;try{n=(0,import_node_fs.readFileSync)((0,import_node_path.join)(e,t),"utf8")}catch{continue}const s=new Map;for(const o of n.split(/\r?\n/)){const a=o.match(/^\s*(?:export\s+)?([A-Z_][A-Z_0-9]*)\s*=\s*(.*)$/);if(!a)continue;const i=a[2].trim();const c=i.match(/^(['"])(.*?)\1\s*(?:#.*)?$/);s.set(a[1],c?c[2]:i.split("#")[0].trim())}for(const o of KEY_NAMES)if(s.has(o))return s.get(o).trim().toUpperCase()||null}return null}var NodeAccount=class{constructor(e,r={}){this.changed=e;this.key=null;this.cache=null;this.timer=null;this.generation=0;this.busy=false;this.nextCheck=0;this.retry=0;this.lastAdmission=false;this.hasAccess=()=>(0,import_headless.resolveAccountAccess)({initialized:true,validating:false,key:this.key,tierSource:this.cache?.tier?"key":"none",verifiedAt:this.cache?.cachedAt??null}).status==="admitted";this.projectDir=(0,import_node_path.resolve)(r.projectDir??process.env.BUOY_PROJECT_DIR??process.cwd());const t=(0,import_node_crypto.createHash)("sha256").update(this.projectDir).digest("hex").slice(0,20);this.cacheFile=r.cacheFile??(0,import_node_path.join)((0,import_node_os.homedir)(),".buoy",`account-${t}.json`)}start(){if(this.timer)return;try{const e=JSON.parse((0,import_node_fs.readFileSync)(this.cacheFile,"utf8"));if(e.valid&&(0,import_headless.verifyCacheSignature)(e))this.cache=e}catch{}void this.tick();this.timer=setInterval(()=>{void this.tick()},1e3);this.timer.unref()}stop(){++this.generation;if(this.timer)clearInterval(this.timer);this.timer=null;this.key=null;this.cache=null;this.notify()}notify(){const e=this.hasAccess();if(e===this.lastAdmission)return;this.lastAdmission=e;this.changed()}async tick(){const e=readProjectKey(this.projectDir);if(e!==this.key){++this.generation;this.key=e;if(this.cache?.licenseKey!==e)this.cache=null;this.nextCheck=0;this.retry=0;if(!e)this.removeCache()}this.notify();if(!e||this.busy||Date.now()<this.nextCheck)return;if(this.cache&&this.hasAccess()&&Date.now()-this.cache.lastValidatedAt<import_headless.LICENSE_LAUNCH_VALIDATION_TTL&&this.nextCheck===0){this.nextCheck=this.cache.lastValidatedAt+import_headless.LICENSE_LAUNCH_VALIDATION_TTL;return}this.busy=true;const r=this.generation;try{if((0,import_headless.isBuoyToken)(e)){const i=(0,import_headless.verifyFreeToken)(e);if(!i){this.cache=null;this.removeCache()}else{const c=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:`token:${i.accountHash}`,valid:true,tier:"free",isPro:false,expiresAt:null,cachedAt:c,lastValidatedAt:c});this.persistCache()}this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const t=await(0,import_headless.validateLicense)(e);if(r!==this.generation||readProjectKey(this.projectDir)!==e)return;const n=t.data?.relationships?.product?.data?.id;const s=t.meta.code==="EXPIRED"&&((0,import_headless.isRecognizedAccount)(t.data)||this.cache?.licenseKey===e);if(n&&n!==import_headless.KEYGEN_CONFIG.productId||!s&&!(0,import_headless.isAcceptedValidation)(t)){this.cache=null;this.removeCache();this.nextCheck=Date.now()+import_headless.LICENSE_REVALIDATION_INTERVAL;return}const o=s?"free":(0,import_headless.resolveTier)(t.data);const a=Date.now();this.cache=(0,import_headless.signCache)({licenseKey:e,licenseId:t.data?.id??"",valid:true,tier:o,isPro:o==="pro",expiresAt:s?null:t.data.attributes.expiry,cachedAt:a,lastValidatedAt:a});this.persistCache();this.retry=0;this.nextCheck=a+import_headless.LICENSE_REVALIDATION_INTERVAL}catch(t){if(r!==this.generation)return;const n=(0,import_headless.isTransientLicenseError)(t)?import_headless.LICENSE_TRANSIENT_RETRY_DELAYS[this.retry++]:void 0;this.nextCheck=Date.now()+(n??import_headless.LICENSE_REVALIDATION_INTERVAL)}finally{this.busy=false;this.notify()}}persistCache(){try{(0,import_node_fs.mkdirSync)((0,import_node_path.dirname)(this.cacheFile),{recursive:true,mode:448});(0,import_node_fs.writeFileSync)(`${this.cacheFile}.tmp`,JSON.stringify(this.cache),{mode:384});(0,import_node_fs.renameSync)(`${this.cacheFile}.tmp`,this.cacheFile)}catch{}}removeCache(){try{(0,import_node_fs.unlinkSync)(this.cacheFile)}catch{}}};var import_node_http=__toESM(require("http"));var import_node_crypto2=require("crypto");var import_socket=require("socket.io-client");var import_sync_broker2=require("@buoy-gg/sync-broker");var import_sync_broker=require("@buoy-gg/sync-broker");var BROKER_PORT=import_sync_broker.DEFAULT_BROKER_PORT;var BROKER_URL=`http://localhost:${BROKER_PORT}`;var DEFAULT_ACTION_TIMEOUT_MS=15e3;var UNRESPONSIVE_ACTION_TIMEOUT_MS=2500;var UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS=1200;var RECOVERY_PROBE_MS=2e4;var UNRESPONSIVE_AFTER_MS=3e4;var DEFAULT_SNAPSHOT_TIMEOUT_MS=5e3;var DEVICE_RETENTION_MS=15*6e4;var DASHBOARD_NAME="Dashboard";var ALL_DEVICES="All";function isRecoveryAction(e,r){return e==="app"&&(r==="ping"||r==="reloadApp")}function isUnresponsive(e){return e>=2}function actionTimeoutMs(e){if(isRecoveryAction(e.toolId,e.action)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(!isUnresponsive(e.consecutiveTimeouts)){return{timeoutMs:e.fullMs,usedFullBudget:false}}if(e.now-e.lastFullBudgetAt>e.recoveryProbeMs){return{timeoutMs:e.fullMs,usedFullBudget:true}}return{timeoutMs:e.shortMs,usedFullBudget:false}}function diagnoseCrashed(e){if(e.consecutiveTimeouts>=2)return true;if(e.lastOkAt>0&&e.now-e.lastOkAt>e.unresponsiveAfterMs){return true}return false}var SyncClient=class{constructor(e={}){this.socket=null;this.admitted=false;this.lockedDevices=new Set;this.hostAccount=null;this.broker=null;this.capabilities=new Map;this.devices=[];this.snapshots=new Map;this.watching=new Map;this.pendingActions=new Map;this.snapshotWaiters=new Map;this.snapshotAt=new Map;this.health=new Map;this.devServerOrigins=new Map;this.originProbed=new Set;this.lastSeen=new Map;this.port=e.port??BROKER_PORT;this.url=e.url??(e.port?`http://localhost:${e.port}`:BROKER_URL);this.actionTimeoutMs=e.actionTimeoutMs??DEFAULT_ACTION_TIMEOUT_MS;this.log=e.log??(r=>process.stderr.write(`[buoy-mcp] ${r}
2
- `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let r="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);r="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return r}connectSocket(){return new Promise((e,r)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let n=false;const s=()=>{if(n)return;n=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(n)return;n=true;r(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(n)return;n=true;clearTimeout(o);r(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,r=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!r)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const n of t)n(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const r=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(r)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,n]of this.snapshotWaiters)if(t.startsWith(r)){this.snapshotWaiters.delete(t);for(const s of n)s(void 0)}for(const[t,n]of this.pendingActions)if(n.target===e||n.target===ALL_DEVICES){clearTimeout(n.timer);this.pendingActions.delete(t);n.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[r,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===r))continue;this.lastSeen.delete(r);this.capabilities.delete(r);this.health.delete(r);this.devServerOrigins.delete(r);this.originProbed.delete(r);const n=`${r}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(n)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(n))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const r of e)r(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const r=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:r?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(r=>{const t=r.tools.find(n=>n.toolId===e);return t?{deviceId:r.deviceId,deviceName:r.deviceName,actions:t.actions}:null}).filter(r=>r!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const r=this.connectedDevices();const t=r.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(r.length>0){throw new Error(`Devices are connected (${r.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const n=t.filter(o=>o.tools.length>0);if(n.length===1)return n[0].deviceId;if(n.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=n.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const r=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${r(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const n=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===n);if(!s?.isPro)throw new Error(t(s));return n}healthOf(e){let r=this.health.get(e);if(!r){r={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,r)}return r}noteOk(e){const r=this.healthOf(e);r.consecutiveTimeouts=0;r.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,r,t){const n=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:n.consecutiveTimeouts,lastFullBudgetAt:n.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:r,action:t});if(o)n.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const r=this.healthOf(e);const t=r.lastOkAt?Math.round((Date.now()-r.lastOkAt)/1e3):null;const n=diagnoseCrashed({consecutiveTimeouts:r.consecutiveTimeouts,lastOkAt:r.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${r.consecutiveTimeouts} call${r.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,n?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const r=this.snapshots.get(snapKey(e,"console"));const t=r?.entries;if(!Array.isArray(t))return null;for(let n=t.length-1;n>=0;n--){const s=t[n];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,r){this.requireAdmission();const t=this.snapshotAt.get(snapKey(r,e));return t?Date.now()-t:null}noteDevServerOrigin(e,r){if(typeof r==="string"&&/^https?:\/\//.test(r)){this.devServerOrigins.set(e,r)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const r=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!r)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const r=this.devServerOrigins.get(e);if(r)return r;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,r){const t=snapKey(r,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:r});this.sendWatch(e,r)}sendWatch(e,r){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(r))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:r}of this.watching.values()){this.sendWatch(e,r)}}unwatch(e,r){const t=snapKey(r,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:false})}cachedSnapshot(e,r){this.requireAdmission();this.requireDeviceAccount(r);return this.snapshots.get(snapKey(r,e))}async getSnapshot(e,r,t={}){const n=this.resolveDevice(r);const s=snapKey(n,e);this.ensureWatching(e,n);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(n)){const d=this.healthOf(n);if(Date.now()-d.lastFullBudgetAt>RECOVERY_PROBE_MS){d.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:n});const i=await a;this.requireAdmission();this.requireDeviceAccount(n);if(i!==void 0){if(!t.silent)this.noteOk(n);return{deviceId:n,payload:i}}if(!t.silent)this.noteTimeout(n);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:n,payload:c,stale:this.staleNote(e,n)}}throw new Error(`Timed out waiting for "${e}" state from ${n} (is the tool active?). ${this.unresponsiveNote(n)}`)}staleNote(e,r){const t=this.snapshotAgeMs(e,r);const n=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${n}) \u2014 ${this.unresponsiveNote(r)}`}waitForSnapshot(e,r){return new Promise(t=>{const n=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},r);const o=a=>{clearTimeout(s);t(a)};n.push(o);this.snapshotWaiters.set(e,n)})}async callAction(e,r,t,n,s={}){this.requireAdmission();const o=n===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(n);if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto2.randomUUID)();const i=s.timeoutMs??this.timeoutFor(o,e,r);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${r}" got no answer in ${i}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:r,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${r}" failed on device`)}return c.result}};var snapKey=(e,r)=>`${e}::${r}`;function probeBroker(e,r=800){return new Promise(t=>{const n=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});n.setTimeout(r,()=>{n.destroy();t(false)});n.on("error",()=>t(false))})}async function waitForBroker(e,r=20){for(let t=0;t<r;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(r=>setTimeout(r,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod27=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.42"?"7.0.42":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,r){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const n=t(e);const s=t(r);for(let o=0;o<3;o++){const a=n[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const r=JSON.parse(e);if(typeof r.checkedAt==="number"&&typeof r.latest==="string"){return r}}catch{}return null}async function fetchLatest(e){const r=new AbortController;const t=setTimeout(()=>r.abort(),FETCH_TIMEOUT_MS);try{const n=await fetch(REGISTRY_URL,{signal:r.signal});if(!n.ok)return null;const s=await n.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const r=await readCache();if(r&&e-r.checkedAt<CHECK_TTL_MS)return r.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const r=await resolveLatest(e);if(r&&isNewer(r,VERSION)){return`A newer buoy-mcp is available (${r}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,r){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const n=r.listDevices();if(n.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=n.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return json({devices:n,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const n=r.toolActions(t);if(n.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:n})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:n,timeoutMs:s})=>{try{const o=r.requireProDevice(n);const{payload:a}=await r.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:a})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:n,params:s,deviceId:o,timeoutMs:a})=>{try{const i=r.requireProDevice(o);const c=await r.callAction(t,n,s,i,{timeoutMs:a});return json({toolId:t,action:n,ok:true,result:c??null})}catch(i){return fail(errMsg(i))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,r){return typeof e==="number"&&e>0?e:r}function staleHeader(e){return e?`${e}
2
+ `));this.autoSpawn=e.autoSpawnBroker??true}async start(){const e=await probeBroker(this.url);let r="connected";if(!e){if(!this.autoSpawn){throw new Error(`No Buoy broker reachable at ${this.url} and autoSpawnBroker is off. Start the Buoy desktop app, or enable autoSpawnBroker.`)}this.log(`no broker at ${this.url} \u2014 spawning one in-process on :${this.port}`);this.hostAccount=new NodeAccount(()=>this.broker?.refreshAccess());this.hostAccount.start();this.broker=(0,import_sync_broker2.createBroker)({canUseDesktop:this.hostAccount.hasAccess,port:this.port,onError:t=>this.log(`broker error: ${t.code??t.message}`)});await waitForBroker(this.url);r="spawned"}else{this.log(`found a running broker at ${this.url} \u2014 connecting as a dashboard`)}await this.connectSocket();return r}connectSocket(){return new Promise((e,r)=>{const t=(0,import_socket.io)(this.url,{transports:["websocket"],query:{deviceName:DASHBOARD_NAME},reconnection:true,reconnectionDelay:500,timeout:4e3});this.socket=t;let n=false;const s=()=>{if(n)return;n=true;this.log("connected to broker");e()};const o=setTimeout(()=>{if(n)return;n=true;r(new Error("Broker account is locked or the broker needs an update. Sign in to Buoy Desktop or run npx buoy login for standalone MCP."))},5e3);t.on("buoy:access",a=>{const i=!!a&&typeof a==="object"&&a.protocol===1&&a.ready===true;this.setAdmission(i);if(i){clearTimeout(o);this.rearmWatches();s()}});t.on("connect",()=>{t.emit("buoy:access:request")});t.on("disconnect",()=>this.setAdmission(false,true));t.once("connect_error",a=>{if(n)return;n=true;clearTimeout(o);r(new Error(`failed to connect to broker at ${this.url}: ${a.message}`))});t.on("devtool-capabilities",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;this.capabilities.set(a.persistentDeviceId,a);void this.learnDevServerOrigin(a.persistentDeviceId)});t.on("all-devices-update",a=>{if(!this.admitted)return;if(!Array.isArray(a))return;let i=false;for(const d of a){if(d.deviceId&&d.tier==="locked")this.lockDevice(String(d.deviceId));else if(d.deviceId&&(d.tier==="free"||d.tier==="pro")){i=this.lockedDevices.delete(String(d.deviceId))||i}}this.devices=a;const c=Date.now();for(const d of a)if(d.deviceId)this.lastSeen.set(d.deviceId,c);this.pruneStaleDevices();if(i)this.rearmWatches()});t.on("devtool-sync",a=>{if(!this.admitted||this.lockedDevices.has(a.persistentDeviceId))return;const i=snapKey(a.persistentDeviceId,a.toolId);this.snapshots.set(i,a.payload);this.snapshotAt.set(i,Date.now());const c=this.snapshotWaiters.get(i);if(c&&c.length){this.snapshotWaiters.set(i,[]);for(const d of c)d(a.payload)}});t.on("devtool-action-result",a=>{if(!this.admitted)return;const i=this.pendingActions.get(a.requestId);if(!i)return;this.pendingActions.delete(a.requestId);clearTimeout(i.timer);i.resolve(a)})})}requireAdmission(){if(!this.admitted)throw new Error("A verified Buoy account and an updated broker are required. Sign in to Buoy Desktop, or run npx buoy login for standalone MCP.")}setAdmission(e,r=false){this.admitted=e;if(e)return;this.devices=[];this.lockedDevices.clear();this.capabilities.clear();this.snapshots.clear();this.snapshotAt.clear();this.devServerOrigins.clear();this.originProbed.clear();this.health.clear();this.lastSeen.clear();if(!r)this.watching.clear();for(const t of this.pendingActions.values()){clearTimeout(t.timer);t.reject(new Error("Buoy account access ended while the action was pending."))}this.pendingActions.clear();for(const t of this.snapshotWaiters.values())for(const n of t)n(void 0);this.snapshotWaiters.clear();if(this.socket)this.socket.sendBuffer=[]}requireDeviceAccount(e){if(this.lockedDevices.has(e))throw new Error("The device's Buoy account access ended. Sign in and verify its key.")}lockDevice(e){this.lockedDevices.add(e);this.capabilities.delete(e);this.health.delete(e);this.devServerOrigins.delete(e);this.originProbed.delete(e);const r=`${e}::`;for(const t of this.snapshots.keys())if(t.startsWith(r)){this.snapshots.delete(t);this.snapshotAt.delete(t)}for(const[t,n]of this.snapshotWaiters)if(t.startsWith(r)){this.snapshotWaiters.delete(t);for(const s of n)s(void 0)}for(const[t,n]of this.pendingActions)if(n.target===e||n.target===ALL_DEVICES){clearTimeout(n.timer);this.pendingActions.delete(t);n.reject(new Error("Device account access ended while the action was pending."))}}pruneStaleDevices(){const e=Date.now()-DEVICE_RETENTION_MS;for(const[r,t]of this.lastSeen){if(t>e)continue;if(this.devices.some(s=>s.deviceId===r))continue;this.lastSeen.delete(r);this.capabilities.delete(r);this.health.delete(r);this.devServerOrigins.delete(r);this.originProbed.delete(r);const n=`${r}::`;for(const s of[...this.snapshots.keys()]){if(s.startsWith(n)){this.snapshots.delete(s);this.snapshotAt.delete(s)}}for(const s of[...this.watching.keys()]){if(s.startsWith(n))this.watching.delete(s)}}}async stop(){this.setAdmission(false);this.hostAccount?.stop();this.hostAccount=null;for(const[,e]of this.pendingActions){clearTimeout(e.timer);e.reject(new Error("SyncClient stopped"))}this.pendingActions.clear();for(const[,e]of this.snapshotWaiters)for(const r of e)r(void 0);this.snapshotWaiters.clear();this.socket?.close();this.socket=null;if(this.broker){await this.broker.close();this.broker=null}}listDevices(){if(!this.admitted)return[];return this.devices.filter(e=>e.deviceName!==DASHBOARD_NAME&&e.deviceId).map(e=>{const r=this.capabilities.get(e.deviceId);return{deviceId:e.deviceId,deviceName:e.deviceName,platform:e.platform,isConnected:e.isConnected!==false,isPro:e.isPro===true,tier:e.tier==="free"||e.tier==="pro"?e.tier:e.tier===void 0&&e.isPro===true?"pro":"locked",collision:e.collision===true,tools:r?.tools??[]}})}connectedDevices(){return this.listDevices().filter(e=>e.isConnected)}toolActions(e){return this.listDevices().map(r=>{const t=r.tools.find(n=>n.toolId===e);return t?{deviceId:r.deviceId,deviceName:r.deviceName,actions:t.actions}:null}).filter(r=>r!==null)}resolveDevice(e){this.requireAdmission();if(e){this.requireDeviceAccount(e);return e}const r=this.connectedDevices();const t=r.filter(o=>!this.lockedDevices.has(o.deviceId));if(t.length===0){if(r.length>0){throw new Error(`Devices are connected (${r.map(o=>o.deviceName).join(", ")}), but the broker has not admitted their accounts. Check app account validation and the account announcement after reconnect; this does not prove the user is signed out.`)}throw new Error("No devices are connected to the broker. Open the app with Buoy devtools running.")}if(t.length===1)return t[0].deviceId;const n=t.filter(o=>o.tools.length>0);if(n.length===1)return n[0].deviceId;if(n.length===0){const o=t.map(a=>a.deviceName).join(", ");throw new Error(`Connected devices (${o}) are not exposing any Buoy tools yet. Make sure the app is in the foreground with devtools mounted, then retry.`)}const s=n.map(o=>`${o.deviceName} (${o.deviceId})`).join(", ");throw new Error(`Multiple devices connected \u2014 specify deviceId. Connected: ${s}`)}requireProDevice(e){this.requireAdmission();const r=o=>{if(!o)return"";return o.tier==="locked"?` The broker has not confirmed account access for "${o.deviceName}"; a locked status does not establish whether a key is configured.`:` The connected app "${o.deviceName}" is on the ${o.tier} tier.`};const t=o=>`Buoy Pro is required to use the MCP.${r(o)} Enter a license in the app, or \u2014 if it runs headless (no on-device UI) \u2014 pass \`licenseKey\` to \`<FloatingDevTools />\`. Run \`list_devices\` to check status.`;if(e===ALL_DEVICES){if(!this.connectedDevices().some(o=>o.isPro))throw new Error(t());return ALL_DEVICES}const n=this.resolveDevice(e);const s=this.connectedDevices().find(o=>o.deviceId===n);if(!s?.isPro)throw new Error(t(s));return n}healthOf(e){let r=this.health.get(e);if(!r){r={consecutiveTimeouts:0,lastOkAt:0,lastFullBudgetAt:0};this.health.set(e,r)}return r}noteOk(e){const r=this.healthOf(e);r.consecutiveTimeouts=0;r.lastOkAt=Date.now()}noteTimeout(e){this.healthOf(e).consecutiveTimeouts+=1}isUnresponsive(e){return isUnresponsive(this.healthOf(e).consecutiveTimeouts)}timeoutFor(e,r,t){const n=this.healthOf(e);const{timeoutMs:s,usedFullBudget:o}=actionTimeoutMs({consecutiveTimeouts:n.consecutiveTimeouts,lastFullBudgetAt:n.lastFullBudgetAt,now:Date.now(),fullMs:this.actionTimeoutMs,shortMs:UNRESPONSIVE_ACTION_TIMEOUT_MS,recoveryProbeMs:RECOVERY_PROBE_MS,toolId:r,action:t});if(o)n.lastFullBudgetAt=Date.now();return s}unresponsiveNote(e){const r=this.healthOf(e);const t=r.lastOkAt?Math.round((Date.now()-r.lastOkAt)/1e3):null;const n=diagnoseCrashed({consecutiveTimeouts:r.consecutiveTimeouts,lastOkAt:r.lastOkAt,now:Date.now(),unresponsiveAfterMs:UNRESPONSIVE_AFTER_MS});const s=[`The app on "${e}" is not responding (${r.consecutiveTimeouts} call${r.consecutiveTimeouts===1?"":"s"} unanswered${t!=null?`, last answered ${t}s ago`:""}).`,n?"It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded.":"It may be busy, mid-reload, or crashing."];const o=this.lastFatalFor(e);if(o)s.push(`Last captured error: ${o}`);s.push(o?"Read the crash state with get_console / get_storage (served from the last sync), then reload_app to recover.":"Try get_console (served from the last sync) for the app's last words, then reload_app to recover.");return s.join(" ")}lastFatalFor(e){const r=this.snapshots.get(snapKey(e,"console"));const t=r?.entries;if(!Array.isArray(t))return null;for(let n=t.length-1;n>=0;n--){const s=t[n];if(s?.fatal||s?.level==="error"){const o=(s.message??"").split("\n")[0];if(o)return o.length>300?`${o.slice(0,300)}\u2026`:o}}return null}snapshotAgeMs(e,r){this.requireAdmission();const t=this.snapshotAt.get(snapKey(r,e));return t?Date.now()-t:null}noteDevServerOrigin(e,r){if(typeof r==="string"&&/^https?:\/\//.test(r)){this.devServerOrigins.set(e,r)}}async learnDevServerOrigin(e){if(this.devServerOrigins.has(e)||this.originProbed.has(e))return;this.originProbed.add(e);const r=this.capabilities.get(e)?.tools?.some(t=>t.toolId==="app");if(!r)return;try{const{payload:t}=await this.getSnapshot("app",e,{timeoutMs:4e3,silent:true});this.noteDevServerOrigin(e,t?.devServerOrigin)}catch{}}devServerOrigin(e){this.requireAdmission();const r=this.devServerOrigins.get(e);if(r)return r;const t=this.snapshots.get(snapKey(e,"app"));if(typeof t?.devServerOrigin==="string")return t.devServerOrigin;return null}ensureWatching(e,r){const t=snapKey(r,e);if(this.watching.has(t))return;this.watching.set(t,{toolId:e,targetDeviceId:r});this.sendWatch(e,r)}sendWatch(e,r){if(!this.admitted||!this.socket?.connected||this.lockedDevices.has(r))return;this.socket?.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:true,stream:false})}rearmWatches(){for(const{toolId:e,targetDeviceId:r}of this.watching.values()){this.sendWatch(e,r)}}unwatch(e,r){const t=snapKey(r,e);if(!this.watching.delete(t))return;if(this.admitted&&this.socket?.connected)this.socket.emit("devtool-watch",{toolId:e,targetDeviceId:r,watching:false})}cachedSnapshot(e,r){this.requireAdmission();this.requireDeviceAccount(r);return this.snapshots.get(snapKey(r,e))}async getSnapshot(e,r,t={}){const n=this.resolveDevice(r);const s=snapKey(n,e);this.ensureWatching(e,n);let o=t.timeoutMs??DEFAULT_SNAPSHOT_TIMEOUT_MS;if(t.timeoutMs===void 0&&this.isUnresponsive(n)){const d=this.healthOf(n);if(Date.now()-d.lastFullBudgetAt>RECOVERY_PROBE_MS){d.lastFullBudgetAt=Date.now()}else{o=UNRESPONSIVE_SNAPSHOT_TIMEOUT_MS}}const a=this.waitForSnapshot(s,o);this.socket?.emit("request-devtool-state",{toolId:e,targetDeviceId:n});const i=await a;this.requireAdmission();this.requireDeviceAccount(n);if(i!==void 0){if(!t.silent)this.noteOk(n);return{deviceId:n,payload:i}}if(!t.silent)this.noteTimeout(n);const c=this.snapshots.get(s);if(c!==void 0){return{deviceId:n,payload:c,stale:this.staleNote(e,n)}}throw new Error(`Timed out waiting for "${e}" state from ${n} (is the tool active?). ${this.unresponsiveNote(n)}`)}staleNote(e,r){const t=this.snapshotAgeMs(e,r);const n=t==null?"an earlier sync":`${Math.round(t/1e3)}s ago`;return`\u26A0\uFE0F Served from the last sync (${n}) \u2014 ${this.unresponsiveNote(r)}`}waitForSnapshot(e,r){return new Promise(t=>{const n=this.snapshotWaiters.get(e)??[];const s=setTimeout(()=>{const a=(this.snapshotWaiters.get(e)??[]).filter(i=>i!==o);this.snapshotWaiters.set(e,a);t(void 0)},r);const o=a=>{clearTimeout(s);t(a)};n.push(o);this.snapshotWaiters.set(e,n)})}async callAction(e,r,t,n,s={}){this.requireAdmission();const o=n===ALL_DEVICES?ALL_DEVICES:this.resolveDevice(n);if(!this.socket)throw new Error("SyncClient is not connected");const a=(0,import_node_crypto2.randomUUID)();const i=s.timeoutMs??this.timeoutFor(o,e,r);const c=await new Promise((d,u)=>{const l=setTimeout(()=>{this.pendingActions.delete(a);if(o!==ALL_DEVICES&&!s.silent)this.noteTimeout(o);u(new Error(`Action "${e}.${r}" got no answer in ${i}ms. `+(o===ALL_DEVICES?"No device responded.":this.unresponsiveNote(o))))},i);this.pendingActions.set(a,{resolve:d,reject:u,timer:l,target:o});this.socket.emit("devtool-action",{requestId:a,toolId:e,targetDeviceId:o,action:r,params:t??{}})});if(o!==ALL_DEVICES&&!s.silent)this.noteOk(o);if(!c.ok){throw new Error(c.error||`Action "${e}.${r}" failed on device`)}return c.result}};var snapKey=(e,r)=>`${e}::${r}`;function probeBroker(e,r=800){return new Promise(t=>{const n=import_node_http.default.get(e,s=>{s.resume();t((s.statusCode??0)>0)});n.setTimeout(r,()=>{n.destroy();t(false)});n.on("error",()=>t(false))})}async function waitForBroker(e,r=20){for(let t=0;t<r;t++){if(await probeBroker(e,300))return;await delay(50)}throw new Error(`Spawned broker never became reachable at ${e}`)}var delay=e=>new Promise(r=>setTimeout(r,e));var import_mcp=require("@modelcontextprotocol/sdk/server/mcp.js");var import_zod27=require("zod");var import_zod=require("zod");var import_node_fs2=require("fs");var import_node_os2=__toESM(require("os"));var import_node_path2=__toESM(require("path"));var VERSION="7.0.43"?"7.0.43":"0.0.0-dev";var PACKAGE_NAME="@buoy-gg/mcp";var CHECK_TTL_MS=24*60*60*1e3;var FETCH_TIMEOUT_MS=1500;var REGISTRY_URL=`https://registry.npmjs.org/${PACKAGE_NAME}/latest`;var CACHE_FILE=import_node_path2.default.join(import_node_os2.default.tmpdir(),"buoy-mcp-update-check.json");function isNewer(e,r){const t=o=>o.split("-")[0].split(".").map(a=>Number(a)||0);const n=t(e);const s=t(r);for(let o=0;o<3;o++){const a=n[o]??0;const i=s[o]??0;if(a>i)return true;if(a<i)return false}return false}async function readCache(){try{const e=await import_node_fs2.promises.readFile(CACHE_FILE,"utf8");const r=JSON.parse(e);if(typeof r.checkedAt==="number"&&typeof r.latest==="string"){return r}}catch{}return null}async function fetchLatest(e){const r=new AbortController;const t=setTimeout(()=>r.abort(),FETCH_TIMEOUT_MS);try{const n=await fetch(REGISTRY_URL,{signal:r.signal});if(!n.ok)return null;const s=await n.json();const o=typeof s.version==="string"?s.version:null;if(o){await import_node_fs2.promises.writeFile(CACHE_FILE,JSON.stringify({checkedAt:e,latest:o})).catch(()=>{})}return o}catch{return null}finally{clearTimeout(t)}}async function resolveLatest(e){const r=await readCache();if(r&&e-r.checkedAt<CHECK_TTL_MS)return r.latest;return fetchLatest(e)}async function getUpdateNotice(e=Date.now()){if(VERSION==="0.0.0-dev")return null;try{const r=await resolveLatest(e);if(r&&isNewer(r,VERSION)){return`A newer buoy-mcp is available (${r}; you're on ${VERSION}). Update by re-running: npx -y ${PACKAGE_NAME}@latest init \u2014 then restart your editor.`}}catch{}return null}function json(e){return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}function fail(e){return{isError:true,content:[{type:"text",text:e}]}}function registerGenericTools(e,r){e.registerTool("list_devices",{description:"List the React Native devices connected to the Buoy broker and the dev tools each one exposes (toolId, version, available actions). Start here.",inputSchema:{}},async()=>{const t=await getUpdateNotice();const n=r.listDevices();if(n.length===0){return json({devices:[],hint:"No devices connected. Open the app with Buoy devtools running, then retry.",...t?{updateAvailable:t}:{}})}const s=n.filter(c=>c.isConnected);const o=s.some(c=>c.isPro);const a=s.some(c=>c.tier!=="locked");const i=s.filter(c=>c.collision);return json({devices:n,...i.length>0?{warning:`${i.map(c=>`"${c.deviceId}"`).join(", ")}: more than one app is registered under this id, so actions sent to it land on an arbitrary device. Give each device its own externalSync.deviceId (or leave it unset for a per-install default) before mutating anything.`}:{},...o?{}:{hint:a?"The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app, or pass `licenseKey` to `<FloatingDevTools />` if it runs headless.":"The broker has not confirmed account access for the connected devices, so data/action tools are blocked. A locked status can mean missing configuration, pending/failed validation, or a missing account announcement after reconnect; it does not prove the user is signed out. Check app account status and broker compatibility."},...t?{updateAvailable:t}:{}})});e.registerTool("list_tool_actions",{description:"List the actions a given tool exposes, and which connected devices offer it. Use the action names with call_action.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor", "network", "react-query".')}},async({toolId:t})=>{const n=r.toolActions(t);if(n.length===0){return fail(`No connected device exposes tool "${t}". Call list_devices to see available tools.`)}return json({toolId:t,offerings:n})});e.registerTool("get_snapshot",{description:"Read a tool's current state (its sync snapshot) from a device. Starts watching the tool and returns the next fresh snapshot.",inputSchema:{toolId:import_zod.z.string().describe("The tool id to read state from."),deviceId:import_zod.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),timeoutMs:import_zod.z.number().optional().describe("How long to wait for a fresh snapshot before falling back to the cached one.")}},async({toolId:t,deviceId:n,timeoutMs:s})=>{try{const o=r.requireProDevice(n);const{payload:a}=await r.getSnapshot(t,o,{timeoutMs:s});return json({toolId:t,deviceId:o,snapshot:a})}catch(o){return fail(errMsg(o))}});e.registerTool("call_action",{description:"Invoke any action a tool exposes (see list_tool_actions) and return its result. This is the generic escape hatch that works for every Buoy tool.",inputSchema:{toolId:import_zod.z.string().describe('The tool id, e.g. "perf-monitor".'),action:import_zod.z.string().describe('The action name, e.g. "startRecording", "clearAll".'),params:import_zod.z.record(import_zod.z.unknown()).optional().describe("JSON params object for the action (shape depends on the action)."),deviceId:import_zod.z.string().optional().describe('Target device id. Omit to auto-select one device, or "All" to broadcast.'),timeoutMs:import_zod.z.number().optional().describe("Max time to wait for the device's result.")}},async({toolId:t,action:n,params:s,deviceId:o,timeoutMs:a})=>{try{const i=r.requireProDevice(o);const c=await r.callAction(t,n,s,i,{timeoutMs:a});return json({toolId:t,action:n,ok:true,result:c??null})}catch(i){return fail(errMsg(i))}})}function errMsg(e){return e instanceof Error?e.message:String(e)}var import_zod3=require("zod");var import_zod2=require("zod");var text=e=>({content:[{type:"text",text:e}]});var fail2=e=>({isError:true,content:[{type:"text",text:e}]});var errMsg2=e=>e instanceof Error?e.message:String(e);var deviceIdInput=import_zod2.z.string().optional().describe("Target device id (auto-selected if one is connected).");function clampLimit(e,r){return typeof e==="number"&&e>0?e:r}function staleHeader(e){return e?`${e}
3
3
 
4
4
  `:""}function inactiveToolMessage(e){return`No ${e.toolId} result from the device. The ${e.toolId} tool may be inactive, or the app needs a newer Buoy (${e.mcpTool} requires the ${e.action} action).`}function withDevice(e,r){return async t=>{try{const n=e.requireProDevice(t.deviceId);return await r(t,n)}catch(n){return fail2(errMsg2(n))}}}var PERF_TOOL_ID="perf-monitor";var clamp=(e,r,t)=>Math.max(r,Math.min(t,e));var WARMUP_CASE_NAME="__batch_warmup__";var THERMAL_MARK={nominal:"\u{1F7E2} nominal",fair:"\u{1F7E1} fair",serious:"\u{1F7E0} serious",critical:"\u{1F534} critical",unknown:"\u2014"};var FALLBACK={perCaseDurationMs:5e3,settleMs:600,navTimeoutMs:5e3,runsPerCase:3,coolDownMs:2e3,discardWarmupRuns:0};function pick(e,r,t){return e??r??t}function buildAutomationConfig(e,r={}){const t=[];const n=(e.targetRoute||"").trim();if(!n)throw new Error("targetRoute is required (the screen to benchmark).");let s=(e.bounceRoute||"").trim();if(!s)s=n==="/"?"/_buoy_bounce":"/";if(s===n){throw new Error(`bounceRoute ("${s}") must differ from targetRoute \u2014 it's the screen we bounce to so the test screen fully remounts each run.`)}if(!Array.isArray(e.cases)||e.cases.length===0){throw new Error("At least one case is required.")}const o=e.cases.map((l,m)=>{const y=(l.name||"").trim()||`case ${m+1}`;return{id:`case-${m}`,name:y,params:l.params??{},route:l.route}});const a=clamp(Math.round(pick(e.runsPerCase,r.runsPerCase,FALLBACK.runsPerCase)),1,10);const i=clamp(Math.round(pick(e.coolDownMs,r.coolDownMs,FALLBACK.coolDownMs)),0,3e4);const c=clamp(Math.round(pick(e.discardWarmupRuns,r.discardWarmupRuns,FALLBACK.discardWarmupRuns)),0,a-1);const d=e.shuffleCases??r.shuffleCases??o.length>3;if(o.length>3&&d===false){t.push("shuffleCases is off with >3 cases \u2014 later cases may be thermally throttled, biasing results. Consider enabling it.")}if(a===1){t.push("runsPerCase=1 \u2014 no median/variance; a single noisy run can mislead. 3+ is recommended.")}const u={targetRoute:n,bounceRoute:s,perCaseDurationMs:clamp(Math.round(pick(e.perCaseDurationMs,r.perCaseDurationMs,FALLBACK.perCaseDurationMs)),1e3,6e4),settleMs:clamp(Math.round(pick(e.settleMs,r.settleMs,FALLBACK.settleMs)),0,1e4),navTimeoutMs:clamp(Math.round(pick(e.navTimeoutMs,r.navTimeoutMs,FALLBACK.navTimeoutMs)),1e3,3e4),cases:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,discardWarmupCase:e.discardWarmupCase??r.discardWarmupCase??true,shuffleCases:d,reloadBetweenCases:e.reloadBetweenCases??r.reloadBetweenCases??false,captureRenders:e.captureRenders??r.captureRenders??true};return{config:u,warnings:t}}function describeBenchmarkSettings(e){const r=c=>c>=1e3?`${(c/1e3).toFixed(c%1e3?1:0)}s`:`${c}ms`;const t=e.runsPerCase??1;const n=estimateBatchMs(e);const s=n/6e4;const o=s>=1?`~${s.toFixed(1)} min`:`~${Math.round(n/1e3)}s`;const a=[`**${r(e.perCaseDurationMs)} measured per page**`,t>1?`**${t} runs each** (median taken)`:`**1 run each** (no median \u2014 noisier)`,e.reloadBetweenCases?"**full reload between pages** (no state leaks across tests)":"no reload between pages",e.shuffleCases?"shuffled order (cancels thermal bias)":"fixed order",e.captureRenders!==false?"render capture on (per-component why-data)":"render capture off"];const i=e.settleMs+e.perCaseDurationMs+(e.coolDownMs??0)+1800;return`${a.join(", ")}. That's ~${r(i)} wall-clock per page; for ${e.cases.length} page(s) \xD7 ${t} run(s), **${o}** total.`}function estimateBatchMs(e){const r=1800;const t=e.settleMs+e.perCaseDurationMs+e.coolDownMs+r;let n=e.cases.length*(e.runsPerCase??1)*t;if(e.reloadBetweenCases)n+=e.cases.length*3e3;return n}function computeFlags(e){const r=[];const t=e.deviceMaxRefreshRate;if(e.uiFps!=null&&t&&e.uiFps<t*.95){r.push("UI thread drops frames")}else if(e.uiJank!=null&&e.uiJank>=2){r.push("UI jank spikes")}if(e.cpu!=null&&e.cpu>=90)r.push("CPU near cap");if(e.jsJank!=null&&e.jsJank>=5)r.push("JS thread stalls");return r}function rankBatchFromIndex(e,r,t,n=[]){const s=e.filter(h=>h.batchId===r);const o=new Map(n.map(h=>[h.index,h.reason]));const a=Array.from(new Set(s.map(h=>h.batchIndex??0))).sort((h,f)=>h-f);const i=h=>{const f=s.filter(g=>(g.batchIndex??0)===h);if(f.length===0)return void 0;return f.find(g=>g.isMedianRun===true)??f.find(g=>g.runIndex===0||g.runIndex===void 0)??f[0]};const c=h=>typeof h==="number"?h:null;const d=a.map(h=>{const f=i(h);const g=s.filter(A=>(A.batchIndex??0)===h).length;const v=f?.name??t?.[h]?.name??`case ${h+1}`;const $=o.get(h)??f?.batchFailureReason;const _=Boolean($)||!f;const k={batchIndex:h,caseName:v,params:f?.params??{},runs:g,failed:_,failureReason:_?$??"no run recorded":void 0,uiFps:c(f?.uiFpsAvg),jsFps:c(f?.jsFpsAvg),cpu:c(f?.cpuAvg),memMb:c(f?.memMaxMb),jsJank:c(f?.jsJankFrames),uiJank:c(f?.uiJankFrames),deviceMaxRefreshRate:c(f?.deviceMaxRefreshRate),deltaUiFps:null,deltaJsFps:null,...f?.thermalState?{thermalState:f.thermalState}:{},renderCommits:c(f?.renderCommits),renderWasted:f?.renderWasted,topRenderers:f?.topRenderers??[]};return{...k,flags:_?[]:computeFlags(k)}}).filter(h=>h.caseName!==WARMUP_CASE_NAME).map((h,f)=>({...h,batchIndex:f}));const u=d[0];if(u){for(const h of d){if(h===u)continue;if(h.uiFps!=null&&u.uiFps!=null)h.deltaUiFps=round(h.uiFps-u.uiFps,1);if(h.jsFps!=null&&u.jsFps!=null)h.deltaJsFps=round(h.jsFps-u.jsFps,1)}}const l=d.filter(h=>!h.failed);const m=(h,f)=>{let g;let v=NaN;for(const $ of l){const _=h($);if(_==null)continue;if(g===void 0||f(_,v)){g=$;v=_}}return g?.caseName};const y=h=>h.jsJank==null&&h.uiJank==null?null:(h.jsJank??0)+(h.uiJank??0);const b={uiFps:m(h=>h.uiFps,(h,f)=>h>f),jsFps:m(h=>h.jsFps,(h,f)=>h>f),lowestCpu:m(h=>h.cpu,(h,f)=>h<f),lowestMem:m(h=>h.memMb,(h,f)=>h<f),fewestJank:m(y,(h,f)=>h<f)};const p=l.filter(h=>h.flags.length>0).map(h=>({caseName:h.caseName,flags:h.flags}));const w=l.filter(h=>h.flags.length===0).sort((h,f)=>(f.uiFps??0)-(h.uiFps??0)||(h.cpu??Infinity)-(f.cpu??Infinity)||(h.memMb??Infinity)-(f.memMb??Infinity)).map(h=>h.caseName);return{batchId:r,baselineName:u?.caseName??null,deviceMaxRefreshRate:l.find(h=>h.deviceMaxRefreshRate!=null)?.deviceMaxRefreshRate??null,rows:d,leaders:b,flagged:p,balanced:w}}function formatRankingMarkdown(e){const r=(l,m=1)=>l==null?"\u2014":l.toFixed(m);const t=l=>l==null?"":l>0?` (+${l.toFixed(1)})`:` (${l.toFixed(1)})`;const n=[];n.push(`### Benchmark batch \`${e.batchId}\``);n.push(`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`);n.push("");const s=e.rows.some(l=>l.thermalState);const o=s?" Thermal |":"";const a=s?"-------|":"";n.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${o} Flags |`);n.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${a}-------|`);for(const l of e.rows){const m=s?` ${THERMAL_MARK[l.thermalState??"unknown"]??"\u2014"} |`:"";if(l.failed){n.push(`| ${l.batchIndex} | ${l.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${m} \u274C FAILED (${l.failureReason}) |`);continue}const y=l.flags.length?`\u26A0\uFE0F ${l.flags.join("; ")}`:"\u2713 clean";n.push(`| ${l.batchIndex} | ${l.caseName} | ${r(l.uiFps)}${t(l.deltaUiFps)} | ${r(l.jsFps)}${t(l.deltaJsFps)} | ${r(l.cpu)} | ${r(l.memMb)} | ${r(l.jsJank,0)} | ${r(l.uiJank,0)} |${m} ${y} |`)}const i=e.rows.filter(l=>l.thermalState==="serious"||l.thermalState==="critical");if(i.length){n.push("");n.push(`**\u{1F525} Thermally throttled: ${i.map(l=>l.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`)}n.push("");const c=e.leaders;const d=[c.uiFps&&`UI FPS \u2192 **${c.uiFps}**`,c.jsFps&&`JS FPS \u2192 **${c.jsFps}**`,c.lowestCpu&&`lowest CPU \u2192 **${c.lowestCpu}**`,c.lowestMem&&`lowest mem \u2192 **${c.lowestMem}**`,c.fewestJank&&`fewest jank \u2192 **${c.fewestJank}**`].filter(Boolean).join(" \xB7 ");if(d)n.push(`**Per-metric best:** ${d}`);n.push(e.balanced.length?`**No configured risk flags:** ${e.balanced.join(", ")}. Check missing metrics and workload requirements before choosing.`:`**No configured risk flags:** no candidates listed. Review failures, available metrics and flags before choosing.`);if(e.flagged.length){n.push(`**\u26A0\uFE0F At-risk:** ${e.flagged.map(l=>`${l.caseName} (${l.flags.join("; ")})`).join(" \xB7 ")}`)}const u=e.rows.filter(l=>!l.failed&&(l.renderCommits!=null||l.topRenderers.length>0)).map(l=>{const m=l.topRenderers.map(p=>`${p.name} \xD7${p.renders} (${p.totalMs}ms${p.cause?`, ${p.cause}`:""})`).join(", ");const y=l.renderCommits!=null?`${l.renderCommits} commits`:"";const b=l.renderWasted!=null?`${l.renderWasted} wasted`:"";return`- **${l.caseName}** \u2014 ${[y,b,m].filter(Boolean).join(": ")}`});if(u.length){n.push("");n.push("**Renders (median run)** \u2014 `wasted` = parent cascades + identity-only prop churn (removable). Cause per component: `propsUnstable` means a parent handed it a fresh function/object with unchanged values \u2014 fix upstream.");n.push(...u)}n.push("");n.push(`_Deltas vs baseline (case #0). Don't pick on one metric \u2014 explain the trade-off (e.g. "A is cheapest but drops UI frames \u2192 jank risk; B costs more CPU but caps nothing")._`);return n.join("\n")}function compareReportStats(e,r){const t=(n,s)=>({a:{avg:round(n.avg,1),p95:round(n.p95,1)},b:{avg:round(s.avg,1),p95:round(s.p95,1)},deltaAvg:round(s.avg-n.avg,1)});return{a:e.name,b:r.name,uiFps:t(e.stats.uiFps,r.stats.uiFps),jsFps:t(e.stats.jsFps,r.stats.jsFps),cpuUsage:t(e.stats.cpuUsage,r.stats.cpuUsage),memoryUsage:t(e.stats.memoryUsage,r.stats.memoryUsage),jsJankFrames:{a:e.stats.jsJankFrames,b:r.stats.jsJankFrames,delta:r.stats.jsJankFrames-e.stats.jsJankFrames},uiJankFrames:{a:e.stats.uiJankFrames,b:r.stats.uiJankFrames,delta:r.stats.uiJankFrames-e.stats.uiJankFrames},...compareRenders(e.renders,r.renders)}}function compareRenders(e,r){if(!e&&!r)return null;const t=n=>n??0;return{renders:{total:{a:t(e?.totalRenders),b:t(r?.totalRenders),delta:t(r?.totalRenders)-t(e?.totalRenders)},commits:{a:t(e?.totalCommits),b:t(r?.totalCommits),delta:t(r?.totalCommits)-t(e?.totalCommits)},wasted:{a:t(e?.wastedRenders),b:t(r?.wastedRenders),delta:t(r?.wastedRenders)-t(e?.wastedRenders)},causes:{a:e?.causes??{},b:r?.causes??{}}}}}function round(e,r){const t=Math.pow(10,r);return Math.round(e*t)/t}var POLL_INTERVAL_MS=1500;var delay2=e=>new Promise(r=>setTimeout(r,e));var json2=e=>({content:[{type:"text",text:JSON.stringify(e,null,2)}]});var caseSchema=import_zod3.z.object({name:import_zod3.z.string().describe("Display name for this variant (becomes the saved run's name)."),params:import_zod3.z.record(import_zod3.z.string()).optional().describe('Query params applied to the route for this case, e.g. { "renderer": "v2", "count": "100" }.'),route:import_zod3.z.string().optional().describe("Optional per-case route override; defaults to targetRoute.")});async function readDeviceSettings(e,r){try{const t=await e.callAction(PERF_TOOL_ID,"getAutomationConfig",{},r);if(!t)return{};const{perCaseDurationMs:n,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}=t;return{perCaseDurationMs:n,settleMs:s,navTimeoutMs:o,runsPerCase:a,coolDownMs:i,discardWarmupRuns:c,shuffleCases:d,reloadBetweenCases:u,captureRenders:l}}catch{return{}}}async function readPerfSnapshot(e,r){const{payload:t}=await e.getSnapshot(PERF_TOOL_ID,r,{timeoutMs:4e3});return t}function batchIdsIn(e){const r=new Set;for(const t of e??[])if(t.batchId)r.add(t.batchId);return r}function runCountFor(e,r){return(e??[]).filter(t=>t.batchId===r).length}function newestBatchId(e){let r=null;for(const t of e??[]){if(t.batchId&&(!r||t.createdAt>r.at))r={id:t.batchId,at:t.createdAt}}return r?.id??null}function failuresFromIndex(e,r){const t=new Map;for(const n of e??[]){if(n.batchId===r&&n.batchFailureReason&&!t.has(n.batchIndex??0)){t.set(n.batchIndex??0,n.batchFailureReason)}}return[...t.entries()].map(([n,s])=>({index:n,reason:s}))}function expectedRunsFor(e){const r=e.cases.length+(e.discardWarmupCase===true?1:0);return r*(e.runsPerCase??1)}var SETTLE_WAIT_MS=1e4;function isRunnerBusy(e){return!!e&&e!=="idle"&&e!=="done"&&e!=="cancelled"}async function waitForRunnerSettled(e,r,t,n=SETTLE_WAIT_MS){const s=Date.now();while(Date.now()-s<n){const o=await readPerfSnapshot(e,r).catch(()=>null);if(o&&!isRunnerBusy(o.live?.automation?.phase))return true;await delay2(POLL_INTERVAL_MS)}t(`automation still busy after ${Math.round(n/1e3)}s`);return false}async function waitForBatchByIndex(e,r,t){const{preBatchIds:n,expectedRuns:s,maxWaitMs:o,log:a}=t;const i=Date.now();let c="";let d=false;let u=0;while(Date.now()-i<o){let y;try{y=await readPerfSnapshot(e,r)}catch{await delay2(POLL_INTERVAL_MS);continue}const b=y.index??[];const p=y.live?.automation?.phase??"idle";const w=y.live?.automationCompleted;if(w?.phase==="cancelled"&&!n.has(w.batchId)){return{batchId:w.batchId,failures:[],cancelled:true}}if(w?.phase==="done"&&!n.has(w.batchId)){return{batchId:w.batchId,failures:w.failures??failuresFromIndex(b,w.batchId)}}if(p!=="idle")d=true;if(p!==c){c=p;a(`automation phase: ${p}`)}const h=[...batchIdsIn(b)].find(f=>!n.has(f));if(h){const f=runCountFor(b,h);if(s>0&&f>=s){await waitForRunnerSettled(e,r,a);a(`batch ${h} complete (${f}/${s} runs saved)`);return{batchId:h,failures:failuresFromIndex(b,h)}}if(p==="idle"&&d&&f>0){if(++u>=3){a(`batch ${h} settled (${f} runs, automation idle)`);return{batchId:h,failures:failuresFromIndex(b,h)}}}else{u=0}}await delay2(POLL_INTERVAL_MS)}const l=await readPerfSnapshot(e,r).catch(()=>null);const m=l?[...batchIdsIn(l.index)].find(y=>!n.has(y)):void 0;if(m&&l)return{batchId:m,failures:failuresFromIndex(l.index,m),timedOut:true};throw new Error(`Batch did not start within ${Math.round(o/1e3)}s (last phase: ${c||"idle"}). Check get_automation_status \u2014 the device may not have navigated.`)}function registerPerfMonitorTools(e,r){e.registerTool("run_benchmark_batch",{description:"Run a perf-monitor benchmark batch and return per-metric leaders, risk flags and a comparison of available case results. Give it a target route and a matrix of cases (each a name + query params that select a variant on that route); it navigates, records, and benchmarks each case on the device, then ranks them. This blocks until the batch completes (can be minutes for large batches). The app must have the target route render its variants from the params.",inputSchema:{targetRoute:import_zod3.z.string().describe('The screen to benchmark, e.g. "/perf-test". Reads case params to pick a variant.'),cases:import_zod3.z.array(caseSchema).min(1).describe("The variants to compare. Case #0 is the baseline for deltas."),bounceRoute:import_zod3.z.string().optional().describe('Screen to bounce to between runs so the test screen fully remounts. Defaults to "/" (or a stub if target is "/").'),runsPerCase:import_zod3.z.number().optional().describe("Runs per case; the median becomes the canonical result. Default 3 (1\u201310)."),perCaseDurationMs:import_zod3.z.number().optional().describe("How long to record each run. Default 5000."),settleMs:import_zod3.z.number().optional().describe("Idle gap after navigation before recording. Default 600."),coolDownMs:import_zod3.z.number().optional().describe("Idle between runs/cases for thermal recovery. Default 2000."),navTimeoutMs:import_zod3.z.number().optional().describe("Max wait for navigation per run. Default 5000."),discardWarmupRuns:import_zod3.z.number().optional().describe("Discard first N runs of each case before taking the median. Default 0."),shuffleCases:import_zod3.z.boolean().optional().describe("Interleave and shuffle case order to reduce order-related bias. Default on for >3 cases."),reloadBetweenCases:import_zod3.z.boolean().optional().describe("Fully reload JS between cases (needs <AutomationResumer/> mounted). Default false."),captureRenders:import_zod3.z.boolean().optional().describe("Capture per-component render counts/durations during each run (needs @buoy-gg/highlight-updates installed; dev builds only). Adds instrumentation overhead that can vary with the workload. Default true."),deviceId:import_zod3.z.string().optional().describe("Target device id. Omit to auto-select when one device is connected."),maxWaitMs:import_zod3.z.number().optional().describe("Safety cap for how long to wait for completion. Defaults to ~3\xD7 the estimate + 60s.")}},async t=>{const n=s=>process.stderr.write(`[buoy-mcp] ${s}
5
5
  `);try{const s=r.requireProDevice(t.deviceId);const o=await readDeviceSettings(r,s);const{config:a,warnings:i}=buildAutomationConfig(t,o);const c=estimateBatchMs(a);const d=t.maxWaitMs??c*3+6e4;const u=expectedRunsFor(a);if(!await waitForRunnerSettled(r,s,n)){return fail2("Another benchmark batch is still running on this device. Wait for it to finish, or cancel it (call_action perf-monitor cancelAutomation), then retry.")}await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"setEnabled",{enabled:true},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const l=await readPerfSnapshot(r,s);const m=batchIdsIn(l.index);n(`starting batch: ${a.cases.length} cases \xD7 ${a.runsPerCase} runs (~${Math.round(c/1e3)}s estimated) on ${s}`);await r.callAction(PERF_TOOL_ID,"startAutomation",{config:a},s);const y=await waitForBatchByIndex(r,s,{preBatchIds:m,expectedRuns:u,maxWaitMs:d,log:n});if(y.cancelled){await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});return fail2(`Batch ${y.batchId} was cancelled before completing.`)}await r.callAction(PERF_TOOL_ID,"refreshIndex",{},s).catch(()=>{});const b=await readPerfSnapshot(r,s);const p=rankBatchFromIndex(b.index??[],y.batchId,a.cases,y.failures);await r.callAction(PERF_TOOL_ID,"acknowledgeAutomation",{},s).catch(()=>{});await r.callAction(PERF_TOOL_ID,"setEnabled",{enabled:false},s).catch(()=>{});const w=formatRankingMarkdown(p);const h={batchId:y.batchId,leaders:p.leaders,balanced:p.balanced,flagged:p.flagged,rows:p.rows,failures:y.failures,warnings:i,estimateMs:c,...y.timedOut?{note:"Wait cap hit \u2014 results may be partial (some runs may still be saving)."}:{}};return text(`${w}
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{ALL_DEVICES as e,BROKER_PORT as o,BROKER_URL as r,DASHBOARD_NAME as t,DEFAULT_ACTION_TIMEOUT_MS as s,DEFAULT_SNAPSHOT_TIMEOUT_MS as i,PACKAGE_NAME as T,SyncClient as n,VERSION as a,buildAutomationConfig as g,compareReportStats as l,createMcpServer as E,estimateBatchMs as R,formatRankingMarkdown as A,getUpdateNotice as S,rankBatchFromIndex as _,registerConsoleTools as c,registerEventsTools as M,registerGenericTools as O,registerJotaiTools as m,registerPerfMonitorTools as u,registerReactQueryTools as I,registerReduxTools as d,registerRoutesTools as p,registerScreenshotTools as C,registerStorageTools as N,registerZustandTools as U,runInit as B}from"./chunk-OOYI5S34.mjs";export{e as ALL_DEVICES,o as BROKER_PORT,r as BROKER_URL,t as DASHBOARD_NAME,s as DEFAULT_ACTION_TIMEOUT_MS,i as DEFAULT_SNAPSHOT_TIMEOUT_MS,T as PACKAGE_NAME,n as SyncClient,a as VERSION,g as buildAutomationConfig,l as compareReportStats,E as createMcpServer,R as estimateBatchMs,A as formatRankingMarkdown,S as getUpdateNotice,_ as rankBatchFromIndex,c as registerConsoleTools,M as registerEventsTools,O as registerGenericTools,m as registerJotaiTools,u as registerPerfMonitorTools,I as registerReactQueryTools,d as registerReduxTools,p as registerRoutesTools,C as registerScreenshotTools,N as registerStorageTools,U as registerZustandTools,B as runInit};
1
+ import{ALL_DEVICES as e,BROKER_PORT as o,BROKER_URL as r,DASHBOARD_NAME as t,DEFAULT_ACTION_TIMEOUT_MS as s,DEFAULT_SNAPSHOT_TIMEOUT_MS as i,PACKAGE_NAME as T,SyncClient as n,VERSION as a,buildAutomationConfig as g,compareReportStats as l,createMcpServer as E,estimateBatchMs as R,formatRankingMarkdown as A,getUpdateNotice as S,rankBatchFromIndex as _,registerConsoleTools as c,registerEventsTools as M,registerGenericTools as O,registerJotaiTools as m,registerPerfMonitorTools as u,registerReactQueryTools as I,registerReduxTools as d,registerRoutesTools as p,registerScreenshotTools as C,registerStorageTools as N,registerZustandTools as U,runInit as B}from"./chunk-FLZAEJH4.mjs";export{e as ALL_DEVICES,o as BROKER_PORT,r as BROKER_URL,t as DASHBOARD_NAME,s as DEFAULT_ACTION_TIMEOUT_MS,i as DEFAULT_SNAPSHOT_TIMEOUT_MS,T as PACKAGE_NAME,n as SyncClient,a as VERSION,g as buildAutomationConfig,l as compareReportStats,E as createMcpServer,R as estimateBatchMs,A as formatRankingMarkdown,S as getUpdateNotice,_ as rankBatchFromIndex,c as registerConsoleTools,M as registerEventsTools,O as registerGenericTools,m as registerJotaiTools,u as registerPerfMonitorTools,I as registerReactQueryTools,d as registerReduxTools,p as registerRoutesTools,C as registerScreenshotTools,N as registerStorageTools,U as registerZustandTools,B as runInit};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/mcp",
3
3
  "mcpName": "io.github.Buoy-gg/mcp",
4
- "version": "7.0.42",
4
+ "version": "7.0.43",
5
5
  "description": "MCP server exposing supported Buoy inspection and action tools through the device sync protocol. Tool access requires account and Pro setup.",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -32,8 +32,8 @@
32
32
  "socket.io-client": "^4.8.1",
33
33
  "zod": "^3.25.0",
34
34
  "ws": "^8.18.0",
35
- "@buoy-gg/license": "^7.0.42",
36
- "@buoy-gg/sync-broker": "^7.0.42"
35
+ "@buoy-gg/license": "^7.0.43",
36
+ "@buoy-gg/sync-broker": "^7.0.43"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.0.0",